在普通的单机程序中,我们为了避免资源竞争,通常会使用synchronize
、lock
等方式进行加锁防止并发问题。不过在分布式系统中,请求是并发的在多台机器上执行,这时候就需要使用分布式锁来防止资源竞争问题。
提到分布式锁,我们最常见的方案就是基于redis实现,本文将依次解释
- 如何用redis实现一个基本的分布式锁
- 基本方案中还有哪些问题?如何解决?
redis分布式锁
我们知道redis由于其单线程的模式,可以保证各命令按顺序、原子的执行。
Redis的分布式锁主要使用了setnx
命令。
- 加锁。使用
setnx key value
,当key不存在时,设置成功表示获取到锁,否则认为获取锁失败。 - 解锁。使用
del key
,删除缓存对应的key表示成功释放锁。
一般来说,为了防止发生由于服务问题导致解锁命令未执行而造成锁一致无法被释放(死锁)的情况。会在加锁时同时使用expire key timeout
命令设置一个默认的超时时间。由于setnx
和 expire
是两个命令,为了保证原子性,可以通过lua脚本的方式进行执行。
上述方案已经可以实现一个基本的分布式锁,但是还是会有一些特殊的场景及问题需要我们去关注并解决。
分布式锁特殊场景
-
锁错误删除
举一个特殊场景的例子。线程A获取锁L,然后设置超时时间10s。然后线程B在12s时成功获取锁,然后开始执行到18s。然后线程A在15秒时执行完,就会去解锁(删除缓存key)。此时就发生了错误解锁的问题,即释放了线程B持有的锁。
这种场景,可以通过每个线程设置value都是与之唯一对应的value来解决(例如UUID)。然后在del
的时候做一个value校验来防止误删除。
-
超时解锁导致并发问题
上图中除了锁误删的情况,还因为 锁超时自动释放 进而导致了线程A和线程B并发问题。针对这种情况,一般的解决方案是为线程创建一个守护进程,来进行不断的续约操作,避免锁超时释放。
注意守护进程也需要进行value校验,防止错误的续约操作。当主线程执行完业务后,同时关闭掉守护线程即可。(如果主线程意外关闭了,守护线程也会自动关闭)
其他问题
除了上述问题,redis本身在主从切换、脑裂问题等极端情况导致的数据不一致问题,也会导致分布式锁的错误。
参考资料
https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
网友评论