华为云 DCS Redis 分布式锁的三个失效场景与可落地写法

举报
yd_295232380 发表于 2026/10/30 09:34:15 2026/10/30
【摘要】 定时任务在单机上跑得好好的,一旦扩到多副本,同一份对账逻辑就会有两台机器同时执行:一台在扣减库存,另一台在生成对账单,最后账目对不上。这类问题最常见的止血方案就是分布式锁。我们用华为云 DCS for Redis 做这件事走了不少弯路,这篇把三个真实失效场景和最终的写法整理出来。一、只用 SETNX 加锁,三个漏洞同时存在最初版本是这样的:先用 SETNX 抢 key,抢到就 SET 一个过...

定时任务在单机上跑得好好的,一旦扩到多副本,同一份对账逻辑就会有两台机器同时执行:一台在扣减库存,另一台在生成对账单,最后账目对不上。这类问题最常见的止血方案就是分布式锁。我们用华为云 DCS for Redis 做这件事走了不少弯路,这篇把三个真实失效场景和最终的写法整理出来。

一、只用 SETNX 加锁,三个漏洞同时存在

最初版本是这样的:先用 SETNX 抢 key,抢到就 SET 一个过期时间,业务跑完再 DEL。上线后出了三件事。

第一件是锁永远不会释放。SETNX 成功之后、EXPIRE 之前,进程被 kill 或者机器重启,key 就永久留在库里,后面所有实例都抢不到锁,任务直接停摆。加锁和设过期必须是原子的,一条命令完成。

第二件是删掉了别人的锁。A 的锁因为业务超时已经过期,B 抢到了锁;这时候 A 跑完执行 DEL,把 B 的锁删了,C 马上又抢到,于是 A 和 C 并发。所以锁的值必须是每次加锁生成的唯一标识(UUID 或「实例ID+线程ID+时间戳」),删除前先比对。

第三件是过期时间拍脑袋。业务正常耗时 3 秒,就把 TTL 设成 5 秒,遇到一次慢查询就会提前过期。TTL 应该是「P99 耗时 × 安全系数」,并且要有续期机制兜底。

二、加锁的正确写法

一条 SET 命令同时完成互斥和过期:

SET lock:reconcile "8f3c2a1e-..." NX PX 30000

返回 OK 表示抢到,返回 nil 表示被别人持有。这里用 PX 毫秒而不是 EX 秒,是因为很多短任务的临界区只有几百毫秒,秒级 TTL 要么太长要么没法设。NX 保证只在 key 不存在时写入,整条命令在 Redis 内部是原子的,不存在「设了 key 没设过期」的中间状态。

拿到锁之后,业务代码里一定要 try/finally,把释放放在 finally 里,避免异常路径漏放锁——虽然 TTL 能兜底,但让锁提前 30 秒可用总比干等好。

三、解锁必须用 Lua 比对持有者

「先 GET 比对、再 DEL」在客户端是两步,中间可能被别的实例插进来,仍然不安全。用 Lua 脚本让比对和删除在服务端原子执行:

if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
else
  return 0
end

返回值 0 说明锁已经不是自己的了(过期后被人抢走),这种情况要在日志里打一条 warn:它意味着业务执行时间超过了 TTL,是容量或慢查询的信号,不能当无事发生。

四、续期要不要做

只要 TTL 比真实耗时短,就一定会出现「锁提前释放 + 双写」。续期(看门狗)的做法是起一个后台线程,在 TTL 的三分之一处检查锁是否还属于自己,属于就 PEXPIRE 续到原值,不属于立刻停止业务。

续期能救慢任务,但会带来新问题:线程卡死时续期线程还在续,锁变成永久持有。所以我们的做法是给业务加硬超时——看门狗最多续 3 次,超过就放弃续期并记录告警,让锁自然过期。宁可任务失败重跑,也不要两个实例同时写。

五、主从切换这件事必须说清楚

DCS 主备实例在网络分区时,主节点可能还没来得及把锁同步到备节点就被切走,新主节点上锁不存在,另一个客户端立刻抢到锁。这不是实现 bug,是异步复制的固有特性,任何基于单实例的锁都有这个窗口。

对这个窗口的处理分两种:临界区是幂等的(比如重复生成同一份对账单,靠唯一索引去重),接受这个窗口,用单实例锁就够了;临界区绝对不能重复(比如扣款),就要在业务侧加第二道防线——数据库唯一约束、状态机 CAS、或者乐观锁版本号。指望分布式锁独自保证正确性,是这类事故最根本的原因。

至于多节点 Redlock,业界争论多年,我们的结论很朴素:它能把窗口压小,但压不到零,还要多付几个实例的延迟和运维成本。既然最终还是要靠数据库约束兜底,多数场景没必要上。

六、上线前检查清单

锁的 key 带业务前缀和维度,不要用一个全局 key 把所有任务串行化;锁的值必须是唯一标识,不能用固定的 "1";加锁用 SET NX PX 一条命令,禁止 SETNX + EXPIRE 两步;释放用 Lua 比对后删除;TTL 按 P99 的三倍设,配看门狗并设续期上限;抢锁失败要退避重试,不要立刻失败也不要死循环;锁内代码只做临界区,日志、HTTP 调用尽量挪到锁外;监控三项指标——加锁失败率、锁持有时间分布、比对失败次数(最后一个最能暴露 TTL 设置不合理)。

把这七条对齐之后,我们的对账任务从每周两次重复执行降到零,锁相关告警也基本消失。分布式锁本身不难写,难的是承认它只是「降低并发概率」的手段,真正的正确性还得靠数据层的约束来保证。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。