单机设置锁
redis提供了SET NX
命令来设置不存在的key值,以及SET PX
设置过期时间。我们可以结合这两个命令在redis做到加锁操作
锁的值最好设置为唯一的随机值,这是为了方便之后以安全的方式去释放锁
# 设置不存在的lock_test键的值为1,且过期时间为5000ms
SET lock_test 1 NX PX 5000
以下是删除特定value的锁的lua脚本,这样就可以防止删除不属于自己的锁
lua脚本是原子运行,因此并不会出现获取是目标值,删除的时候变成了其他值
if redis.call("get", KEYS[1]) == ARGV(1) then
return redis.call("del", KEYS[1])
else
return 0
end
超时同时持有锁
在上述锁中,存在一个问题——如果A获取了锁,但是由于执行时间过长,导致B也获取到过期后的锁,此时并会同时存在多方获取锁
为了解决该问题,有以下解决方案
-
将过期时间设置的足够长。
若获取到锁的任务被中止后,下一个任务需要等待较长时间才能重新获取
-
续过期时间
对业务有一定侵入
无法堵塞等待锁释放
由于上述命令都是立刻返回的,所以无法进行堵塞获取锁,因此
-
进行轮询
在锁竞争比较激烈情况下,性能损耗较大
-
采用redis发布订阅,接受锁释放成功消息
不存在ack和持久化保存机制,这可能导致出现意外的情况下,比如redis宕机、网络异常导致已经解锁的消息无法成功发送给订阅者,导致永远也无法解锁
-
redis stream
RedLock
考虑到redis主从同步集群中,如果从master获取到锁之后,就故障换成了从节点,那么就会导致锁失效,因此提出了RedLock的分布式锁算法
假设有N=5个Redis master节点,保证相互独立
- 获取当前时间(以毫秒为单位)
- 按照顺序向5个master节点请求加锁。
- 设置客户端连接和响应超时时间(超时时间要小于锁失效时间)
- 如果请求超时,则立刻尝试下一个master节点
- 超过一半master节点都获取到锁,且锁的使用时间小于锁失效时间,认为锁获取成功
- 若获取锁失败,则要在所有master节点上解锁
该算法依赖于这样的假设:虽然进程之间没有同步时钟,但每个进程中的本地时间几乎以相同的速率更新,与锁的自动释放时间相比,误差很小
那如果发生了过期,导致锁同时被多任务占有,该如何解决呢?
快过期时可以更新集群锁过期时间,比如可以用以下lua去续锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
else
return 0
end
思考——使用RedLock是个好主意吗?
Martin Kleppmann提出分布式锁的主要作用在于
- 效率:节省不必要的相同工作
- 正确性:保证由于并发导致的不正确状态、数据丢失、业务错误等
而Redlock从效率来说不如单redis节点锁,从正确性来说又比不上像zookeeper这样的共识系统
从正确性来分析说,redlock假定了以下条件
- 所有redis节点时间一致
- 网络延迟相对于过期时间较小
- 有限的进程暂停
这可能会存在以下情况
- client 1请求节点A、B、C锁,而D、E由于网络原因无法到达
- 节点C时钟调快,导致锁过期(或者C丢失锁信息)
- client 2请求节点C、D、E锁,而A、B由于网络原因无法到达
- client 1、client 2都相信已经持有锁
或者
- client 1请求A、B、C、D、E节点锁
- client 1发生较长时间的gc
- 锁在所有节点过期
- client 2获取到所有锁
- client 1完成gc后认为已经持有锁,但client 2此时也认为持有锁
antirez对此反驳道redlock记录了获取锁的时间差,并会再次检查是否超时
并且此步骤也考虑到网络延迟问题
以上两种情况都说明了redlock在这种情况下的不可靠性
Ref
- https://redis.io/docs/manual/patterns/distributed-locks/
- https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
- https://github.com/go-redsync/redsync
- https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
- http://antirez.com/news/101