分布式锁
核心需求:一个端点上锁成功之后,在它解锁之前其他端点无法上锁
但是生产环境中,我们需要考虑复杂一点的场景,比如:上锁之后,如果这个端点 hang 了,killed 了,网络不可达了...无法解锁了,怎么办?
PS: 保障锁机制的高性能和高可用又是另一个话题了。
我们需要支持超时解锁。
这样一来,除了服务异常的情况,服务正常运行时,也可能出现锁超时的情况。不管怎么设计这个超时时间,从理论上来讲,有多个端点同时上锁的可能性。程序设计时需要记住这一点。
PS: 在服务正常运行时,自动给锁续期,可以大幅降低这种情况出现的可能性。
万一多个端点同时上锁,那么有一个新的问题必须考虑清楚:如何避免解了别人的锁?
要求这个锁必须可以记录状态。解锁时发现不是自己的锁,就打个 WARN 日志跳过。
综合一下,写个伪代码:
endpointFlag = '...' # 随机串或者别的唯一标识
while 1:
status = lock(endpointFlag)
if status:
log('get lock, do_something')
do_something()
log('release lock')
unlock(endpointFlag)
break
else:
log('wait lock')
sleep(0.1)
Redis 分布式锁的方案
一些比较老的资料采用 SETNX lockKey timestamp
或者 SETNX
+ EXPIRE
实现上锁(前者需要保障各端点时间完全同步,并且需要不停 GET
检测,就不予考虑了)。
PS: 我看到过一些非常新的文章还在介绍这种方案,非常不应该。
PS: 老方案中,解决这个原子性问题的办法是采用 Lua 脚本来实现(Redis 保证 Lua 的原子性),甚至有一些库(Redisson)设计了非常精妙的处理逻辑,其思想还是非常可以借鉴。
Redis 替我们解决了这个上锁的原子性问题,2.6.12 对 SET
做了拓展,可以设置过期时间。
所以,现在上锁和设置过期时间可以通过 SET
一步完成。
# SET key value
# [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL]
# [NX|XX]
# [GET]
function lock (endpointFlag)) {
return SET lockKey endpointFlag EX 10 NX
}
function unlock (endpointFlag) {
lockedBy = GET lockKey
if lockedBy != endpointFlag:
log("locked by $lockedBy, ignore")
return
DEL lockKey
}