一、复习
场景模拟
初始化
在获取锁的时候,会初始化一些参数,例如commandExecutor
leaseTime
uuid
- commandExecutor 命令执行器
- leaseTime 持有锁的最大时间,租期
- uuid 客户端的唯一标识
第一次加锁
- 设置获取到锁的主体,使用线程id作为标识
- 当未设置leaseTime的时候,也就是代表的锁可以无限持有时间
- 判断redis中是否已经存在了这个锁
- 第一次加锁肯定不存在,递增一个lockName为key的hash数据结构里面的一个key为uuid:threadId的元素
{
"lockName": {
"uuid:threadId": 1
}
}
复制代码
- 给设置这个redis key设置一个过期时间 leaseTime,返回一个null
- 此时,就表明了客户端已经加锁成功了
- 收到监听加锁结果后,判断条件是
ttl == null
就表示这个客户端加锁成功 - 如果锁是有最大持有时长的,就会直接结束整个流程
- 如果是没有最大持有时长,会启动一个
leaseTime/3
后执行的任务,任务会将锁的过期时间重新给设置为leaseTime,这个机制也被称之为watchdog
- 同时,本地还会维护一个watchdog需要监听的map,也就是每一个加锁成功且需要watchdog去设置过期时间的锁都会被记录在map中
{
"uuid:lockName": {
"timeout": timeout object,
"threadIds": {
"threadId": 1,
"counter": number
}
}
}
复制代码
第二个其他线程来加锁: 互斥锁,锁竞争
- 获取当前线程的标识 threadId
- 第二个线程来加锁的时候,会发现一些不同的地方
- 判断redis中是否已经存在了这个锁
- 判断redis中这个key是否存在一个key为
uuid:threadId
的元素
- 因为是其他线程,所以也不存在
- 会获取这个key的有效时间再返回出去
- 此时i,内部会判断ttl不为空,就会走进一个死循环
- 在循环内部会去持续的去获取锁
- 为了提高分布式锁的效率,就是在这个循环的内部是引入了信号量的数据结构的
- 这个时候,线程就阻塞住了
第三个线程来加锁:可重入锁
- 获取当前线程的标识threadId
- 判断redis中是否已经存在了这个锁
- 判断redis中这个key是否存在一个key为
uuid:threadId
的元素 - 发现第二个条件是满足的
- 此时,会递增一下redis key的里面
uuid:threadId
的元素 - 重置过期时间为
leaseTime
- 返回null,表示加锁成功
- 维护本地的map的时候,发现已经存在了当前key的元素,就会将里面的conter递增
释放锁
- 释放锁,其实和加锁的核心流程是类似的
- 先获取当前线程的threadId
- 去本地维护的map里,找到这个uuid:locakName删除这个threadId对应的map,如果key中元素threadIds已经是空了,就删除key
- 判断redis中lockName的key中是否存在一个key为uuid:threadId的元素
- 正常释放锁的情况下,会将redis中key对应的hash中的元素的值-1
- 判断递减之后的这个值是否是大于0的
- 如果大于零,也就是存在重入锁,就会做一个重置过期时间
- 否则就会直接删除redis key,还发了一个广播消息,不知道干啥的
- 返回1
- 本地处理只要返回的不是null就相当于释放锁成功,否则释放锁失败
获取锁后客户端宕机
- 当获取锁以后,客户端宕机了
- 此时,在客户端辛勤劳作的watchdog就会死掉
- 然后,等待redis中key的超时,其他客户端就会获取到锁
二、互斥、可重入、释放锁、锁超时和自动释放
互斥
通过在redis中维护一个hash结构,控制加锁的线程和机器不会重复
- redis性能足够高,能支撑高并发
- hash结构足够灵活
阻塞
如果一个锁获取失败的话,会在本地无限循环来获取锁,锁竞争都处于一个循环执行的状态
- 同时引入了信号量避免并发过高的问题
可重入
可重入锁是为了避免出现死锁的情况,需要的一个特点
通过在redis和本地都维护了一个当前机器threadId的加锁次数
释放锁
本质上,释放锁其实就是加锁的反操作,会递减加锁次数,去删除redis中的key,本地map中的key
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
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
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
复制代码
锁超时
就是加锁超时:waitTime
- 获取当前时刻current
- 尝试加锁后,如果加锁成功则直接返回,如果加锁失败则会waitTime减掉这个加锁过程的时间
- 判断是否已经超时了,超时则失败
- 后续会进入一个循环中去持续加锁
- 只要尝试加一次锁,就要去计算一下这个时间
超时锁自动释放
就设置了leaseTime参数
- 不会再去触发watchdog,等待redis中的锁超时后就会自动释放
总结
大图
图比较乱奥,但是应该能大概看得明白,比较核心的组件都画出来了
隐患
本质还是在集群中挑选一个master来加锁,实现了高可用机制,如果master宕机,slave会自动切换为master
如果master刚被写入一个锁,然后就宕机了,还没有被异步写入到slave中,slave就切换成了新的master,这样的话,其他线程就也会就获取锁成功