https://juejin.im/post/5cc165816fb9a03202221dd5
分布式锁的特点:
- 互斥性
- 可重入性
- 锁超时
- 高性能和高可用
- 具备阻塞和非阻塞型:能够及时从阻塞状态中被唤醒
setnx:set if not exist,如果不存在,则设置成功
设置过期时间防止死锁:expire
- key 用来标识这个锁是哪个场景的
- value 要用唯一值,用来标识哪个线程或哪个节点持有当前锁
**
2.8版本之前,setnx 和 expire 不能一步完成,不是原子性的,所以需要用 Lua 脚本实现。
2.8 之后就可以了。
# key: notifyLock,value: host1,超时时间 20s
set notifyLock host1 nx ex 20
Lua 脚本也可以实现
代码:
// 获取分布式锁:原子操作,同时使用 NX 和 PX
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
// 释放分布式锁
// 防止 A 业务超时后,B 获得到锁,而 A 又把 B 的锁释放,所以需要判断一下
if (jedis.get(lockKey).equals(requestId)) {
jedis.del(lockKey);
return true;
}
TODO:如何实现可重入的锁?
LUA 脚本,要判断获得当前锁的进程标识,以及要保存重入次数
Redisson:
Redisson 内部实现了分布式锁,也包含多种类型的锁
Watch dog:
如果过期时间到了,但是业务没有执行完成?
获取到锁的客户端,启动一个 watch dog,检测到如果当前机器持有锁的话,就更新锁的过期时间。
比如锁过期 30s,则每过 10s,watch dog 就将锁续期到 30s。
如果业务宕机了,则 watch dog 也会跟着一起挂掉,也会自动过期释放锁。
分布式锁的四个条件:容错、复制、不能解锁别人的、不能死锁
Redis 集群场景下的问题:主从切换后,如果锁没有复制过去,就会有两个线程持有锁
RedLock 算法:
布式环境中,我们假设有N个完全互相独立的 Redis 节点,在 N 个 Redis 实例上使用与在 Redis 单实例下相同方法获取锁和释放锁。
现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:
- 获取当前Unix时间,以毫秒为单位
- 依次尝试从5个实例,使用相同的 key 和具有唯一性的 value 获取锁当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端死等
- 客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的 Redis 节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功
- 如果取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要
如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题
记下开始时间
- 并发向 5 个节点获取锁,超过半数且超时时间小于锁的失效时间,即成功。
- 成功后,key 的有效时间 = 有效时间 - 获取锁使用的时间
- 因为每个节点 setnx 的时间是不一样的,所以按照最早成功的节点来说,它还等待了其他节点获取锁的时间
- 如果失败了,即没有半数以上成功或者超时了,则应该对全部节点进行解锁
同时获取锁,线程1从左边开始,一个一个节点获取锁,线程2从右边开始获取锁。
或者线程1获取到某个节点的锁,突然间某个节点down了,然后又重启了,此时线程2获取锁
分布式锁:base、etcd、mysql、redis、zk