当前位置:首页 > 问答 > 正文

Redis怎么确保锁释放一步到位不出错,保证原子性那点事儿

要理解Redis怎么确保锁释放不出错,核心就是“原子性”这三个字,原子性你可以把它想象成一个开关操作:按下去,灯要么亮,要么不亮,绝不会出现按下去只亮了一半或者闪烁一下又灭了的情况,在锁的释放过程中,就是要保证“判断锁是不是我的”和“删除锁”这两个动作像按开关一样,一气呵成,中间不能被打断。

为什么这个“一气呵成”这么重要呢?我们来看一个典型的错误场景,这个场景在几乎所有讨论Redis分布式锁的文章里都会提到,Redis实战》和马丁·克莱普曼(Martin Kleppmann)的相关论述中都有深入分析。

假设我们有一个分布式系统,里面有两个客户端,A和B,它们都在竞争同一把锁。

  1. 第一步: 客户端A成功获取了锁,并设置了一个过期时间,比如10秒,然后A开始执行自己的业务逻辑。
  2. 第二步: 由于某些原因(比如垃圾回收、网络延迟),A的业务逻辑执行时间超过了10秒,这时,Redis中的锁因为到了过期时间,就自动释放了。
  3. 第三步: 客户端B看到锁已经释放了,于是成功地获取了这把锁,并开始执行它自己的业务逻辑。
  4. 第四步: 就在B正在执行业务逻辑的时候,客户端A终于完成了它的漫长任务,A的任务代码最后一步是释放锁,如果释放锁的逻辑很简单,只是直接删除那个锁的Key,那么问题就来了:A会毫不犹豫地执行删除操作,但它删除的是谁的呢?此时锁已经被B持有了!A的这个操作会把B刚刚获取到的锁给删掉。
  5. 第五步: 锁被A误删后,系统可能变得混乱,因为现在没有锁的保护了,可能马上又有客户端C来加锁,和B同时操作共享资源,这就导致了数据错乱。

你看,问题的根源就在于,客户端A在释放锁的时候,它已经不再拥有这把锁了(锁因超时自动释放),但它自己并不知道,它还以为锁是自己的,于是执行了一个“越权”的删除操作。

怎么解决这个问题呢?答案就是让释放锁的操作变得“聪明”起来,让它具备原子性的判断能力,具体做法在Redis的官方文档和社区最佳实践中被广泛推荐,通常被称为“令牌检查”或“唯一值验证”。

做法其实不复杂:

  1. 在加锁时埋下伏笔: 当客户端(比如A)在设置锁的那个Key时,不要只设置一个简单的值如“1”,而是应该设置一个全局唯一的值,这个值必须能够唯一标识当前这次加锁请求,最常用的就是UUID,或者一个包含客户端ID和时间戳的复合字符串,我们把这个值称为“随机值”或“令牌”。

    • 命令类似:SET lock_key [一个唯一的随机值] NX PX 10000
  2. 在释放时验证身份: 当客户端(A)要释放锁的时候,它不能直接删除lock_key,它需要先做一个判断:当前Redis里存储的lock_key对应的值,是不是我当初设置的那个唯一随机值?

    • 如果是,说明我从加锁到释放锁的这段时间内,锁一直是我持有的,没有被超时释放,也没有被其他客户端抢走,我可以安全地删除它。
    • 如果不是,说明这把锁已经不属于我了(可能已经由B持有),那么我就不能删除它,应该直接返回释放失败。

最关键的一步来了:“判断值是否相等”和“删除Key”这两个操作,必须保证是原子性的。 如果我分开执行,先执行GET lock_key获取值,判断发现确实是我的随机值,但就在我准备执行DEL lock_key之前,锁过期了,并且被客户端C抢走了,那么我紧接着的DEL命令又会误删C的锁。

我们不能用两条Redis命令来完成释放,幸运的是,Redis支持Lua脚本,我们可以把整个逻辑写在一个Lua脚本里,然后一次性发送给Redis服务器执行,因为Lua脚本在Redis中是单线程执行的,所以在执行脚本的过程中,不会有其他命令插队进来,从而完美地保证了原子性。

一个标准的释放锁的Lua脚本看起来是这样的:

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

这个脚本的意思很简单:

  • KEYS[1]就是我们锁的Key,order_lock”。
  • ARGV[1]就是我们当初加锁时设置的那个唯一随机值。
  • 脚本的执行过程是:获取Key的值,与传入的随机值比较,如果相等就删除,否则什么都不做,整个流程在Redis内部是原子完成的。

通过这种“唯一值验证+Lua脚本原子执行”的方式,我们就实现了锁释放的“一步到位”,客户端在释放锁时,能够精准地确认自己仍然是锁的主人,从而避免了误删他人锁的严重问题,这可以说是使用Redis实现一个安全、可靠的分布式锁不可或缺的关键环节,虽然Redis后来推出了Redlock算法来解决更复杂的分布式环境下的锁问题,但上述这个保证释放原子性的基础原理,在任何一种基于Redis的锁实现中都是核心和基石。

Redis怎么确保锁释放一步到位不出错,保证原子性那点事儿