用 Redis 做分布式锁,我踩过的三个坑
用 Redis 做分布式锁,我踩过的三个坑
去年给一个订单服务加分布式锁,用的就是最常见的 SETNX + EXPIRE 那套,当时觉得逻辑简单,随手就上了。上线后陆续出了三次问题,每次都是深夜被报警叫起来。这三条经验记下来,希望后来人少踩一遍。
背景
场景很典型:用户下单时,同一订单不能被重复处理。单机时用一个本地锁 synchronized 就够了,但服务是多实例部署,本地锁拦不住跨实例的并发。于是引入 Redis 分布式锁。
最初的实现:
def acquire_lock(key, ttl=10):
ok = redis.setnx(key, "1")
if ok:
redis.expire(key, ttl)
return ok
现在回头看,这段代码有两个隐藏的致命问题。
坑一:setnx 和 expire 不是原子的
setnx 成功和 expire 之间如果服务宕机,这把锁就永远不会过期,成了死锁。这个坑我当时没遇到(概率低),但确实是隐患。
正确做法是用一条命令完成:
def acquire_lock(key, token, ttl=10):
# SET key value NX PX ttl —— 原子操作
return redis.set(key, token, nx=True, px=ttl * 1000)
SET ... NX PX 是 Redis 2.6.12 之后支持的单条原子命令,把设置值和过期时间一次做完,中间没有空窗。
坑二:误删别人的锁
这是真正把我坑惨的一个。加锁时建议给锁存一个唯一的 token(比如 UUID),删除锁前要确认这把锁还是自己的。
错误的删除方式:
def release_lock(key):
redis.delete(key) # 危险:可能删掉别人的锁
问题出在超时场景:线程 A 拿到锁,执行业务花了 15 秒,但锁的 TTL 是 10 秒——锁已经自动释放,线程 B 拿到了锁。这时线程 A 执行完,一句 delete(key) 把 B 的锁删了,于是并发又回来了。
正确做法是"检查 token 再删除",而且检查和删除也要保证原子性(用 Lua 脚本):
-- release.lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
RELEASE_SCRIPT = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
def release_lock(key, token):
redis.eval(RELEASE_SCRIPT, 1, key, token)
先比对 token 是不是自己的,是才删。Lua 脚本在 Redis 里是单线程执行的,天然原子。
坑三:业务执行时间超过锁的 TTL
坑二提到的"锁自动释放"根源就是这里。业务耗时不确定,TTL 设短了锁提前失效,设长了又怕宕机后长时间占锁。
我的处理办法是锁续期(watchdog):加锁成功后启动一个后台线程,每隔 TTL/3 的时间检查一次,如果业务还没结束就自动延长锁的过期时间。
import threading
def renew_lock(key, token, ttl=10, stop_event=None):
def _renew():
while not stop_event.is_set():
time.sleep(ttl / 3)
if stop_event.is_set():
break
# 只有锁还是自己的才续期
redis.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] then "
" return redis.call('expire', KEYS[1], ARGV[2]) "
"else return 0 end",
1, key, token, ttl
)
th = threading.Thread(target=_renew, daemon=True)
th.start()
return th
业务结束后停掉续期线程,再走正常的释放流程。这套机制思路来自 Redisson 的看门狗,自己实现一遍后对它的设计理解深了很多。
补充:为什么不用官方的 Redisson
如果项目本身是 Java 的,直接上 Redisson 就好,它把上面这些问题都封装好了,还实现了可重入锁、公平锁等。我这边是 Python,社区里 redis-py 配 Lua 脚本的方案已经够用,就没额外引组件。能用成熟轮子就别自己造,这是踩完这三个坑之后最深的体会。
小结
分布式锁看着简单,细节全在边界条件上:原子性、锁归属、超时续期。三条原则:
- 加锁用
SET NX PX一条命令,避免竞态 - 释放锁必须校验 token 且用 Lua 保证原子,防止误删
- 业务可能超时就用续期机制,别指望固定 TTL 能覆盖所有场景
把这三条做扎实,分布式锁基本就不会出大问题了。
- 点赞
- 收藏
- 关注作者
评论(0)