华为云 DCS for Redis 分布式锁实战:加锁要原子,释放要校验
一次线上事故的复盘:并发压测下订单重复扣款,查到最后发现是"假锁"惹的祸。
一、假锁比没锁更可怕
分布式系统里,多实例同时操作同一资源是家常便饭:库存扣减、定时任务、幂等控制。没有分布式锁,会出现超卖和重复执行;而用错锁,比没锁还危险——你以为已经安全了,其实漏洞一直都在。
最常见的错误写法是 SETNX + EXPIRE 两条命令拼接。问题在于它们不是原子操作:加锁成功、还没来得及设过期时间,实例就挂了,锁永远不释放,直接变成死锁;就算设了过期时间,业务还没跑完锁就到期,别的实例立刻拿到锁,并发照旧发生。
二、正确姿势:SET NX PX 一步到位
Redis 2.6.12 之后,SET 命令原生支持 NX 和 PX,可以把"判断 key 是否存在"和"设置过期时间"合并成一个原子操作,还支持毫秒级精度:
import redis, uuid
r = redis.Redis(host="你的DCS实例地址", port=6379,
password="***", decode_responses=True)
def acquire(key, ttl_ms=10000):
token = uuid.uuid4().hex # 每个持锁者一个唯一标识
ok = r.set(key, token, nx=True, px=ttl_ms)
return token if ok else None
一条命令完成加锁与超时,既不会有死锁,也不会出现"忘了设 TTL"的低级错误。生产环境建议配合连接池(如 redis.ConnectionPool)复用连接——DCS 实例的连接数是有配额的,每次加锁都新建连接很容易把配额打满。
三、释放锁必须原子
释放锁时如果直接 DEL,可能误删别人的锁:A 的锁到期后 B 拿到了锁,A 执行完却把 B 的锁删掉了。正确做法是先校验 value 再删除,而且校验与删除要用 Lua 脚本原子完成,否则校验通过到删除之间仍可能被抢占:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
在 Python 里调用:
UNLOCK_LUA = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
def release(key, token):
r.eval(UNLOCK_LUA, 1, key, token)
另外,Lua 脚本建议复用:先用 SCRIPT LOAD 拿到返回值 SHA,后续用 EVALSHA 调用,能省去每次传输脚本文本的开销,在高 QPS 场景下这笔优化相当可观。
四、实战避坑清单
- 锁的值必须唯一:用 UUID 或线程 ID,绝不能用固定字符串,否则无法区分锁的归属,误删风险极高。
- 想清楚锁续期:业务耗时不可控时,用后台"看门狗"线程定期续期(Redisson 的思路),或把 TTL 设得足够大,同时监控实际执行时长。
- 集群模式要分清:DCS 主备架构下,上述单实例方案足够稳妥;只有在主备切换会破坏互斥的场景下才考虑 Redlock,且要知道它在极端网络分区下的争议,别盲目照搬。
- 连接要保活:DCS 会回收长时间空闲的连接,客户端务必开启 keepalive 与自动重连,避免一次锁操作直接抛连接异常。
- 锁不等于超时控制:锁只保证互斥,不保证执行时长。超时逻辑必须单独设计,不能指望锁自己兜底。
五、小结
分布式锁的核心其实就两句话:加锁要原子(SET NX PX),释放要校验(Lua 比对 value)。再配上合理的 TTL 与续期机制,以及对 DCS 连接的健康管理,绝大多数并发问题都能兜住。锁用对了,线上事故能少一大半。把上面五条检查项逐一过一遍,基本能避开绝大多数与锁相关的事故。
- 点赞
- 收藏
- 关注作者
评论(0)