尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Redis 分布式锁的正确实现:SET NX PX + Lua 释放,别再写原子性有 bug 的锁了
Redis 分布式锁的正确实现SET NX PX Lua 释放别再写原子性有 bug 的锁了这篇文章解决什么问题在多个服务实例共享同一份数据时用 Redis 做分布式锁是最常见的方案。但「把锁加出来」和「把锁写对」是两回事加锁和设过期时间如果不是一个原子操作进程崩溃就会留下死锁释放时如果不校验持有者就可能误删别人的锁。本文从最朴素的写法开始一步步指出 bug 并给出正确实现读完能直接用于生产。适合谁读需要在后端做并发控制、防止重复扣款/重复下单等场景的开发者。为什么需要分布式锁单机环境下synchronized、Lock、Mutex就能做互斥因为所有线程共享同一个 JVM/进程内存。但服务一旦部署成多实例或多台机器内存锁就失效了——两个实例各自加锁互不知情共享资源照样被并发修改。这时需要一个所有实例都能访问的「公共仲裁者」Redis 因其高性能和原子命令天然适合这个角色。典型的应用场景防止同一订单被并发重复支付防止定时任务在多实例下被重复执行防止库存超卖配合乐观锁或库存扣减逻辑写法一先 SET 再 EXPIRE有 bugimportredisimportuuid rredis.Redis(hostlocalhost,port6379,decode_responsesTrue)defacquire_bad(key):# 1. 先尝试加锁ifr.set(key,1,nxTrue):# nxTrue 表示不存在才设置# 2. 再设置过期时间r.expire(key,30)returnTruereturnFalse这段代码的问题在于set和expire是两个独立命令中间没有原子性保证。如果进程在set成功之后、expire执行之前崩溃了这个 key 就永远不会过期——锁被永久占用其他实例永远拿不到形成死锁。即使不崩溃网络抖一下也可能让expire丢失。写法二不校验持有者直接 DEL还是有 bug有人为了省事释放锁时无条件删除defrelease_bad(key):r.delete(key)看起来没问题看这个时序实例 A 拿到锁expire设了 30 秒。实例 A 执行业务因为 GC 或网络原因卡了 35 秒此时锁已自动过期。实例 B 趁锁过期拿到锁正在操作共享资源。实例 A 恢复运行执行release_baddelete掉的是 B 当前持有的锁。此时锁消失实例 C 又拿到了锁——B、C 同时在操作共享资源互斥被打破。这就是「误删别人的锁」。要解决它必须让每个锁带上持有者标识释放前校验「这个锁是不是我加的」。正确实现加锁用 SET NX PX释放用 Lua 原子校验加锁一条命令同时完成「加锁 过期」importredisimportuuid rredis.Redis(hostlocalhost,port6379,decode_responsesTrue)defacquire(key,expire_seconds30):tokenuuid.uuid4().hex# 每个实例每次加锁生成唯一标识# SET key token NX PX 毫秒原子地「不存在则设置且带过期时间」okr.set(key,token,nxTrue,pxexpire_seconds*1000)returntokenifokelseNone# 返回 token 用于后续释放校验redis-py的set(name, value, nxTrue, px...)底层就是一条SET key value NX PX ms命令天然原子。NX保证只在 key 不存在时设置PX参数以毫秒为单位设置过期时间。加锁和过期绑定在同一条命令里前面写法一「崩溃留死锁」的问题就消除了。这里用uuid4生成随机 token 而不是固定值「1」是为了让每个持有者可被区分。释放Lua 脚本里「先校验、再删除」释放锁必须保证「校验 token 是否匹配」和「删除 key」两个动作原子执行否则校验通过后、删除之前锁可能刚好过期被他人拿走。Redis 的 Lua 脚本在服务端原子执行正好满足ifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end用 redis-py 执行RELEASE_SCRIPT if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end defrelease(key,token):resultr.eval(RELEASE_SCRIPT,1,key,token)returnresult1# 1 表示成功删除锁确实是我的0 表示不是我的或已失效只有当 key 的当前值等于传入的 token 时才删除这样就避免了写法二的「误删他人的锁」。完整使用示例带失败重试importtimedefdo_with_lock(r,key,expire_seconds30,retry_times3):tokenacquire(key,expire_seconds)iftokenisNone:print(获取锁失败)returnFalsetry:# 这里放你的临界区业务逻辑print(拿到锁执行业务 ...)time.sleep(1)returnTruefinally:# 无论如何都要释放锁release(key,token)需要注意finally里释放的前提是「锁还属于自己」。如果业务耗时超过了过期时间锁已经自动过期且可能被他人拿走此时release会返回 0不误删这是正确行为。因此关键原则是锁的过期时间要大于业务最坏执行时间或者在业务内做续期。续期业务跑得久怎么办如果业务执行时间可能超过锁的过期时间可以用「看门狗」机制持锁期间定期检查若即将过期且自己仍持有就把过期时间往后延。这里给出一个简化的续期 Lua 脚本ifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(pexpire,KEYS[1],ARGV[2])elsereturn0end调用时传入新的毫秒数即可。续期同样通过「校验 token 是否匹配」保证只给自己的锁续期。生产环境中RedissonJava已经内置了 watchdog 自动续期避免重复造轮子。与其他方案对比方案优点缺点数据库唯一索引/行锁实现简单事务保证强一致性能差易产生锁等待连接资源占用Zookeeper 临时顺序节点强一致可靠客户端崩溃自动释放性能较低部署维护成本高Redis SET NX PX Lua高性能实现简洁依赖 Redis 单点/主从切换时可能丢失锁非强一致Redis 方案在「极端情况下」不是绝对安全的如果使用了主从架构主节点刚写入锁还未同步到从节点就宕机发生主从切换后锁可能丢失。如果你的业务对一致性要求极高如金融资金操作应评估 RedLock 或 Zookeeper 等更强一致性的方案一般业务场景下单实例 Redis 的 SET NX PX Lua 已经足够可靠。小结加锁必须用SET key value NX PX一条命令原子完成「加锁 过期」别拆成setexpire。释放必须校验持有者 token并用 Lua 脚本让「校验 删除」原子执行别无条件DEL。token 用随机唯一值如 UUID区分每次加锁。锁的过期时间要大于业务最坏执行时间长时间任务考虑 watchdog 续期。拿本文代码在本地起一个 Redis 用两个进程/线程并发测试观察 acquire 只有一个返回非 None、release 不会误删是理解这些细节最直接的方式。延伸阅读Redis 官方文档中 SET 命令 与 EVAL/Lua 脚本、Martin Kleppmann 关于分布式锁的经典讨论文章。
RELATED

