select_for_update() 能防超卖是因为它在数据库层面加行级写锁,使并发事务对同一行的查询阻塞,但必须置于 transaction.atomic() 中且依赖索引与正确查询方式,否则仍会超卖。

为什么 select_for_update() 能防超卖,但不是加了就万事大吉
它本质是在数据库层面加行级写锁,后续对同一行的 SELECT ... FOR UPDATE 会阻塞,直到前一个事务提交或回滚。但这只在事务内生效,且依赖数据库支持(PostgreSQL/MySQL InnoDB),SQLite 直接抛 NotSupportedError。常见误用是没开事务、跨请求用、或锁了错的行(比如只按商品 ID 查但没加唯一索引,导致锁住多行或漏锁)。
必须套在 transaction.atomic() 里,且不能用 get_or_404 等快捷函数
select_for_update() 是 QuerySet 方法,必须作用于明确的查询结果,且整个操作需包裹在原子事务中。以下写法是错的:
product = get_object_or_404(Product, id=1) # 这步没事务,也没锁
with transaction.atomic():
product.stock -= 1 # 此时 stock 已可能被其他请求改过
product.save()
正确做法是:
from django.db import transaction
<p>try:
with transaction.atomic():</p><h1>必须用 filter + select_for_update,不能用 get()</h1><pre class="brush:python;toolbar:false;"> product = Product.objects.select_for_update().get(id=1)
if product.stock > 0:
product.stock -= 1
product.save()
else:
raise ValueError("库存不足")except ValueError as e:
处理失败
pass
-
select_for_update()必须紧跟在filter()或get()前,且get()内部会触发查询 - 不能用
get_or_404,因为它绕过了 QuerySet 链式调用,无法插入锁逻辑 - 若查询条件不命中任何行,
select_for_update()不会报错,但后续save()可能引发ObjectDoesNotExist
锁粒度和性能:别锁全表,也别锁太松
默认 select_for_update() 锁的是 WHERE 条件匹配的行。但如果 WHERE 用的是非索引字段(如 name="iPhone"),MySQL 可能升级为表锁;PostgreSQL 则可能锁住更多行。更糟的是,如果只查 Product.objects.select_for_update() 没带条件,就是锁整张表。
- 确保 WHERE 字段有数据库索引(尤其是主键或唯一键),否则锁效率极低
- 高并发场景下,避免锁住热门商品的单一行——可考虑分库分表、库存分片(如把 1000 库存拆成 10 个 slot,每次随机扣一个)
- 加
nowait=True可让冲突时立刻抛DatabaseError,而非等待,默认会等(MySQL 默认 50 秒超时)
超卖还是发生了?检查这三处
即使用了 select_for_update(),线上仍出现超卖,大概率卡在这几个地方:
- 视图或 API 没用
transaction.atomic()包裹,导致锁在查询后就释放,save()时已无锁保护 - 数据库隔离级别太低(如 MySQL 的 READ-COMMITTED),某些旧版本 Django 在该级别下
select_for_update()行为异常 - 前端重复提交未拦截,同一个请求被发了多次,而事务间互不可见,每个都查到“还有库存”
最隐蔽的坑是:Django 测试用的 SQLite 不支持行锁,本地跑通不代表生产安全。上线前务必在真实 DB(PostgreSQL/MySQL)上压测验证锁行为。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











