尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Redis分布式锁高并发实战:锁过期、误删与看门狗
1. 为什么能用的分布式锁一到高并发就出事先说个我在实际项目中反复看到的场景系统刚上线时并发量不大用 Redis 做锁大家写setnx加锁、del解锁跑得挺顺。等某个大促或活动流量一上来突然出现超卖、重复执行、锁不住的情况。这个时候再去翻日志你会发现加锁和解锁的代码读起来完全没问题逻辑链路也讲得通但就是出问题了。这就是 Redis 分布式锁最迷惑人的地方——它看起来太简单了。一个setnx、一个del两行命令搞定新人也能上手。可分布式锁的核心从来不是能执行而是异常情况下依然安全进程崩了怎么办加解锁之间网络超时怎么办业务代码执行时间超过了锁的过期时间怎么办节点切换怎么办这些问题只要有一个没处理锁就形同虚设。我习惯把分布式锁理解成一个临界区管理人。多个进程同时要操作同一份资源比如扣减库存、发放优惠券、执行定时任务锁保证同一时刻只有一个人进去。但这个管理人自己必须足够健壮既不能把钥匙弄丢也不能让别人进去之后误以为门还锁着。这篇文章不打算讲太泛的理论而是把我在实践中反复踩过的坑梳理成几条明确的问题清单每条都附上解决方案和思考过程。适合正在维护有状态服务的同学也适合准备把 Redis 锁用在生产环境、想提前避坑的团队。1.1 锁本质上是在做资源的串行化很多人一听到分布式锁就想到Redis、ZooKeeper这些中间件容易忽略一个前提锁是用来做串行化的。本来业务代码是并行的多个请求同时到达现在通过锁把它们强制变成一个个排队执行。所以评估一个锁方案好坏核心指标只有一个它能不能在任何异常情况下都保持只有一个执行者的约束。注意是任何异常情况不是正常运行情况。正常运行谁都能写对异常情况才是锁的价值所在。常见的异常包括加锁后进程突然退出、Redis 节点发生主从切换、业务线程发生长 GC 暂停、微服务调用方超时重试。这些都是分布式环境里一定会出现的情况只是概率问题。如果你的锁逻辑只在正常流程里测试过那基本等于没有锁。1.2 一个看似简单却埋雷的入门写法先看一个最典型的错误示范我见过不少项目真就这么写SETNX lock_key 1 EXPIRE lock_key 30 -- 执行业务代码 DEL lock_key这段代码的逻辑很直白先尝试设置锁成功就设置过期时间再执行任务最后删锁。第一眼看上去没问题但拆开看问题很大。SETNX和EXPIRE是两条独立命令。第一条执行成功第二条还没来得及执行进程突然崩了或者 Redis 连接超时锁就变成了一个没有过期时间的键永远占在那里。后续所有请求都会加锁失败系统直接进入假死状态。这种情况在线上事故里非常常见而且越着急越难排查因为你看到 Redis 里明明有个键但不知道是谁设的也不知道它为什么没有过期时间。1.3 这里最容易踩的第一层坑非原子操作上面这个问题的本质是多条命令之间缺少原子性。一个完整的加锁动作必须在一个原子操作里完成。要么设置键并带上过期时间一步到位要么子啊出问题时能够感知到并回滚。正确写法是把设置和过期时间合成一条命令SET lock_key unique_value NX PX 30000这条命令的含义是仅当lock_key不存在时设置值并且同时指定 30 秒过期。返回OK表示加锁成功返回nil表示锁已被占用。从这里开始分布式锁的设计才算真正走上正路。但跟着问题就来了只解决加锁的原子性后面还有更多坑等着你。2. 锁过期与误删最典型的两个必避问题2.1 过期时间设短了任务没跑完锁就没了很多人设置锁的时候习惯把过期时间当作一个保险丝来设比如 10 秒。但这个值根本没法拍脑袋定。假设你设置了 10 秒过期任务实际执行了 15 秒。第 10 秒时锁自动消失第 11 秒时另一个请求成功加锁开始执行同样的任务。此时系统里出现了两个并发执行者分布式锁形同虚设。更麻烦的是这种情况很难通过日志直接看出来。第一个线程的第 11 秒到第 15 秒还在继续运行它并不知道锁已经易主。两个线程同时修改数据最终结果就看谁的写入晚数据可能被覆盖而且错误的数据会在后续流程中被一步一步放大。解决思路有两类一是把过期时间设置得足够长长到几乎不可能超过业务执行时间二是不要让过期时间固定而是让锁的存活时间与任务生命周期挂钩任务没结束就自动续期。第一类思路不推荐原因很简单你永远无法预估业务的最长执行时间。网络抖动、下游服务变慢、GC 停顿都可能让一个平时几十毫秒的任务变成几分钟。把过期时间调到很大反而失去了兜底意义。第二类思路就是大家常说的看门狗机制后面单独展开讲。2.2 不看 value 直接 DEL把别人的锁删了这是另一个高频事故解锁时不校验持有者身份直接用DEL删键。回到刚才的场景。线程 A 加锁成功但锁在到期前被另一个线程 B 抢占了。线程 A 业务跑完后执行DEL lock_key这时它删除的是线程 B 的锁。线程 B 的临界区瞬间被打开线程 C 又进来了。三个线程乱成一团结果完全不可控。我见过线上真实情况是A 线程持锁执行耗时任务B 线程等待获取锁A 超时释放B 拿到锁开始执行。这时 A 突然恢复正常执行完去DEL把 B 的锁删了。B 的任务还没跑完锁已经没了C 线程也进来了。一个简单的重复执行问题被放大成数据错乱事故。2.3 正确删除用 Lua 脚本保证检查删除的原子性正确的解锁动作应该是先确认锁的持有者是自己再删除锁。但检查和删除是两个动作如果分开执行中间依然有被其他线程抢占的时间窗。解决方案是把两个动作放进一个 Lua 脚本由 Redis 保证脚本执行的原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end先get键的值与当前线程的唯一标识比较。如果相等说明锁还是自己的执行del如果不相等说明锁已经被别人持有放弃删除返回 0。这样操作加锁时的唯一值通常用 UUID 或线程 ID随机数就成了持锁者身份凭证。每个线程只删除自己名下的锁从根源上杜绝误删。这里有一个特别容易忽略的细节很多人以为加了唯一值就万事大吉但唯一值本身不能太简单。比如只用一个数字多个服务实例的随机数可能冲突。建议用UUID或者IP线程ID随机数拼接保证全局唯一性。我习惯生成一个类似instanceId-threadId-原子递增计数的字符串调试的时候还能定位到具体是哪个实例、哪个线程加的锁。3. 续期与看门狗让锁的存亡跟随任务的生命周期3.1 定长过期时间为什么本质上不合理每次谈到锁过期时间我都会强调一个观点固定过期时间本身就是一种妥协。分布式锁要保护的业务任务执行时长是动态的。有的任务快几十毫秒就完有的任务慢遇到下游超时要重试可能跑几分钟。不管把过期时间设置成 10 秒、30 秒还是 60 秒都有可能碰到超长任务。设置短了锁提前消失并发漏洞产生设置长了锁故障时恢复时间变长可用性下降。所以比较靠谱的思路是动态调整锁的存活时间让它跟业务走。3.2 看门狗思路快到期时自动续期看门狗的机制是在锁的过期时间到达之前由一个后台任务或者定时调度持续给锁续期。只要业务线程还在运行锁就不会过期业务结束后显式释放锁后台任务取消。它的本质是把锁的存活判定权从固定时间转移到了任务的真实生命周期上。我用一个非常简单的方式实现过看门狗加锁时设置初始过期时间比如 10 秒。同时注册一个定时任务每隔 3 秒执行一次。每次续期前检查业务线程是否还活着。如果线程还活着就用 Lua 脚本更新锁的过期时间重新设置成 10 秒。如果线程已经结束或者锁不存在了就停止续期。这样锁的过期时间就变成一个滑动的窗口业务跑多久锁就能跟着续多久。3.3 续期方案的具体参数与计算方法看门狗的轮询周期不是随便定的需要结合网络延迟和锁的过期时间来计算。通常遵循一个原则续期频率要显著高于过期时间窗口保证在极端情况下丢一次续期请求锁也不会立刻过期。我实践中的参数设置经验是参数推荐值理由初始过期时间10 秒给业务留够启动余量同时不至于锁失效时恢复过慢看门狗轮询周期3 秒约等于过期时间的三分之一留足网络抖动容忍空间续期脚本Lua 原子更新防止续期时锁已被别人持有更新出错锁锁最大续期次数不设限但设总超时上限防止死循环任务永远持有锁业务侧必须有超时中断这里有个容易被忽略的问题续期脚本里同样要带身份校验。假如线程 A 加的锁已经被线程 B 拿走了A 的看门狗还在傻乎乎地续期那它续的就是 B 的锁等于帮别人延长了占用时间。所以续期 Lua 脚本必须检查当前持有者是不是自己的唯一值是才续期不是就退出。3.4 续期失败时的处理路径看门狗不是每次都成功的。如果 Redis 短暂不可用续期没有成功接着锁在过期时间到达后消失了另一个线程抢占了锁你怎么办这种情况下业务线程自己是感知不到的它还以为锁还在自己手里。所以需要在业务代码里在关键节点做防御性检查在每次写共享数据之前重新get一次锁确认持有者身份还是自己如果不是说明锁已经丢失立即终止任务并向监控系统上报。这个检查不能太频繁否则会影响性能。我一般在循环处理大数据量的批次中每隔几十条记录检查一次既保证安全性又不会拖慢整体速度。4. 主从复制与故障转移锁丢失的隐性根源4.1 master 挂了另一个节点还能相信锁吗聊完业务侧的异常再说一个很多人容易忽略的底层问题Redis 节点故障转移时锁会悄悄丢失。Redis 为了保证高可用通常会部署一主一从或者一主多从的结构。写请求打到 mastermaster 异步复制给 slave。如果 master 在极短时间内宕机而写的锁还没有同步到 slave哨兵会提升某个 slave 为新的 master。此时新的 master 上没有这把锁其他客户端就可以成功加锁临界区被打开。这是异步复制带来的固有问题不是代码能解决的。你加锁时以为自己在资源门外站着实际上门已经悄悄被撤走了但你还站在原位挡别人。4.2 多节点加锁的思路与它的争议有人为了解决这个问题提出了一种多节点加锁思路不是只往一个 Redis 实例上写锁而是同时往多个独立部署的实例上写只有大多数实例都加锁成功才算真正拿到锁。释放锁时也向所有节点发送释放请求。这个思路听起来很美好但它在工程实践中有明显争议。核心问题有两个一是它依赖机器时钟一致如果某个实例的时钟出现较大跳跃过期时间的计算就会失真二是它增加了系统复杂度任何一个节点异常都可能导致加锁失败可用性反而下降。我在实际项目里很少采用这种多节点方案。原因很朴素它解决的是极小概率的故障转移导致锁丢失问题但为此引入了更多的故障可能性和运维复杂度。对于绝大多数业务系统来说这不是一个划算的买卖。4.3 谨慎使用多节点方案优先保证业务幂等如果你的业务确实对锁绝对不能丢失有极致要求比如金融级别的转账扣减那么我建议不要把全部希望寄托在 Redis 上。更好的做法是把锁作为第一道拦截但底层数据库还要有兜底约束。比如扣库存场景可以在数据库表加唯一约束或者乐观锁版本号。即使 Redis 锁因为故障转移真的丢了两个线程同时进入临界区数据库层面的约束也能拦住重复扣减。这就是后面要专门讲的系统性兜底思维。分布式锁不是保险箱它只是众多防线中的第一层。承认这一点反而能让你设计出更健壮的系统。5. 可重入、重试与公平性并发场景的三件小事5.1 为什么需要可重入以及怎么实现不带递归的计数锁在实际业务中一个线程有时会嵌套调用多个需要加锁的方法。比如外层方法加了锁内层方法又调用同一个锁工具类。如果锁不支持重入内层就会发现锁已经被占用直接失败。轻则业务异常重则死锁。Redis 的锁天然就是单实例的同一个键只能被一个持有者拥有。要实现可重入就不能简单用字符串键而是要在值里维护一个重入计数。我用过两种实现方式都有效第一种用 Redis 的 Hash 结构key 是锁名称field 是线程唯一标识value 是重入次数。加锁时对 Hash 的 field 执行一次自增解锁时自减减到 0 才删除整个键。第二种在业务内存里维护一个 ThreadLocal 计数器记录当前线程的重入次数。加锁时先检查 ThreadLocal如果当前线程已持有该锁直接计数字加一不发 Redis 请求解锁时计数减一减到零才真正发 Redis 删除命令。第二种方式性能更好但要注意一个问题线程结束时要清空 ThreadLocal否则线程池复用场景下会留下脏数据。5.2 重试机制直接抛异常还是自旋等待并发请求来了但锁已经被别人持有此时你的程序该怎么办有两条路直接返回失败或者等待重试。直接返回失败的好处是响应快不占资源但对调用方不友好。很多业务场景希望稍等一下就能拿到锁比如秒杀场景用户明明看到还有库存只是别的请求先抢到了。等待重试又分为两种固定间隔轮询和高低峰自旋。固定间隔轮询就是每隔 100 毫秒尝试一次代码简单但如果有大量线程同时等待会产生惊群效应——所有线程在同一时刻同时发起加锁请求Redis 压力突然拉高。自旋等待是在一个 while 循环里持续尝试不释放 CPU适合锁持有时间极短的场景。如果锁持有时间较长自旋会白白浪费 CPU。5.3 自旋等待的退避策略与性能取舍比较折中的做法是随机退避 上限时间每次重试间隔不是固定的而是在一个范围内随机取值降低多个线程同时碰撞的概率同时设定一个最大等待时间超过之后就干脆放弃返回超时。我用过一个参数组合效果不错初始等待 50 毫秒每次等待时间在上一次的基础上乘一个 1.5 到 2.0 之间的随机系数最大等待上限 3 秒最大重试次数 15 次这种方式的本质是通过指数退避尽快脱离高并发碰撞区同时用随机性避免同类线程同步去抢。它的核心思想跟网络传输的拥塞控制很像都是在碰撞发生后主动降低攻击频率。实际测试下来这种退避策略比固定间隔的吞吐量高不少。虽然单次请求的均匀性变差了但整体系统的稳定性好得多。6. 锁失效的系统性兜底别把 Redis 锁当唯一防线6.1 分布式锁之外的防线幂等设计与数据库约束做后端越久越明白一个道理任何分布式组件都可能故障Redis 也不例外。锁丢失、锁过期、网络分区这些都不是小概率事件而是必然事件。所以一个成熟的系统不能只靠锁来保证数据安全。最经典的兜底方案有三类第一类是业务幂等。给每次操作生成一个唯一请求号处理端根据请求号去重。同一请求重复到达时直接返回第一次的处理结果不再执行实际逻辑。这样即使锁失效导致两个线程同时进入后续幂等校验也会拦下来。第二类是数据库约束。比如扣库存场景在数据库表加库存足够才更新的条件语句UPDATE stock SET count count - 1 WHERE goods_id ? AND count 1这样即使用户并发提交了多个请求数据库的行锁和条件判断会保证只有一条更新成功。第三类是乐观锁版本号。更新数据时带上 version 字段只有版本号匹配才更新。面试时经常问到的乐观锁 vs 悲观锁在分布式锁兜底方案里非常实用。6.2 锁粒度与作用域的常见误区分布式锁还有一个藏在细节里的问题锁的粒度。很多人习惯用一个全局锁来保护所有资源比如锁名就叫lock_all。这样写最简单但并发度会降到最低所有请求都要排队严重拖垮系统吞吐。正确做法是按资源维度加锁。比如商品 ID、订单号、用户 ID锁名应该是lock:goods:10001或者lock:order:20230601001。这样不同资源的请求可以并发执行只有同一资源的请求才会彼此等待。锁作用域也要注意。如果多个服务实例共享同一个 Redis但每个服务有自己的服务名前缀锁名里必须包含服务名或者业务类型。否则两个无关业务用了同一个锁名就会互相阻塞还很难排查。6.3 一套可落地的自检清单每次上线涉及分布式锁的代码前我都会对照下面的清单检查一遍。这份清单是在多次线上事故之后总结出来的建议直接抄下来用检查项自查问题合格标准加锁原子性SEL 命令是否同时包含 NX 和 PX单条命令完成不能拆成 SETNX EXPIRE解锁身份校验解锁前是否校验唯一值用 Lua 脚本比较后 DEL过期时间是否设置了合理的过期时间窗口不设会死锁设太短会漏锁看门狗长任务是否自动续期续期前必须校验持有者身份可重入嵌套调用同一锁是否支持Hash 计数或 ThreadLocal 计数重试策略等待锁时是否明确策略固定间隔、指数退避、抛异常三选一幂等兜底底层是否还有数据库约束至少有一层锁之外的防线监控告警锁获取失败是否有指标记录次数、耗时、故障目标这份清单看起来简单但每一条都是血泪教训。能全部通过你的分布式锁代码基本可以放心上生产了。7. 线上踩坑后的几条实操体会聊了这么多最后分享几个我在实际项目里反复验证过的经验。第一条锁的过期时间要单独做成配置并且允许动态调整。不要硬编码成 30 秒、60 秒。业务特征会变化某个任务以前 1 秒完成后来数据量大了变成 10 秒如果锁还停留在初始配置问题迟早爆发。第二条加锁和解锁的日志要带上唯一值。线上排查几乎全靠这两个字段串起来。没有唯一值你根本分不清哪段日志对应哪次加锁。我见过太多因为日志信息不足最后只能靠猜来定位问题的案例。第三条尽量使用现成的高质量客户端库不要自己从零手写。分布式锁看起来简单但看门狗、续期、重入、重试这些机制都暗藏细节。成熟的开源客户端已经把常见坑都处理好了踩过的雷比你预想的多得多。当然用了现成库也要读懂它的原理否则遇到问题还是两眼一抹黑。最后想说的是分布式锁没有银弹。无论你用 Redis、数据库还是其他协调服务都只能提高并发安全的上限不能完全消除风险。真正可靠的设计永远是锁 业务幂等 数据约束多层防线一起上。先保证不产生错误数据再追求高并发下的效率这个顺序不能反。
RELATED

