深入理解Redis分布式锁 - Go语言中文社区

深入理解Redis分布式锁


背景

项目中有个场景是做数据分析,然后将目标数据保存在db,有个要求是将同类型的目标数据值保存一次,因此每次保存时需要判断之前是否已经存在了,由于qps较高,之前使用redis来缓存已经保存的数据,来抵挡对db的大部分流量,最近业务拓展同类型目标数据的qps可能超大(极限时达到20Wqps以上),此时由于在分布式场景下redis查询未加锁,可能多个线程同时查询redis得到的结果为并未保存,此时问题发生了,可能同时有多个线程执行保存到db操作,虽然db做了unique Key的限制,但担心到db qps过高导致被打挂,影响服务正常运行,因此准备使用redis分布式锁,来阻止此类事故的发生。
总结而言,我们担心的问题有以下特征:

  • 分布式场景
  • 流量qps 特高,redis不加锁可能直接击穿db

在分布式场景下,我们需要同类型数据保存操作在多个节点不会重复执行,因此需要使用redis分布式锁来实现。
下面我们先简单学习下分布式锁的概念以及如何在redis中实现:

分布式锁

我们参考Martin Kleppmann大佬(很牛的位大佬,有兴趣的同学可以了解下其跟redis之父Antirez关于RedLock(红锁,后续有讲到)是否安全的激烈讨论)的文章 How to do distributed locking 来理解redis锁。
文章地址:https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

The purpose of a lock is to ensure that among several nodes that might try to do the same piece of work, only one actually does it (at least only one at a time)

分布式锁的作用在于在分布式场景下,当有多个服务结点尝试执行相同的任务,通过该锁确保实际只有1个结点执行。显然这样可以节省资源,避免重复执行,也能减少重复执行导致的错误,此处也涉及到分布式锁的两个特征场景:

  • 效率:使用分布式锁避免不同节点执行重复的任务,导致资源浪费。
  • 正确性;加分布式锁同样可以避免破坏正确性的发生,如果两个节点在同一条数据上面操作。比如多个节点机器对同一个用户进行转账5美元的操作,如果不加锁可能导致实际给用户转账了多个5美元,造成资损。

Martin Kleppmann文章中提到了常见的分布式锁RedLock,也支持其不能保证很高的正确性:
下面我们来学习下RedLock的使用,以及其为什么不够正确。

RedLock

官方文档:https://redis.io/docs/reference/patterns/distributed-locks/
分布式锁,主要用于多进程间互斥访问共享资源,RedLock是redis实现分布式锁的算法。

单Redis实例实现分布式锁的正确方法

首先我们了解下单实例下RedLock如何实现分布式锁,通过执行以下命令获取锁:

SET resource_name my_random_value NX PX 30000

这个命令因为加了NX,只有在key(resource_name)不存在时才能被执行成功,并且通过PX加了30s的自动失效时间,对应的值时一个随机数my_random_value,该值需确保在所有redis客户端都是唯一的,value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

使用这种方式释放锁可以避免删除别的客户端获取成功的锁。例如:client A获取资源锁后,此时由于其他操作阻塞了,若阻塞时间超过了锁的超时时间,当client A运行完阻塞任务,进行释放锁的时候,此时锁可能已经被其他client持有,此时client A将删除其他客户端的锁。上述Lua脚本在释放锁时判断锁的值是否为客户端的唯一值,可以避免释放其他客户端的锁。
key的失效时间,被称作"锁定有效期"。它不仅是key自动失效时间,而且还是一个客户端持有锁多长时间后可以被另外一个客户端重新获得。

截至到目前,我们已经有较好的方法获取锁和释放锁。基于Redis单实例,假设这个单实例总是可用,这种方法已经足够安全。现在让我们扩展一下,假设Redis没有总是可用的保障。

分布式环境实现RedLock

在redis的分布式环境,假设有N个redis master节点。这些节点完全相互独立,不存在主从复制或者和其他集群协调机制,确保将在每(N)个实例上使用此方法获取和释放锁。在这个样例中,我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时都宕掉。为了取到锁,客户端应该执行以下操作:

  1. 获取当前Unix时间,以毫秒为单位。
  2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
