news 2026/9/6 11:34:10

Redis 应用实战(4):分布式锁实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis 应用实战(4):分布式锁实现

上一篇处理了流量和容量集中,本篇转向并发执行集中:多个进程都认为自己应该修改同一资源。Redis 锁只是一份带期限的协调记录,不是数据库事务。可靠边界由原子获取、随机所有者令牌、比较后释放,以及由最终资源验证的 fencing token 共同组成。

一、边界:先判断是否真的需要锁

库存扣减若能写成数据库UPDATE ... WHERE stock > 0,唯一约束若能拒绝重复订单,原子条件写通常比外部锁更可靠。锁适合协调定时任务、缓存重建或只能串行调用的外部资源。把锁加在“先读再写”的外围,却不在最终存储层验证版本,会让过期持有者仍能覆盖新结果。

最小正确获取是SET resource token NX PX 10000:NX 防止覆盖已有锁,PX 设置租约,随机 token 标识所有权。SETNX后再EXPIRE有崩溃窗口,会留下死锁。释放不能直接DEL:客户端 A 停顿至锁过期,B 获取新锁,A 恢复后裸删就会删除 B 的锁。比较 token 与删除必须在同一个 Lua 脚本中原子完成。

二、原理:租约解决活性,不保证旧持有者失效

租约必须长于正常临界区,但进程暂停、网络抖动和下游慢请求都可能超时。自动续期只能减少过期概率:若续期线程同样暂停,锁仍会丢;若客户端与 Redis 失联,它也不知道自己是否还持有锁。因此业务动作要幂等,并使用 fencing token 防止陈旧写。

fencing token 是锁服务每次成功授予时递增的编号。受保护存储只接受大于已见编号的写入:即使编号 41 的旧客户端晚到,也会被已经处理编号 42 的存储拒绝。Redis 自增可产生编号,但必须把校验落在真正资源侧;如果下游 API 无法比较 token,就无法获得这种防护。

单实例在主节点确认写后、复制到副本前崩溃,故障转移可能丢失锁,新主又授予一次。能否接受取决于风险模型。缓存重建通常能接受偶发并行;金融扣款不能把 Redis 锁当唯一正确性屏障。多节点 Redlock 也有时钟、网络和暂停假设,使用前应明确安全目标,而不是因节点更多就宣称绝对安全。

三、实现:所有权检查与 fencing token

下面程序模拟两个持有者乱序完成。资源记录最后 token,并拒绝过期持有者;这是需要由数据库条件更新或下游服务实现的关键契约。

classFencedResource:def__init__(self):self.last_token=0self.value=Nonedefwrite(self,token,value):iftoken<=self.last_token:returnFalseself.last_token=token self.value=valuereturnTrueresource=FencedResource()holder_a=41holder_b=42accepted_b=resource.write(holder_b,"new-result")accepted_a=resource.write(holder_a,"stale-result")assertaccepted_bisTrueassertaccepted_aisFalseassertresource.value=="new-result"print(f"holder_b_accepted={accepted_b}")print(f"holder_a_accepted={accepted_a}")print(f"stored_token={resource.last_token}")print(f"stored_value={resource.value}")

运行输出:

holder_b_accepted=True holder_a_accepted=False stored_token=42 stored_value=new-result

下面脚本完整演示获取、生成 fencing token、续期和安全释放。两个 Lua 脚本都验证 token;示例临界区很短,生产租约应根据执行时间分布和故障预算决定。

#!/usr/bin/env bashset-euopipefailredis_url="${REDIS_URL:-redis://127.0.0.1:6379/0}"lock='demo:lock:report'sequence='demo:lock:sequence'token="owner-$$-$(date+%s%N)"redis-cli-u"$redis_url"DEL"$lock""$sequence">/dev/nullresult="$(redis-cli-u"$redis_url"--rawSET"$lock""$token"NX PX5000)"if[["$result"!=OK]];thenprintf'lock busy\n'>&2exit1fifence="$(redis-cli-u"$redis_url"--rawINCR"$sequence")"renew='if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("pexpire",KEYS[1],ARGV[2]) else return 0 end'renewed="$(redis-cli-u"$redis_url"--rawEVAL"$renew"1"$lock""$token"5000)"test"$renewed"=1printf'fencing_token=%s\n'"$fence"printf'critical_section=completed\n'release='if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end'released="$(redis-cli-u"$redis_url"--rawEVAL"$release"1"$lock""$token")"printf'released=%s\n'"$released"redis-cli-u"$redis_url"DEL"$sequence">/dev/null

