背景
在分布式架构下,特别是微服务架构下,很多业务场景为了解决共享资源访问的问题,都会采用分布式锁,但是不同业务场景对分布式锁的可用性要求不一样,因此出现了几种分布式锁的实现版本,这篇文章简单总结一下。
首先分布式锁需要有以下几个特性:
- 安全性: 在任意时刻,只有一个客户端可以获得锁。
- 避免死锁:客户端最终一定可以获得锁,即使持有锁的客户端在释放锁之前崩溃或者网络不可达。
- 容错性:只要锁服务集群中的大部分节点存活,客户端就可以进行加锁解锁操作。
基于单实例的Redis分布式锁
这个是最常见的, 也是最容易实现的,其中获取锁用redis的SETNX命令:
代码语言:javascript复制SET {key} {random_value} NX PX {expire_time_ms}
- NX:仅在不存在 key 的时候才能被执行成功;
- PX:失效时间,多少(expire_time_ms)ms 后自动释放锁;
- random_value:就是随机值,可以是线程号之类的。主要是为了更安全的释放锁,避免一个客户端释放其他客户端的锁。
释放锁需要通过lua脚本的方式来操作:
代码语言:javascript复制if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
之所设置随机值,是为了在释放锁的时候,在删除key之前检查随机值,如果和之前设置的随机值不一样,则认为是不同客户端的删除锁操作,就不会做del操作。
如果锁被其他客户端释放了,新的获取锁请求就被允许了,超过1个客户端同时获取到锁,这是不允许的。
基于单实例的Redis分布式锁存在的问题
单实例的redis分布式锁,存在一个很大的问题,就是可用性问题,如果单个redis实例挂了,分布式锁服务就不可用了,而且存的锁数据都不存在了。
为了提高可用性,可以增加副本,但是副本也不能解决可能有不同客户端获取到锁的问题。
- 客户端A做加锁操作,获取了锁,master接收setnx请求
- setnx请求没有同步到副本之前,master挂掉了
- 副本被选举为新的master
- 客户端B请求锁,获取到了锁
- 这时候客户端A和客户端B都获取了锁
集群方案也是不可行的,因为集群方案本质上key-value也是到单个实例上。
RedLock方案
RedLock提出用多个master节点共同决策获取锁操作,具体步骤如下:
- 获取当前时间ms单位
- 顺序地从2N 1个实例上申请锁,这里一个 client 需要合理设置与 master 节点沟通的 timeout.
- 当 client 在大于等于 N 1 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
- 如果获取锁成功,锁的持续时间是过期时间减获取锁需要的时间。
- 如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态。
具体参考: Distributed locks with Redis