相关推荐

Excel ROW与COLUMN函数实战指南:从序列生成到矩阵运算

Excel ROW与COLUMN函数实战指南:从序列生成到矩阵运算

很多人第一次接触 ROW 和 COLUMN 函数,都是在做序号的时候:在A2输入 ROW()-1 ,下拉,得到1、2、3……看起来简单到不值一提。可等你真正深入研究之后才发现,这一对“行号列号函数”是整个Excel坐标系统的窗口&…

📅 2026/10/22 13:51:37
Log变换:偏态数据归一化的关键技巧与Python实战

Log变换:偏态数据归一化的关键技巧与Python实战

1. 为什么数据归一化绕不开Log变换做数据分析和机器学习建模的朋友,一定都遇到过这种场景:拿到一份数据,先画个分布图,结果发现某个特征列的直方图呈现出极其明显的“右偏”形态,长尾拖出去老远,中位数和均…

📅 2026/10/22 13:51:37
IoTDB数据订阅机制详解:实时流式与TsFile文件订阅的工程实践

IoTDB数据订阅机制详解:实时流式与TsFile文件订阅的工程实践

1. 先从现场问题说起:为什么我最后选择了订阅机制 这几年做工业物联网数据接入,我遇到最多的一个需求,不是“怎么把数据写进IoTDB”,而是“数据刚写进去,怎么让下游系统立刻拿到”。比如一个车间里几十台设备、每台设备…

