Redis分布式——分布式锁

什么是分布式锁?

在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况.此时就需要通过锁来做互斥控制,避免出现类似于"线程安全"的问题.

而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 步:

  1. 获取当前时间戳:客户端记录发起锁请求的起始时间(用于计算总耗时)。
  2. 向所有 Redis 节点发起锁请求 :对每个节点,使用 SET key value NX EX ttl 命令(NX= 仅当 key 不存在时才设置,EX= 设置过期时间 ttl)尝试获取锁,其中:
    • key:全局唯一的锁标识(如 "lock:order:123",对应 "订单 123" 的锁);
    • value:客户端唯一标识(如 UUID + 线程 ID,确保锁只能由持有者释放,避免 "误释放他人锁");
    • ttl:锁的过期时间(需合理设置,既要大于业务执行时间,又要避免锁长期占用)。
    • 注意:若某个节点请求超时(如网络故障),直接跳过该节点,不等待。
  3. 计算锁获取总耗时:客户端记录所有节点请求完成后的时间戳,计算总耗时(当前时间 - 起始时间)。
  4. 判断锁是否获取成功 :需同时满足两个条件:
    • 成功获取锁的节点数量 ≥(总节点数 / 2 + 1)(即 "过半原则",如 5 个节点需 ≥3 个成功);
    • 总耗时 ≤ 锁的过期时间 ttl(避免因耗时过长,导致部分已获取的锁提前过期)。
  5. 锁释放 / 重试逻辑
    • 若锁获取成功:锁的实际有效时间 = ttl - 总耗时(需在剩余时间内完成业务逻辑);
    • 若锁获取失败:立即向所有节点发起锁释放请求(无论该节点是否成功授予锁,避免 "僵尸锁"),之后可重试(建议设置重试间隔,避免频繁请求);
    • 业务完成后:客户端向所有节点发送 DEL key 命令释放锁(若锁已过期,DEL 操作不影响)。
相关推荐
月空MoonSky2 小时前
GaussDB与Oracle数据库的比较
数据库·oracle·gaussdb
少云清2 小时前
【性能测试】13_JMeter _JMeter分布式
分布式·jmeter·性能测试
程序员敲代码吗2 小时前
持续集成/持续部署(CI/CD) for Python
jvm·数据库·python
倔强的石头1062 小时前
让时序开发更可控:金仓时序 DB 的易用性实践与平台化路径
数据库·kingbase
霸道流氓气质2 小时前
SpringBoot+modbus4j实现ModebusTCP通讯定时读取多个plc设备数并存储进redis中
java·spring boot·redis·modbustcp·plc
数据知道2 小时前
PostgreSQL实战:如何用 CTE(公用表表达式)解决复杂的查询逻辑
数据库·postgresql
1.14(java)2 小时前
MySQL索引原理与B+树应用详解
数据库·b树·mysql
java干货2 小时前
用 MySQL SELECT SLEEP() 优雅模拟网络超时与并发死锁
网络·数据库·mysql
哈哈不让取名字2 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python