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

Redis分布式锁怎么搞,聊聊实现原理和常见方法探讨

你有一个程序,部署在了三台机器上,现在有一个任务,给用户发一张优惠券”或者“扣减某个商品的库存”,这个任务在同一时间只能被一台机器上的一个程序执行一次,不然就会乱套,比如一张优惠券被发了两次,或者库存被多扣了,这个时候,你就需要一个“锁”来保证互斥性,单机程序用线程锁就行,但现在你的程序跑在多台机器上,这个锁就必须是“分布式”的,也就是所有机器都能访问和认可的一个东西。

Redis因为它速度快、单线程处理命令的特性(这很关键,避免了并发问题),自然就成了实现这种分布式锁的热门选择,它的核心思想很简单:就是大家来抢一个“钥匙”,谁先在Redis里创建了这个钥匙,谁就获得了锁,就可以去执行任务。

(来源:Redis官方文档中关于分布式锁的论述,常被称为“SETNX”方法)

最古老也是最直接的方法,就是用Redis的两个命令:SETNXDEL

  • SETNX 的意思是“SET if Not eXists”,如果键不存在,它就设置键值对,并返回1表示成功;如果键已经存在,就不做任何操作,返回0表示失败。
  • 加锁的过程就是:机器A执行 SETNX lock_key 1,如果返回1,恭喜你,加锁成功,赶紧去干活,干完活后,为了释放锁让别人能用,机器A再执行 DEL lock_key 把钥匙删掉。
  • 这时,如果机器B也尝试执行 SETNX lock_key 1,就会返回0,加锁失败,它就得等着(比如过一会儿再试),或者直接放弃。

这个方法有个致命问题:死锁,如果机器A加锁成功后,在释放锁之前突然崩溃了,那么这把锁就永远留在Redis里了,其他机器再也无法获得锁,系统就“死”了,为了解决这个问题,我们得给锁加一个“过期时间”。

Redis分布式锁怎么搞,聊聊实现原理和常见方法探讨

(来源:同样是Redis官方文档的改进方案)

改进后的方法是使用 SET 命令的一个参数组合:SET lock_key random_value NX PX 30000

  • NX 还是表示“如果不存在才设置”,保证了互斥性。
  • PX 30000 表示给这个键设置一个30000毫秒(30秒)的过期时间,这样,即使获得锁的客户端崩溃了,最多30秒后,Redis也会自动删除这把锁,避免了死锁。
  • 这里还有一个关键点:random_value(随机值),为什么值不简单地设为1,而要一个随机值(通常是UUID)呢?这是为了确保锁是由加锁者自己来释放的,考虑这个场景:机器A加锁成功,开始执行任务,这个任务执行了比较久,超过了30秒,锁被Redis自动释放了,这时机器B获得了锁,紧接着,机器A的任务执行完了,它去执行 DEL lock_key,结果把机器B刚创建的锁给删掉了!这就会导致混乱,正确的做法是,每个客户端用自己唯一的随机值作为锁的值,在释放锁的时候,要先判断一下当前锁的值是不是自己设置的那个随机值,如果是,才能删除,这个判断和删除操作必须是原子的,所以要用Lua脚本来实现:
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这套方案在大多数情况下已经够用了,但它还不是完美的,它存在一个问题,我们称之为“时钟漂移”问题,锁的过期时间是10秒,但客户端1可能因为某种原因(如GC垃圾回收)卡顿了15秒,它以为自己还持有锁,但实际上锁已经过期并被客户端2获取了,这时就可能出现两个客户端同时进入临界区的情况。

Redis分布式锁怎么搞,聊聊实现原理和常见方法探讨

(来源:Martin Kleppmann的论文《How to do distributed locking》)

为了解决更复杂的可靠性问题,Redis的作者Salvatore Sanfilippo提出了一个更严格的算法,叫做Redlock,这个算法适用于在Redis是真正分布式(多个主节点,且彼此独立)的场景下,追求更高的一致性。

Redlock的基本思路是,你不只在一台Redis实例上创建锁,而是在一个由多个(比如5个)独立的Redis主节点组成的集群上依次尝试创建锁,它的步骤是:

  1. 获取当前时间戳。
  2. 依次向5个Redis实例发送加锁命令(就是上面带NX、PX和随机值的SET命令)。
  3. 当客户端从大多数(N/2 + 1,这里是3台)Redis实例上成功获取锁时,并且总耗时小于锁的有效时间,才算加锁成功。
  4. 如果加锁失败(比如成功数没达到3个,或者总耗时超过了锁的有效期),那么客户端要向所有Redis实例发起释放锁的请求(用那个Lua脚本)。
  5. 加锁成功后,锁的真正有效时间等于最初设置的有效时间减去加锁过程的总耗时。

Redlock通过引入多节点投票机制,降低了单点Redis故障带来的风险,但它也更重、更复杂,而且它是否能提供绝对的可靠性在学术界还有争议(就是Martin Kleppmann和Redis作者之间那场著名的辩论),通常的建议是:如果你的业务场景可以接受偶尔的锁失效(比如为了性能做秒杀,偶尔超卖一两个也能接受),那么单Redis实例的锁就足够了;如果你需要万无一失的严格锁,那可能需要考虑ZooKeeper等更强调一致性的系统,而不是Redlock。

用Redis搞分布式锁,核心就是“占坑+过期时间+所有者验证”,从简单的SETNX/DEL,到带过期时间和随机值的SET命令配合Lua脚本,再到复杂的Redlock算法,都是在不同程度上为了平衡性能、可用性和一致性,选择哪种方法,完全取决于你的具体业务场景和容忍度。