四、工程化:等待、续期和可观测性

争锁失败不要固定间隔自旋,否则所有客户端会同步轰击 Redis。采用带随机抖动的指数退避,设置总等待上限,并让调用方能取消。等待时间超过业务截止时间就应失败,而不是拿到锁后已无时间完成工作。临界区不要包含不必要的网络请求;把准备工作移到锁外,并再次校验锁内条件。

续期只允许当前 token 执行,最大持有时间必须有限,防止逻辑错误永久占锁。进程收到终止信号时可尽力释放,但正确性不能依赖优雅退出。指标至少包括获取成功率、冲突率、等待分布、持有时长、续期失败、租约过期和业务重复执行次数;日志记录资源类别与 token 摘要,不记录敏感完整 key。

锁 key 的 Cluster 哈希槽会影响 Lua 涉及的多 key 操作。若要在脚本中同时操作锁与序列,二者需用相同哈希标签,例如lock:{report}seq:{report};本文为兼容单实例演示分开调用,因此 fencing 编号与获取并非一个原子授予动作。严格系统应将授予流程放在同槽脚本或专门协调服务中。

五、验证:主动制造暂停与失联

单元测试不足以证明分布式行为。集成测试应让 A 获取锁后暂停超过 TTL,让 B 获取并写入,再恢复 A,确认下游拒绝旧 fencing token;还要模拟 Redis 命令超时但服务端实际成功,确认重试不会形成两个业务动作。释放脚本要测试 token 不匹配时返回 0 且不删除。

评审时逐项询问:租约多长、最大等待多久、谁续期、如何取消、进程崩溃如何恢复、主从切换是否允许重复、业务是否幂等、下游能否 fencing。没有答案的锁只是把竞态隐藏在罕见时序里。

还要区分锁粒度。全局锁实现简单,却把无关订单串行化;按资源 ID 加锁并发更高,但 key 数、监控与死锁排查更复杂。一次操作若必须获取多把锁,应固定全局顺序,并在拿不到后释放已持有锁,否则会形成循环等待。更稳妥的做法往往是重新划分事务边界,让一次业务动作只拥有一个聚合根。

安全测试应记录线性时间线,而不只是最终断言:客户端何时发送获取、Redis 何时确认、租约何时到期、业务何时提交、释放返回什么。这样才能区分“重复执行但被幂等吸收”和“锁本身从未重叠”。若告警只统计获取失败,会漏掉最危险的租约内执行超时;应单独统计临界区时长超过租约比例,并对持续上升立即降载。

锁解决互斥,不适合承载工作历史、重试与消费者进度。下一篇转向 Redis 的 List、Pub/Sub 与 Stream,分别建立简单队列、瞬时广播和可恢复消费模型。

参考来源

  • Redis 官方文档:分布式锁
  • Redis 官方文档:SET
  • Redis 官方文档:EVAL

👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。

🚀 本文属于《Redis 应用实战》系列,持续更新,关注不迷路。

📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 11:33:27

上位机开发实战:从通信协议选型到项目落地全解析

1. 上位机不是"一台电脑"那么简单&#xff1a;先把行业底层逻辑捋清楚1.1 上位机和下位机怎么分工先回答一个很多新人问过我的问题&#xff1a;上位机到底是啥&#xff1f;简单说&#xff0c;上位机就是发出指令、做数据展示和分析的那一端&#xff0c;通常跑在PC、工…

作者头像 李华
网站建设 2026/9/6 11:32:11

腾讯云AI Skills实战:Agent技能开发与编排避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 11:27:14

基于Stable Diffusion的角色定向图像生成:萍琪派鬃毛打理场景实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 11:21:56

JMeter性能测试实战:从安装到压测报告全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 11:21:28

TinySR轻量级扩散模型实战:真实世界图像超分辨率部署指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 11:18:21

客户端侧架构如何赋能Sleeper选秀与阵容优化——以Scout Bowie为例

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华