什么是分布式锁?
在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况.此时就需要通过锁来做互斥控制,避免出现类似于"线程安全"的问题.
而java的synchronized或者C++的std::mutex,这样的锁都是只能在当前进程中⽣效,在分布式的这 种多个进程多个主机的场景下就无能为力了. 此时就需要使用到分布式锁.
本质上就是使⽤⼀个公共的服务器,来记录加锁状态. 这个公共的服务器可以是Redis,也可以是其他组件(⽐如MySQL或者ZooKeeper等),还可以 是我们⾃⼰写的⼀个服务.
以买票为例,假设有多个买票服务器连接票数量的数据库,而客户端要买票就需要连接多个服务器之一,进而查询数据库中票数量,如果>=1就进行更改(买票成功),这样肯定会产生线程安全问题,而我们此时引入了分布式锁,也就是将所有的买票服务器再引入redis,所有的服务器要想买票(访问/修改数据库)前必须先访问redis,如果某个服务器访问redis时内部并没有设置相关的key-value,代表此时数据库对应的这种票是无人竞争的,此时这个服务器就可以大胆访问并查看是否可以买票了,在访问期间,会设置这种票的key-value(任意值都行,代表了有服务器正在买这种票)当买票操作执行完毕,就会删除对应的key-value(解锁)让其他服务器有权"加锁"并买票。当加锁期间,如果其他服务器想买票,就会尝试设置key-value,肯定是设置失败的,至于是直接返回还是阻塞等待要根据实际设定。
上述的加锁解锁过程在redis中也有对应的命令------setnx、del
极端情况1,某个服务器加锁后突然崩了,导致没有解锁------解决方案,给锁设置过期时间set ex nx
这样即使拿着锁的服务器挂了,过了过期时间也会自动释放。(使用setnx+expire是不行的!redis的多条命令间无法保证原子性!)
极端情况2:服务器1加锁,服务器2给解锁了?!!(有可能)
解决方案:引入校验机制,给服务器编号,每个服务器有自己的身份标识,在加锁时,key-value中的key代表根据哪个资源加锁(比如某一车次),value标识锁是哪个服务器加的(服务器编号)
当要解锁时,可以进行校验,先查询锁对应的valueid,看看是否对应此服务器编号,如果一致时才能解锁(del)
极端情况3:某个服务器加锁了,当要解锁时,可能由于内部多线程导致多次解锁?(因为操作非原子,在多个del操作间,如果其他服务器要加锁了,就会被这个操作导致没加上)
解决方法:可以使用redis事务/引入lua脚本(更好),用lua写执行逻辑并运行脚本,通过客户端控制redis脚本,实现上述操作原子化。
极端情况4:如果设置key的过期时间过短,导致业务逻辑还没处理完锁就释放了,如果设置时间过长,而会导致释放不及时影响效率问题。
解决方案:动态续约,先设置一个过期时间,当还剩一定时间时,检查一下业务是否执行完毕,如果没执行完就自动续约一定时间,直到发现执行完毕,不续约等待时间到自动释放。避免上述长和短问题。(勤拿少取),这种动态续约的操作需要一个专门的线程负责,称为看门狗~~
极端情况5:redis分布式锁本身挂了
解决方案:使用redis哨兵结构,发现某个redis主节点挂了,哨兵选一个从升级为主重新加锁使用。但是,主从数据同步是存在延时的,如果主收到了set请求,还没同步到从就挂了,即使从升级为主,因数据不同步导致新主不知道有加锁的操作。
解决方案:redlock算法
引入多个redis主节点,加锁时,按一定顺序把每一个节点都尝试加锁,当加锁成功数量超过总节点数一半视为加锁成功,可以有效解决一台redis节点挂了另一个衔接不上锁的问题,解锁同理,按顺序解锁。
以下是redlock的原理简述
Redlock 的核心思想是:让客户端向多个独立的 Redis 节点(通常建议 5 个节点,且节点间无主从关系,均为 "主节点" 角色)发起锁请求,只有当客户端成功获取超过半数(≥3 个)节点的锁,且总耗时不超过锁的有效时间时,才认为 "锁获取成功"。
具体流程可拆解为 5 步:
- 获取当前时间戳:客户端记录发起锁请求的起始时间(用于计算总耗时)。
- 向所有 Redis 节点发起锁请求 :对每个节点,使用
SET key value NX EX ttl命令(NX= 仅当 key 不存在时才设置,EX= 设置过期时间 ttl)尝试获取锁,其中:
key:全局唯一的锁标识(如 "lock:order:123",对应 "订单 123" 的锁);value:客户端唯一标识(如 UUID + 线程 ID,确保锁只能由持有者释放,避免 "误释放他人锁");ttl:锁的过期时间(需合理设置,既要大于业务执行时间,又要避免锁长期占用)。- 注意:若某个节点请求超时(如网络故障),直接跳过该节点,不等待。
- 计算锁获取总耗时:客户端记录所有节点请求完成后的时间戳,计算总耗时(当前时间 - 起始时间)。
- 判断锁是否获取成功 :需同时满足两个条件:
- 成功获取锁的节点数量 ≥(总节点数 / 2 + 1)(即 "过半原则",如 5 个节点需 ≥3 个成功);
- 总耗时 ≤ 锁的过期时间 ttl(避免因耗时过长,导致部分已获取的锁提前过期)。
- 锁释放 / 重试逻辑 :
- 若锁获取成功:锁的实际有效时间 = ttl - 总耗时(需在剩余时间内完成业务逻辑);
- 若锁获取失败:立即向所有节点发起锁释放请求(无论该节点是否成功授予锁,避免 "僵尸锁"),之后可重试(建议设置重试间隔,避免频繁请求);
- 业务完成后:客户端向所有节点发送
DEL key命令释放锁(若锁已过期,DEL操作不影响)。