一.分布式锁介绍
1.简介
网址:《分布式锁介绍》
分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
2.分布式锁的特征
「互斥性」: 任意时刻,只有一个客户端能持有锁。
「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
「可重入性」:一个线程如果获取了锁之后,可以再次对其请求加锁。
「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除
二.实现分布式锁的方案
1.Redis实现分布式锁的7种方案
网址:《Redis实现分布式锁的7种方案》
方案一:SETNX + EXPIRE 【问题:SETNX和EXPIRE不是原子操作】
方案二:SETNX + value值是(系统时间+过期时间)【问题:分布式环境,客户端时间不同步】
方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)
方案四:SET的扩展命令(SET EX PX NX)
方案五:SET EX PX NX + 校验唯一随机值,再释放锁
方案六: 开源框架~Redisson
方案七:多机实现的分布式锁Redlock
三.Redis锁可能遇到的问题
1.SETNX + EXPIRE两条指令,保证原子性?
(1)SETNX + value值是(系统时间+过期时间)
(2)使用Lua脚本(包含SETNX + EXPIRE两条指令)
(3)SET的扩展命令(SET EX PX NX)
在 Redisson 中,设置分布式锁的 SETNX + EXPIRE 是 原子性 的,因为 Redisson 通过原子性命令 SET 来完成的。
【命令】SET key value [EX seconds | PX milliseconds] [NX | XX]
-- NX:只在键不存在时设置键。
-- EX / PX:设置键的过期时间,单位分别是秒和毫秒。
【案例】SET lockKey lockValue NX PX 10000
-- 如果 lockKey 不存在,则创建该键并设置其值为 lockValue。
-- 同时设置过期时间为 10 秒(即 10000 毫秒)。
2.锁过期释放了,业务还没执行完?
假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
总结:
(1)如果使用 Redis 原生API:建议采用:自动续期机制 + 唯一标识验证
(2)如果使用 Redis 客户端:直接使用 Redisson 提供的分布式锁(看门狗机制)是最佳选择。
(3)如果涉及到复杂业务流程或超长任务,还可以结合 方案5 进行补偿与监控。
(1)设置超期时间
在设置锁的过期时间时,尽量保证其大于业务的最坏执行时间。一般为正常执行的2-3倍时间。TTL应该足够长,以确保大多数情况下任务能够在TTL内完成,但也不能过长,以免长时间占用锁导致其他客户端无法获取锁。
(2)分布式锁的ID验证机制(唯一标识)
为每次加锁生成一个唯一标识(如UUID),释放锁时检查该标识是否匹配。保证只有锁的拥有者才能释放锁,避免误删。执行过程是原子的,不会被中断。如果业务超时,没有自动续期机制会导致锁失效。
在 Redisson 中,unlock() 方法使用了 UUID + Lua 脚本机制 来防止误删锁。这个机制确保了 只有持有锁的客户端才能成功释放锁。
每个客户端(即每个 RedissonClient 实例)在创建时都会生成一个唯一的 UUID。当调用 lock() 方法时,Redisson 会将这个 UUID 作为锁的 value 写入 Redis,并且附加上当前线程的 threadId(用于区分同一个客户端的不同线程)。
(3)Redisson看看门狗机制(可重入锁与续期机制)
Redisson 的**看门狗机制(Watchdog)**是用于处理分布式锁自动续期的一种机制,确保锁不会在任务未完成时过期。
开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。
Redisson 看门狗机制原理:
1.默认锁超时时间: 当 Redisson 客户端成功获取锁时,如果用户没有自定义指定锁的有效期,Redisson会为该锁分配一个默认的超时时间,通常是 30 秒(lockWatchdogTimeout 配置项可修改)。
2.看门狗线程: Redisson 客户端内部启动一个定时任务线程(看门狗),每隔一段时间(通常是 10 秒),会检查所有当前持有的锁。
3.自动续期: 如果客户端还持有某个锁且未释放,看门狗会自动延长该锁的有效期,将其重置为 30 秒。这个过程会一直持续,直到:
①客户端主动释放锁,
②或 客户端崩溃或因网络原因与 Redis 服务器断开连接。
4.安全性保证:
①如果客户端出现故障(例如宕机或长时间网络中断),看门狗无法继续续期,锁会在 30 秒 后自动失效,从而避免死锁问题。
②如果客户端业务操作较长,只要连接正常,看门狗会一直自动续期,不会释放锁。
(4)定时补偿机制(异步检测与纠正)
使用一个独立的服务或定时任务,定期检测被占用超过预期时间的锁并进行纠正或重试。定期扫描 Redis 中的锁数据。判断锁是否已经超过合理时间,如果超过则进行补偿处理(比如重试或警告)。可通过消息队列或补偿机制触发业务重试。
适用于长时间的业务处理。可以结合监控与报警系统,主动检测问题。实现复杂,尤其是补偿策略的制定与执行。补偿策略可能导致数据不一致问题。
3.锁被别的线程误删?
假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。
(1)SET EX PX NX + 校验唯一随机值
既然锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,不就OK了嘛。「判断是不是当前线程加的锁」和「释放锁」不是一个原子操作。为了更严谨,一般也是用LUA脚本代替。
4.集群部署如何实现分布式锁?
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。为了解决这个问题,Redis作者 antirez提出一种高级的分布式锁算法:Redlock。Redlock核心思想是这样的:
1.获取当前时间,以毫秒为单位。
2.按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
3.客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
4.如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
5.如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。
简化下步骤就是:
(1)客户端 获取当前毫秒级时间戳,并设置超时时间 TTL
(2)依次向 N 个 Redis 服务发出请求,用能够保证全局唯一的 value 申请锁 key
(3)如果从 N/2+1 个 redis 服务中都获取锁成功,那本次分布式锁的获取被视为成功,否则获取锁失败
(4)如果获取锁失败,或执行达到 TTL,则向所有 Redis 服务都发出解锁请求
注:redisson中的getLock()没有使用RedLock 算法。Redisson 的 getLock() 方法返回的是一个 RLock 对象,而不是 RedissonRedLock 对象。
5.自动续期机制中,客户端宕机没法释放锁怎么办?
因为分布式锁的续期是在客户端执行的,所以如果客户端宕机了,续期线程就不能工作了,也就不能续期了。这时应该把分布式锁删除,让其他客户端来获取。因为client1宕机了,只能等到超时时间后锁被自动删除。如果要立刻删除,需要增加额外的工作,比如增加哨兵机制,让哨兵来维护所有redis客户端的列表。哨兵定时监控客户端是否宕机,如果检测到宕机,立刻删除这个客户端的锁。
注: 这里的哨兵并不是redis的哨兵,而且为了检测客户端故障业务系统自己做的哨兵。
四.Redisson实现分布式锁方案
1.基于Redission实现分布式锁
2.Redisson实现了redLock版本的锁
SpringBoot整合Redisson实现RedLock分布式锁同步