ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

Redis 应用实战(4):分布式锁实现 上一篇处理了流量和容量集中本篇转向并发执行集中多个进程都认为自己应该修改同一资源。Redis 锁只是一份带期限的协调记录不是数据库事务。可靠边界由原子获取、随机所有者令牌、比较后释放以及由最终资源验证的 fencing token 共同组成。一、边界先判断是否真的需要锁库存扣减若能写成数据库UPDATE ... WHERE stock 0唯一约束若能拒绝重复订单原子条件写通常比外部锁更可靠。锁适合协调定时任务、缓存重建或只能串行调用的外部资源。把锁加在“先读再写”的外围却不在最终存储层验证版本会让过期持有者仍能覆盖新结果。最小正确获取是SET resource token NX PX 10000NX 防止覆盖已有锁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_token0self.valueNonedefwrite(self,token,value):iftokenself.last_token:returnFalseself.last_tokentoken self.valuevaluereturnTrueresourceFencedResource()holder_a41holder_b42accepted_bresource.write(holder_b,new-result)accepted_aresource.write(holder_a,stale-result)assertaccepted_bisTrueassertaccepted_aisFalseassertresource.valuenew-resultprint(fholder_b_accepted{accepted_b})print(fholder_a_accepted{accepted_a})print(fstored_token{resource.last_token})print(fstored_value{resource.value})运行输出holder_b_acceptedTrue holder_a_acceptedFalse stored_token42 stored_valuenew-result下面脚本完整演示获取、生成 fencing token、续期和安全释放。两个 Lua 脚本都验证 token示例临界区很短生产租约应根据执行时间分布和故障预算决定。#!/usr/bin/env bashset-euopipefailredis_url${REDIS_URL:-redis://127.0.0.1:6379/0}lockdemo:lock:reportsequencedemo:lock:sequencetokenowner-$$-$(date%s%N)redis-cli-u$redis_urlDEL$lock$sequence/dev/nullresult$(redis-cli-u$redis_url--rawSET$lock$tokenNX PX5000)if[[$result!OK]];thenprintflock busy\n2exit1fifence$(redis-cli-u$redis_url--rawINCR$sequence)renewif redis.call(get,KEYS[1])ARGV[1] then return redis.call(pexpire,KEYS[1],ARGV[2]) else return 0 endrenewed$(redis-cli-u$redis_url--rawEVAL$renew1$lock$token5000)test$renewed1printffencing_token%s\n$fenceprintfcritical_sectioncompleted\nreleaseif redis.call(get,KEYS[1])ARGV[1] then return redis.call(del,KEYS[1]) else return 0 endreleased$(redis-cli-u$redis_url--rawEVAL$release1$lock$token)printfreleased%s\n$releasedredis-cli-u$redis_urlDEL$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 官方文档SETRedis 官方文档EVAL 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《Redis 应用实战》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。
返回列表