相关推荐

时间戳转换器:秒/毫秒与本地时间互转,跨时区不再心算

时间戳转换器:秒/毫秒与本地时间互转,跨时区不再心算

打开网页,顶部就每秒刷新「当前时间 当前时间戳(秒/毫秒)」;下面一边输入 Unix 时间戳、一边选个日期时间,几个时区、几种单位来回换算,点一下就复制走。10 位秒、13 位毫秒自动识别,负数时间戳…

📅 2026/11/1 19:16:54
2026年8月27日星期四:从起床到入睡的全天时间管理实战指南

2026年8月27日星期四:从起床到入睡的全天时间管理实战指南

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

📅 2026/10/3 0:07:41
RWA技术解析:从资产代币化到合规架构实战指南

RWA技术解析:从资产代币化到合规架构实战指南

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

📅 2026/9/9 17:31:24
MORE NEWS

更多资讯

📰

启动报Port 8088 was already in use?先查是谁占的

启动报Port 8088 was already in use?先查是谁占的 项目启动报 Port 8088 was already in use,意思很直白:这个端口已经有人占了。 先说原理:端口是"门牌号",一间房只住一个进程 端口就是一台机器上的门牌号…

📰

担心文档上云?本地运行的办公AI才敢放心用

单位上周开了个会,主题是"AI 提效",散会时领导补了一句:涉及客户资料的文档,不许往云端传。这话一出,会议室里本来跃跃欲试的同事全蔫了——大家平时用的那些在线 AI,文档一上传就不知道存哪儿去…

📰

先能用,再好用:从烂尾复盘到两周上线,全栈项目的‘原型—快速迭代‘方法论

先能用,再好用:从烂尾复盘到两周上线,全栈项目的“原型—快速迭代”方法论 全栈学习路径的文章已经很多了:怎么选技术栈、怎么搭环境、怎么记文档,几乎每个方向都有人写。但一个更扎心的问题很少有人正面讨论——为什…

📰

深度思考模式把 ARC-AGI-2 从 17.6% 拉到 45.1%:对照 Gemini 3 Pro 的三份能力评测,看‘会思考‘改写了哪些边界

深度思考模式把 ARC-AGI-2 从 17.6% 拉到 45.1%:对照 Gemini 3 Pro 的三份能力评测,看“会思考”改写了哪些边界 一、先说结论:变强的不是“知识”,而是“解法” 每次新模型发布,评测榜单都会涨几个点,大…

📰

1.c++入门篇

1.c中的关键字asmdoifreturntrycontinueautodoubleinlineshorttypedefforbooldynamic_castintsignedtypeidpublicbreakelselongsizeoftypenamethrowcaseenummutablestaticunionwchar_tcatchexplicitnamespacestatic_castunsigneddefaultcharexportnewstructusingfriendclassext…

📰

《江灯合声》:有可行的分框版,也仍可以选择连续鸣谢

武汉江岸区栎坡合唱团在免费节目册《江灯合声》中,以表格集中鸣谢资助者。各家共同要求改为独立小框,团里仍想保留表格。现有全名与支持事项清楚可见,双方此前未承诺具体排版。我支持保留表格。补充的试排材料让选择更具体:分框能…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

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

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

📞 💬