这个算法是异步的么?

算法基于这样一个假设:虽然多个进程之间没有时钟同步,但每个进程都以相同的时钟频率前进,时间差相对于失效时间来说几乎可以忽略不计。这种假设和我们的真实世界非常接近:每个计算机都有一个本地时钟,我们可以容忍多个计算机之间有较小的时钟漂移。

从这点来说,我们必须再次强调我们的互相排斥规则:只有在锁的有效时间(在步骤3计算的结果)范围内客户端能够做完它的工作,锁的安全性才能得到保证(锁的实际有效时间通常要比设置的短,因为计算机之间有时钟漂移的现象)。.

想要了解更多关于需要时钟漂移间隙的相似系统, 这里有一个非常有趣的参考: Leases: an efficient fault-tolerant mechanism for distributed file cache consistency.

失败时重试

当客户端无法取到锁时,应该在一个随机延迟后重试,防止多个客户端在同时抢夺同一资源的锁(这样会导致脑裂,没有人会取到锁)。同样,客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低(必要的重试),所以,理想情况一下,客户端应该同时(并发地)向所有Redis发送SET命令。

需要强调,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,这样其他的客户端就不必非得等到锁过完"有效时间"才能取到(然而,如果已经存在网络分裂,客户端已经无法和Redis实例通信,此时就只能等待key的自动释放了,等于被惩罚了)。

释放锁

释放锁比较简单,向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.

安全性

这个算法安全么?我们可以从不同的场景讨论一下。

让我们假设客户端从大多数Redis实例取到了锁。所有的实例都包含同样的key,并且key的有效时间也一样。然而,key肯定是在不同的时间被设置上的,所以key的失效时间也不是精确的相同。我们假设第一个设置的key时间是T1(开始向第一个server发送命令前时间),最后一个设置的key时间是T2(得到最后一台server的答复后的时间),我们可以确认,第一个server的key至少会存活 MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT{.highlighter-rouge}。所有其他的key的存活时间,都会比这个key时间晚,所以可以肯定,所有key的失效时间至少是MIN_VALIDITY。

当大部分实例的key被设置后,其他的客户端将不能再取到锁,因为至少N/2+1个实例已经存在key。所以,如果一个锁被(客户端)获取后,客户端自己也不能再次申请到锁(违反互相排斥属性)。

然而我们也想确保,当多个客户端同时抢夺一个锁时不能两个都成功。

如果客户端在获取到大多数redis实例锁,使用的时间接近或者已经大于失效时间,客户端将认为锁是失效的锁,并且将释放掉已经获取到的锁,所以我们只需要在有效时间范围内获取到大部分锁这种情况。在上面已经讨论过有争议的地方,在MIN_VALIDITY{.highlighter-rouge}时间内,将没有客户端再次取得锁。所以只有一种情况,多个客户端会在相同时间取得N/2+1实例的锁,那就是取得锁的时间大于失效时间(TTL time),这样取到的锁也是无效的.

关于安全性也可以参考Martin Kleppmann的文章进行理解,在某些场景下RedLock并不能保证安全性,可以自行探究下。https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

RedLock的java实现Redisson

在了解RedLock实现后,我们简单说下其用java的实现Redissson如何使用,
使用redisson实现分布式锁的操作步骤,三个步骤

  • 第一步: 获取锁 RLock redissonLock = redisson.getLock(lockKey);
  • 第二步: 加锁,实现锁续命功能 redissonLock.lock();
  • 第三步:释放锁 redissonLock.unlock();

image.png

总结

redlock或redis recipes推荐的锁的实现, 都不是绝对安全的.

如果你需要处理敏感数据, 不要依赖它们来保证数据可靠.

但是, redis锁只会在比较极端的情况下出错, 所以如果你处在一个仅仅需要保证数据大部分时候可靠, 万一有问题也无所谓的情况下, 那么放心的使用单节点redis或主从集群来加锁吧. 至于redlock, 如果你不在乎需要多维护几个独立的redis服务, 那么它可能比单节点redis出错的概率稍低, 仅此而已

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_44271683/article/details/125024703
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2023-01-03 18:01:41
  • 阅读 ( 512 )
  • 分类:Redis

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