TOC

Redis 分布式锁

分布式锁

核心需求:一个端点上锁成功之后,在它解锁之前其他端点无法上锁

但是生产环境中,我们需要考虑复杂一点的场景,比如:上锁之后,如果这个端点 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
}