Redis 分布式锁怎样处理客户端超时
来源:17golang原创
时间:2026-10-14 10:09:10 425浏览 收藏
Redis 分布式锁遇到客户端超时,应先把结果视为未知,而不是直接认定没拿到锁。只有在明确收到成功响应、剩余租约足够且当前任务仍有效时,才进入受保护业务;释放和续租必须原子比较本次申请的身份值。租约到期并不会停止旧进程,重要外部写入还需要资源端的并发保护。
官方地址:https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/。本文解释单实例租约的工程处理思路,不将它描述为跨故障转移的强一致锁;示例没有实际执行。
先分清请求超时、任务超时和租约到期
并发任务常把缓存重建、批量处理等工作集中在某个资源键上,锁能减少同时执行,但客户端只看到自己等待响应的时间。网络断开时,Redis 可能已经写入锁键,只是成功响应没有到达调用方。请求等待结束不能证明服务端操作没有发生。
任务超时是业务不再愿意等待,租约到期是锁键的有效期限结束,三者不是同一件事。举个教学场景:业务请求只愿等待很短时间,而锁有更长的有效期;请求取消后,锁仍可能暂时存在。反过来,线程暂停过久后恢复,任务代码还活着,但锁可能已由别人获得。这不是线上日志,也不涉及固定推荐秒数。
| 现象 | 能确定什么 | 保守处理 |
|---|---|---|
| 加锁明确成功 | 该次申请得到响应 | 检查剩余有效期及业务截止时间 |
| 明确未取得锁 | 该命令条件未满足 | 有界退避,避免拥挤重试 |
| 请求超时或断连 | 没有可靠结果 | 不执行受保护业务,按身份尝试清理 |
| 续租失败或结果未知 | 不能确认可继续使用的租约 | 停止新副作用,交由资源端拒绝过期写入 |
为每次申请建立独立身份,而不是只存一个开关
锁记录包含资源键、每次申请独立生成的身份值和 TTL。身份值应由可靠随机源产生,不把固定机器名、进程号或共享常量当作每次申请的唯一标识。SET 的 NX 条件与 PX 过期时间应在同一次命令中设置,避免先建键、后设过期之间留下中断窗口。
下面的 Lua 示例用于阅读:KEYS[1] 是锁键,ARGV[1] 是本次身份值,ARGV[2] 是正整数毫秒租约。它将结果归一为 1 或 0,不是完整锁库。
-- 本次申请身份和租约必须由调用方准备,不能使用固定共享身份
local ttl = tonumber(ARGV[2])
if not ttl or ttl
调用方还需区分“收到了 0”与“没有收到响应”。明确返回值是业务判断输入,传输错误不是另一个表示失败的 0。不要让客户端封装把所有异常吞掉后统一返回未加锁。

结果未知时,不进入业务,也不无条件删除键
申请超时后,记录这次申请的身份,把它置于不能开展业务的状态。若仍能连接,可以使用有界、独立于已取消业务请求的清理上下文,做条件释放;清理失败就记录未确认,不无限等待。TTL 为残留锁提供最终释放机制,但暂时等待会影响可用性。
释放示例只删除身份相符的当前键。不能将 GET 和 DEL 拆成两个请求,因为两者之间锁可能过期并被另一申请获得。脚本的原子性只覆盖 Redis 内部这些操作,不覆盖后续数据库或 HTTP 调用。
-- 先读取当前身份,在同一脚本内完成判断和删除
local current = redis.call("GET", KEYS[1])
if current ~= ARGV[1] then
return 0 -- 键不存在或属于别人的申请,均不能删除
end
return redis.call("DEL", KEYS[1])
返回 0 不应解释为异常损坏,也不证明业务没有执行过。释放请求本身超时同样是未知结果;重试只能沿用这次身份做条件释放,不能改为强制删除。
另一个常见误区是把同一身份的 SET NX 自动重试视为幂等。第一次可能成功,第二次会因为键已经存在而失败,两次响应语义不同。应了解 SDK 对写请求的重试行为;本次状态结束后,新申请使用新身份,并用业务幂等键处理业务层重试。
续租能提高可用性,不能独自保护旧持有者写入
任务执行时间变化大时,续租可以减少租约过早结束,但续租必须仅延长当前身份相符的键,不能让过期的旧申请重新建立锁。提前量应覆盖网络等待、调度暂停和业务收尾,并设置总执行期限,避免后台续租永久占用资源。
-- 仅为仍属于本次申请的键延长租约,不创建新键
local ttl = tonumber(ARGV[2])
if not ttl or ttl
续租失败、超时或发现有效时间不足时,应取消本地业务并停止启动新副作用。然而取消通常是协作式的,已经发出的外部写请求可能无法撤回,进程长时间暂停也可能无法及时检查租约。这是单纯“续租线程一直活着”无法解决的边界。
对必须保证正确性的共享资源,可以让资源端识别 fencing token:每次合法获得权限对应更高的代次,写入同时携带该代次,资源端在原子写入条件中拒绝落后的代次。随机锁身份只用于区分申请,不具备单调顺序,不能直接当作 fencing token。
代次分配、持久保存和资源端比较必须与授权协议一并设计;不能随手在可回退的 Redis 计数器上 INCR 就宣称已经完成。资源端若无法识别代次,应考虑数据库事务、唯一约束、版本条件更新或已有一致性协调机制。幂等处理可以避免同一业务请求重复执行,但不自动阻止不同申请的旧写覆盖新写。

先按业务代价采用,再观察不确定状态
对重复计算只增加成本的工作,可以先明确可容忍重复范围,再采用租约减少并发。对会产生不可逆副作用的工作,应先把资源端的并发控制与幂等机制准备好,锁只是其中一层。异步复制发生故障转移时,尚未复制的锁可能丢失,不能把主从自动切换当作互斥保证。
落地可以分为三个阶段:先梳理每个操作是否可重复及取消点,再确定 SDK 的超时和重试语义,最后在独立环境模拟响应丢失、进程暂停和续租失败。观察重点不是只统计加锁成功率,而是未知结果数量、条件释放未确认数量、续租失败次数、剩余有效期不足次数和资源端拒绝旧写的次数。这里是读者实施建议,本文没有运行故障实验。
租约长度不应凭平均耗时决定,也不应无限调大。结合长尾任务耗时、暂停风险、资源端保护和恢复等待目标,给出有边界的策略。最终应能回答:请求超时后谁还能写、谁负责停止任务、旧持有者的迟到写入会被哪一层拒绝。
相关问题
客户端超时可以立即重试业务吗?不能仅凭超时重试副作用。先把锁结果记为未知,并让业务请求具有独立幂等或结果查询机制。
延长 TTL 能彻底解决暂停问题吗?不能。更长租约只是改变等待与重叠风险,不能保证暂停总是短于期限。
随机身份与 fencing token 可以合并吗?两者职责不同。前者辨别谁可以释放锁,后者让资源端比较写入代次;是否合并要由完整授权协议决定。
机制参考:https://redis.io/docs/latest/commands/set/;脚本说明:https://redis.io/docs/latest/develop/programmability/eval-intro/;资源端 fencing 的分析:https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html。示例仅解释协议要点,生产使用应选择符合故障模型的维护中实现。
-
483 收藏
-
277 收藏
-
363 收藏
-
380 收藏
-
207 收藏
-
119 收藏
-
134 收藏
-
335 收藏
-
458 收藏
-
445 收藏
-
146 收藏
-
370 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习