Redis 实现可靠分布式锁

Posted 开发者实验手册

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 实现可靠分布式锁相关的知识,希望对你有一定的参考价值。

摘要:在多线程共享资源的环境下,锁是非常常见且重要的组件。但实现一个可靠的锁并不轻松,这里尝试探讨一个可靠的锁该具备怎样的机制。


下面的故事,发生在一个名为初级君的程序员和名为进阶君的程序员之间。

某日,项目遇到一个需求,要给逻辑代码加锁,于是进阶君定义了两个函数,让普通程序员负责实现,分别是acquire_lock()和release_lock():

实现思路

Redis的setnx方法作用是,将key设置值为value,如果key不存在,这种情况下等同SET命令,当key存在时,什么也不做。

这个方法很适合用来实现锁,可以设计成这样:

当key不存在,客户端写入值,表明当前客户端获得锁,当key存在,表明锁被其他客户端占用。再使用循环(while)包裹代码,如果setnx写入成功则表明获得了锁,如果setnx写入失败表明获取失败,则进行重试。

初级君根据这个思路,很快写出了如下代码:

Redis 实现可靠分布式锁

写完代码之后,初级君还是很细心地思考了一会,感觉 while(true) 并不是一个很好的设计。如果出现系统异常导致锁没有被释放,那么将会有大量的客户端卡死在这个无限循环,于是增加了超时的机制,修改代码如下:

Redis 实现可靠分布式锁

进阶君review代码的时候,发现这段代码还存在较大的问题,锁没有设置超时时间,如果程序在获得锁之后系统崩溃,将导致锁无法释放。初级君想了想,想通过在setnx后面通过expire来设置超时时间,于是又修改了代码:

Redis 实现可靠分布式锁

初级君在提交代码之前又再思考了一下,发现如果程序在执行介于setnx和expires之间的时候崩溃,会导致死锁。于是加上了一个机制:在检查锁是否存在之后,还需检查锁是否设置了过时时间,如果没有设置,则为其加上超时时间。

Redis 实现可靠分布式锁

查阅Redis文档之后,初级君发现了更让人振奋的写法,在Redis 2.6.12 版本之后,set命令提供了一些参数来满足部分功能,可以通过下面的写法来保证 setnx 和 expire 的原子性。

Redis 实现可靠分布式锁

于是将代码修改如下:

Redis 实现可靠分布式锁

进阶君继续帮忙review代码,并表示

锁加了超时时间后,现在释放锁的方法可能会导致持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至错误地释放掉其他进程持有的锁,具体情况如下:

Redis 实现可靠分布式锁

所以需要增加一个机制:当前进程只能删除自身持有的锁。修改为在获取锁的时候增加唯一的id,且在删除的时候判断id是否一致。

于是初级君将acquire_lock方法和release_lock方法修改至如下:

Redis 实现可靠分布式锁

在解锁过程中,由于 Redis 的 set 和 del 并不是原子的,这个解锁过程依旧可能发生错误。即当Redis执行完get方法后,如果锁刚好过期,其他客户端就会获得锁,而当前客户端也可以通过 _lock_id == lock_id 这个判断,导致错误地删除其他客户端的锁。

解决这个问题,可以通过两个思路,一个是利用eval调用lua脚本来使set和del这两个方法保持原子性,另一个是使用watch对锁进行监听。

使用watch对锁进行监听:

Redis 实现可靠分布式锁

利用eval这个命令来执行下面的lua脚本以保持原子性:

至此,我们设计的锁具有了以下特性:

  1. 一般情况下(系统崩溃未必能保证),任何时刻同一个锁只能被唯一客户端所拥有

  2. 不会死锁,由于设置了超时限制,锁会被自动回收

  3. 同一个锁的加锁和解锁只能在同一客户端完成,避免了错误释放其他客户端锁的可能



设计缺陷:

当系统操作时间过长,锁被自动释放而导致同一时间有多个客户端拥有锁。要解决这个问题,可以在释放函数里判断锁是否存在,如果锁已经被提前释放,就将这次事件记录到日志,往后做回滚处理。

总结

一般来说,锁出现不正确的原因如下(引用《Redis in action》):

  • 持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至还可能会错误地释放掉了其他进程持有的锁。

  • 一个持有锁并打算执行长时间操作的进程已经崩溃,但其他想要获取锁的进程不知道哪个进程持有锁,也无法检测出持有锁的进程是否已经崩溃,只能白白地浪费时间等待锁被释放。

  • 在一个进程持有的锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁。

  • 上面提到的第一种情况和第三种情况同时出现,导致有多个进程获得了锁,而每个进程都以为自己是唯一一个获得锁的进程。


完成一个锁业务之后可以通过上面的标准去审视代码以判断锁是否可靠。


如果你觉得文章还ok


以上是关于Redis 实现可靠分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

使用redis构建可靠分布式锁

Redis 实现可靠分布式锁

如何设计一把可靠的分布式锁?

RedLock算法-使用redis实现分布式锁服务

关于redis实现分布式锁

redis分布式锁的正确实现