📅 2026/10/22 13:51:37
MORE NEWS

更多资讯

📰

AI率居高不下?从原理到实操,详解降AIGC平台的10类方向

上个月有个学弟抱着一篇毕业论文初稿来找我,说导师在群里扔下一句话:AI率超过40%的,不用交。他这篇稿子是对话式AI花了一夜拼出来的,自己都没细读,一上学院系统测试,AI率73%。他当时特别茫然:不…

📰

Python调用大模型API实战:从零开始构建你的首个AI应用

很多刚入门的朋友会问我,做“AI大模型”相关的应用到底要不要先把模型部署到本机,是不是得先啃完一堆底层原理。我的回答一直很统一:如果目标是尽快做出能用的东西,先把“Python调用大模型API”这条路走通就足够了。把请求发出去、…

📰

从收藏夹到私域知识库:基于RAG打造B站视频问答系统

1. 收藏夹里的六百个视频,为什么我决定做一个问答系统先说一个我自己的尴尬事。某天晚上我想找半年前看过的一个关于注意力机制的讲解视频,印象里UP主拿Transformer铺开讲了一整期,当时觉得讲得特别好,顺手点了收藏,还…

📰

AI迈入智能体时代:从生成内容到执行任务的技术演进与落地实践

1. 从"能说会道"到"能动手干活":智能体到底改变了什么 如果你在过去两年里深度使用过各类生成式大模型,应该会有一种微妙的感觉:它们很聪明,但总差一口气。你问它一个问题,它能给你一段漂亮的回答…

📰

视觉SLAM前沿思考:几何、学习与多传感器融合的鲁棒性之路

前阵子听了一场关于视觉SLAM的分享,主题是“视觉SLAM最新观点”,分享人是章国锋教授。我一边听一边记了不少笔记,事后又回看了一遍,把里面让我反复琢磨的几个点单拎出来聊聊。这篇不是讲稿复述,算是我消化之后的一个梳…

📰

基于扩散模型的年龄迁移:从少年到老年的连续可控人脸生成

1. 先把任务拆清楚:年龄"迁移"到底在改什么 最近我拿了一批跨越不同年龄段的人脸照片做实验,目标是让同一个人既能回到少年时代,也能一键推到七八十岁,而且要"看起来像同一个人",不是换头&#xf…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