- 分布式锁概念
在分布式系统中,同一时间只允许一个线程/进程对共享资源进行操作。例如:秒杀、积分扣减、抢红包、定时任务执行等等。
- 分布式锁四种雷区
- 死锁:加锁成功后,不知什么原因导致服务器出现宕机,未能成功释放锁,出现死锁。(方案:设置超时时间)
- 锁误删:只有持有当前锁的线程,才能删除锁,即:解铃还须系铃人(方案:唯一id标识当前线程)
- 锁超时并发执行:加锁成功后,由于代码执行非常耗时、下游服务执行慢、调用链太长或者GC(垃圾回收)耗时等原因导致锁超时,其他线程获得锁并发执行
- 集群容错:成功在master解锁,未能及时同步到slave节点,此时出现脑裂存在多个master节点,其他节点也可以加锁成功
- 分布式锁特性
- 互斥性:当一个线程/进程加锁成功后,其他线程/进程无法加锁,具有排他性
- 锁失效机制:加锁成功后,服务器宕机导致锁未能释放,服务恢复后一直获取不到锁。应设置超时时间,防止出现类似死锁情况
- 阻塞锁:当前资源已被加锁,其他线程/进程来加锁是否阻塞等待,还是立即返回
- 可重入性:当前锁的持有者是否能再次进入
- 公平性:加锁顺序和请求加锁顺序是否一致,还是随机抢锁
- 实现原理
- 加锁
- 使用set扩展命令,key:锁标识,value:持有当前锁线程标识,PX:超时时间(毫秒)。
-
# 加锁命令 set key value NX PX milliseconds
-
解锁
-
只有当前锁的持有者才可以执行删除操作,通过lua脚本保证了get和del命令执行的原子性操作。
-
// 通过lua脚本,解决了 解铃还须系铃人 的问题。 // 使用redis+lua脚本(保证原子性,减少网络开销) $script = <<<LUA local key=KEYS[1] local value=ARGV[1] if(redis.call('get', key) == value) then return redis.call('del', key) end LUA; // 执行lua脚本 $this->redis->eval($script, [$this->lockKey, $this->lockValue], 1);
-
- 绪期(进程占用时间太长,避免并发获得锁)
-
$script = <<<LUA if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end LUA; $this->redis->eval($script, [$this->lockKey,$this->lockValue,$this->expireTime], 1)
-
- RedLock算法
-
集群脑裂:集群脑裂指因为网络问题,导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区,因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为 master 节点,此时存在两个不同的 master 节点。Redis Cluster 集群部署方式同理。
-
多节点redis实现的分布式锁算法(RedLock):有效防止单点故障
假设有5个完全独立的redis主服务器
1.获取当前时间戳
2.client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
3.client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功
4.如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
5.如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁
算法示意图如下:
-
- 加锁
转载于:Redlock(redis分布式锁)原理分析_狂奔的蜗牛Evan的博客-CSDN博客_redlock,百度安全验证https://baijiahao.baidu.com/s?id=1706527669583273516&wfr=spider&for=pc