直接用cache.set不能当分布式锁用,因其缺乏原子性:先查key是否存在再set,中间存在并发窗口;而Redis的SET key value EX seconds NX指令通过NX保证仅key不存在时设置、EX防死锁,才是原子加锁操作。

为什么直接用 cache.set(key, value, timeout) 不能当分布式锁用
因为 cache.set 没有原子性:先检查 key 是否存在,再 set,中间可能被其他进程插入。秒杀场景下,两个请求几乎同时读到库存为 1,都判断“可扣减”,结果扣成 -1。Redis 的 SET key value EX seconds NX 才是真正原子的加锁指令——NX 保证只在 key 不存在时设置,EX 防止死锁。
用 redis-py 实现带自动续期的锁(不是 django-redis 自带的 lock)
django-redis 提供的 lock 类底层调用的是 SET ... NX EX,但默认不支持自动续期(lock 续约需手动调 extend),而秒杀请求处理时间不确定,超时释放锁会导致重复扣减。必须自己封装带心跳续约的逻辑:
- 加锁时用
redis.set(name, token, ex=10, nx=True),token是唯一随机字符串(防误删,比如 UUID4) - 启动一个后台线程或 asyncio task,每 3 秒执行一次
redis.eval(lua_script, 1, name, token),Lua 脚本校验 token 再EXPIRE - 解锁必须用 Lua:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end,避免 A 加锁、B 过期、C 加锁、A 解锁误删 C 的锁
在 Django 视图里怎么安全地扣减库存
不能把“查库存 → 判断 → 扣减 → 写库”放在 Python 层做,这四步之间都有并发窗口。正确做法是把库存扣减逻辑下沉到 Redis 或数据库原子操作:
- 初始化库存用
redis.set("seckill:stock:123", "100"),而不是存 Django model 字段 - 抢购时先
redis.decr("seckill:stock:123"),再判断返回值是否 ≥ 0;如果是负数,立刻redis.incr("seckill:stock:123")回滚(注意 incr 是原子的) - 只有 decr 返回 ≥ 0,才异步写 DB 记录订单——DB 写失败不影响锁和库存一致性,后续靠补偿任务修复
- 别在事务里嵌套 Redis 锁:Django
transaction.atomic()和 Redis 锁粒度不同,混用反而增加死锁风险
锁超时时间设成多少才既安全又不卡住
设太短(如 2 秒),网络抖动或 GC 暂停就导致锁提前释放;设太长(如 300 秒),出错后锁长期占着,业务不可用。经验值是:取「正常请求耗时的 3–5 倍」,再加缓冲:
立即学习“Python免费学习笔记(深入)”;
- 压测得出平均处理时间是 800ms,那锁超时至少设为
5秒(不是 1 秒) - 如果用了 Celery 异步下单,锁超时得覆盖“Redis 扣减 + 发送任务 + Worker 执行”全链路,建议统一设为
10秒 - 永远不要依赖锁超时来兜底——要配合看门狗监控:定期扫
redis.keys("seckill:lock:*"),发现超时未释放且对应订单未生成,主动告警人工介入
Redis 分布式锁真正的难点不在加锁,而在锁生命周期与业务状态的对齐。比如用户下单中途断网,锁释放了但订单没创建,这个空档期没法靠锁本身解决,得靠最终一致性和幂等设计兜住。


















