select_for_update()在秒杀中易失效,因其仅事务内生效且依赖索引与查询方式;Redis锁需自动续期与业务key,避免手写SET导致丢锁;缓存与DB必须明确主辅并保证扣减原子性。

直接用 select_for_update() 或 Redis 分布式锁都能防超卖,但选错场景或写法错误,效果几乎为零。关键不在“用没用”,而在“锁得准不准、持有时长合不合理、失败后怎么兜底”。
为什么 select_for_update() 在秒杀里容易失效
它只在事务内生效,且依赖数据库索引和查询方式。常见失效点:
- 没包在
transaction.atomic()里——锁根本不会加 - 用了
Product.objects.get(id=1)再调select_for_update()——查完才锁,中间窗口期已超卖 - WHERE 条件字段没建索引(比如按
name查)——MySQL 可能升为表锁,吞吐暴跌 - 锁了整张表:
Product.objects.select_for_update()没带任何 filter —— 所有商品操作全阻塞 - 事务里混了耗时操作(如发短信、调外部 API)——锁持有时间从毫秒拉到秒级,QPS 断崖下跌
Redis 分布式锁必须带自动续期和业务 key
手写 SET key value NX PX 10000 极易丢锁:客户端崩溃、网络延迟、GC 停顿都可能导致锁提前释放。正确做法是用 redis-py 官方 Lock 类:
import redis
from django.conf import settings
<p>r = redis.Redis.from_url(settings.REDIS<em>URL)
lock = r.lock(f"seckill:product</em>{product_id}", timeout=5, blocking_timeout=2)
if not lock.acquire():
return HttpResponse("请求太忙,请稍后再试", status=429)
try:</p><h1>查缓存库存 → 判断 → 扣减 → 更新缓存 + DB</h1><pre class="brush:php;toolbar:false;">stock = r.get(f"stock:{product_id}")
if int(stock) <= 0:
raise ValueError("库存不足")
r.decr(f"stock:{product_id}")
# 同步写 DB(可异步化,但需保证最终一致)
with transaction.atomic():
product = Product.objects.select_for_update().get(id=product_id)
if product.stock > 0:
product.stock -= 1
product.save()finally: lock.release()
timeout 应设为「单次扣减逻辑最大耗时 × 2」;blocking_timeout 控制等待上限,避免雪崩式排队。
立即学习“Python免费学习笔记(深入)”;
别让缓存和 DB 成为两个世界
秒杀中缓存库存(Redis)和 DB 库存不同步,是超卖高发区。必须明确谁主谁辅:
- 读库存优先走 Redis,但每次扣减必须原子:用
DECR或 Lua 脚本保证「查+减」不可分割 - DB 只作为持久化底座,不承担高并发读压力;但每次成功扣减后,必须同步更新 DB(哪怕异步),否则重启后数据丢失
- 严禁「先 DB 减再缓存减」或「先缓存减再 DB 减但不保证原子」——任一环节失败都会导致不一致
- 冷启动时,用 Lua 脚本一次性把 DB 库存刷进 Redis,避免多进程重复加载
真正难的不是加锁,而是锁住之后那几行代码的执行边界:是否所有读写都在锁内?失败路径是否释放资源?缓存与 DB 的更新顺序有没有隐含竞态?这些细节漏掉一个,高并发下超卖就藏在第 1001 个请求里。


















