分别介绍了 Redis, ZooKeeper 和数据库实现分布式锁的相关内容。
Redis
Redission 实现加锁,并增加看门狗进行续期。
- 执行 lock.lock() 进行加锁
- 如果设置的有过期时间,就按照设置的过期时间执行执行 Lua 脚本进行加锁,如果没有设置过期时间,默认过期时间为 internalLockLeaseTime (30s)
- 加锁成功
- 如果没有设置过期时间,机遇 Netty 的时间轮启动一个后台任务,每隔 internalLockLeaseTime / 3 (10s) 检查当前任务是否完成,如果没有完成,就将过期时间重新设置为 30s
- 加锁失败
- 订阅这个锁的 channel
- while 循环尝试获取锁直到成功
看门狗什么时候进行锁续期,什么时候停止续期
续期:
- 加锁时,没有指定过期时间,则默认过期时间为 30s 且每隔 10s 进行锁续期操作
停止续期:
- 锁被释放
- 续期时发生异常
- 执行锁续期 Lua 脚本失败
- 应用宕机、下线或重启后,续期任务将结束(Redission 的续期时 Netty 时间轮)
lock() 和 tryLock() 有什么区别
- lock 的原理是以阻塞的方式获取锁,如果获取失败则一直等待,直到获取成功
- tryLock 是尝试获取锁,如果能获取直接返回 true
- 如果没有指定超时时间,则直接返回 false
- 如果指定了超时时间,在超时时间内,还会尝试获取锁,如果超过了超市时间还没有获取到,则也返回 false
如何保证主从、哨兵下的多节点问题
使用 RedLock , Redission 中有相关实现。
大致原理是获取节点数一半以上的节点的认可,才算加锁成功。
具体过程如下:
- 获取当前时间(毫秒)
- 依次从 n 个节点,使用**相同的 key 和随机值(例如 UUID)**获取锁
- 当向 Redis 请求获取锁时,客户端应该设置一个超时时间,这个时间要远小于锁失效的时间 (例如,如果自动释放时间为 10 秒,超时时间可能在 5-50 毫秒范围内)。这可以防止客户端在尝试与宕机的 Redis 节点通信时被长时间阻塞:如果一个实例不可用,客户端应该尽快尝试与下一个实例通信
- 客户端计算获取锁所用的时间减去步骤 1 的时间,就获得了获取锁消耗的时间。当前仅当大多数(N/2+1)的 Redis 节点都获取到锁,并且获取锁使用的时间小于锁失效的时间,锁才算获取成功
- 成功获取锁后,key 的真正有效时间=TTL-锁的获取时间-时钟漂移
- 如果客户端由于某种原因未能获取锁(无法锁定 N/2+1 个实例或有效时间为负),它将尝试解锁所有实例(甚至是它认为自己无法锁定的实例)
存在的问题:
- 使用成本较高(性能问题:setnx 和 Redission 实现的分布式锁只需要在一个节点写成功就行了,而 RedLock 需要写多个节点才算加锁成功)
- 并不能完全解决分布式锁的问题(严重依赖系统时间、 无法应对无持久化的节点重启、脑裂(网络分区))
Zookeeper
实现方案
- 创建一个锁目录 /locks,该节点为持久节点
- 想要获取锁的线程都在锁目录下创建一个临时顺序节点
- 获取锁目录下所有子节点,对子节点按自增序号从小到大排序
- 判断本节点是不是第一个子节点(序号最小),如果是,则获取锁成功,反之,则监听自己的上一个节点的删除事件
- 持有锁的线程只需要删除当前节点,就可释放锁
- 当自己监听的节点被删除时,监听事件触发,则回到第 3 步重新进行判断,直到获取锁
优点:
- ZK 保证数据的强一致性
问题:
- 性能问题:ZK 在性能方面不如 Redis 高,因为每次创建和释放锁都要创建、销毁节点,并且只能由 Leader 执行之后同步给所有的 Follower
- 并发问题:网络波动下,客户端与 ZK 断连,ZK 会删除临时节点,这时候其他客户端可以获取到分布式锁
MySql
使用唯一索引,使用数据插入尝试作为加锁,如果可以插入成功,则获取锁,执行业务,否则则是已经被抢占。
可使用情况不多,不进行过多的赘述。