一、数据库实现分布式锁
1.1、基于唯一索引
1.1.1、实现逻辑
- 获取锁时在数据库中insert一条数据,包括id、方法名(唯一索引)、线程名(用于重入)、重入计数
- 获取锁如果成功则返回true
- 获取锁的动作放在while循环中,周期性尝试获取锁直到结束或者可以定义方法来限定时间内获取锁
- 释放锁的时候,delete对应的数据
1.1.2、问题
- 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
- 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
- 非阻塞的?搞一个while循环,直到insert成功再返回成功。
1.2、基于悲观锁 for update
1.2.1、实现逻辑
关闭自动提交
set autocommit=0;
添加排他锁
select * from methodLock where method_name = xxx for update;
解锁,提交事务
// 提交事务,解锁
commit;
1.2.2、问题
行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住
-
1.3、基于乐观锁
乐观并发控制(又名“乐观锁”,Optimistic Concurrency Control,缩写“OCC”)是一种并发控制的方法。
在表中添加版本号字段 version
- 在更新是更新版本号
update t_goods
set status=2,version=version+1
where id=#{id} and version=#{version};
1.4、注意
- 使用mysql分布式锁,必须保证多个服务节点使用的是同一个mysql库。
二、Redis 实现分布式锁
2.1、使用 set 命令
2.1.1、加锁设置
SET resource_name my_random_value PX 30000 NX
EX seconds -- 设置过期时间
PX milliseconds -- 设置过期时间,毫秒
EXAT timestamp-seconds -- Set the specified Unix time at which the key will expire, in seconds.
PXAT timestamp-milliseconds -- Set the specified Unix time at which the key will expire, in milliseconds.
NX -- key 不存在时,设置
XX -- key 存在时,设置
KEEPTTL -- Retain the time to live associated with the key.
GET -- 返回旧的值,设置新的值替换
2.1.2、解锁设置
del
2.1.3、原子性问题
- 使用 lua 脚本保证原子性操作
2.2、使用 redisson 实现
2.2.1、redisson 加锁过程
# 判断key 是否存在
if (redis.call('exists', KEYS[1]) == 0) then
# 不存在,设置 hash key 值为 1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
# 设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
# 存在,自增1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
# 设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
2.2.2、redisson 解锁过程
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
# key 不存在,返回空
return nil;
end;
# 减一
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
# 更新过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
# counter <= 0 ,释放锁
redis.call('del', KEYS[1]);
# 推送key 过期事件
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
2.2.3、自动续期问题
- 实现逻辑
客户端A加锁的锁key默认生存时间只有30秒,如果超过了30秒,客户端A还想一直持有这把锁,怎么办?其实只要客户端A一旦加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果客户端A还持有锁key,那么就会不断的延长锁key的生存时间。
- 底层任务存储数据结构
- netty 实现的时间轮
2.3 、了解 Redlock 锁?
这个场景是假设有一个 Redis cluster,有 5 个 Redis master 实例。然后执行如下步骤获取一把锁:
- 获取当前时间戳,单位是毫秒;
- 跟上面类似,轮流尝试在每个 master 节点上创建锁,超时时间较短,一般就几十毫秒(客户端为了获取锁而使用的超时时间比自动释放锁的总时间要小。例如,如果自动释放时间是 10 秒,那么超时时间可能在 5~50 毫秒范围内);
- 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1 ;
- 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
- 要是锁建立失败了,那么就依次之前建立过的锁删除;
- 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
2.4、Redis 相关问题
- 非原子性操作
- 使用 Lua 脚本保证
- 释放了别人的锁
- 针对线程或服务添加唯一标识
- 获取锁时间大于事务等待时间
- 采用手动提交事务,回滚事务
- 锁超时问题
- 使用看门狗机制(自动续期)
- redis主从复制的坑(暂时无法解决,尽量保证机器稳定)
- A 客户端获取到了锁
- 在主从复制过程中,redis master宕机了,此时主备切换,slave变为了redis master
- B客户端从新的redis master中获取到了锁
- 此时就会导致同一时间内多个客户端对一个分布式锁完成了加锁,导致各种脏数据的产生。
三、Zookeeper 实现分布式锁
3.1、Zookeeper 中的相关概念
3.1.1、底层数据结构
- 分层命名空间
3.1.2、节点类型
- 永久节点:不会因为会话结束或者超时而消失;
- 临时节点:如果会话结束或者超时就会消失;不允许有子节点
- 顺序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,从小到大排序。
- 容器节点( 3.6.0添加):
- 容器 znode 是特殊用途的 znode,可用于leader, lock等
- 当容器znode 的最后一个字节被删除时,它将成为将来某个时候被服务器删除的候选节点。
- TTL 节点(3.6.0):
- 可以选择为 znode 设置一个 TTL(以毫秒为单位)。 如果 znode 在 TTL 内没有被修改并且没有子节点,它将成为将来某个时候被服务器删除的候选节点。
- TTL 节点必须通过系统属性启用,因为默认情况下它们是禁用的
3.1.3、节点监听机制
每个线程抢占锁之前,先抢号创建自己的ZNode。同样,释放锁的时候,就需要删除抢号的Znode。抢号成功后,如果不是排号最小的节点,就处于等待通知的状态。等谁的通知呢?不需要其他人,只需要等前一个Znode 的通知就可以了。当前一个Znode 删除的时候,就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个,击鼓传花似的依次向后。
3.2、实现锁的方式
3.2.1、临时顺序节点实现公平锁
- 创建临时顺序节点
- 获得所有的子节点,并排序
- 判断当前节点是否是最小的,如果是最小的节点,则表明此这个client可以获取锁
- 序号不为最小则监听(watch)前一个顺序号
当前一个顺序号被删除的时候表明锁被释放了,则会通知下一个客户端
3.2.2、同名临时节点实现非公平锁
某个节点尝试创建临时 znode
- 创建成功了就获取了这个锁
- 这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁
-
3.2.3、相关开源项目
apache.curator
3.3、Zookeeper 实现分布锁相关问题
3.3.1、多客户获取锁问题
由于 zk 依靠 session 定期的心跳来维持客户端,如果客户端进入长时间的 GC,可能会导致 zk 认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在 GC 恢复后,会认为自己还持有锁,从而可能出现多个客户端同时获取到锁的情形。
可以通过 JVM 调优,尽量避免长时间 GC 的情况发生
四、Redis 和 Zookeeper 作为分布式锁选型
- ZK 保证一致性 CP
- Redis Cluster 保证可用性 AP
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。