必须在数据库事务中使用select_for_update()来避免“读-改-写”竞态,否则锁无效;需确保查询走索引以防升级为表锁,并合理配置nowait、skip_locked、of等参数。

什么时候必须用 select_for_update() 而不是普通查询
当多个并发请求可能同时读取同一行、再基于该行数据做修改(比如扣库存、生成唯一订单号、递增计数器),且你要求「读到的值在后续更新时仍有效」,就必须上悲观锁。普通 get() 或 filter() 不加锁,读完就释放,中间可能被别人改掉 —— 这就是典型的「读-改-写」竞态。
常见错误现象:IntegrityError: duplicate key value violates unique constraint(比如抢购时生成重复订单号),或库存扣成负数。
- 必须在数据库事务中调用,否则 Django 会静默忽略锁(不报错但没效果)
- 只对支持行级锁的数据库生效(PostgreSQL、MySQL InnoDB),SQLite 不支持
- 锁的是查询结果集里的行,不是整个表;但如果
filter()条件没走索引,可能升级为表锁(MySQL 尤其危险)
select_for_update() 的参数怎么选:nowait、skip_locked、of
默认行为是阻塞等待锁释放,但线上服务通常不能卡住请求。关键参数要按场景配:
-
nowait=True:拿不到锁立刻抛DatabaseError(PostgreSQL)或OperationalError(MySQL),适合快速失败重试 -
skip_locked=True:跳过已被锁的行,只锁能拿到的行(MySQL 8.0+/PostgreSQL 9.5+),适合批量处理队列任务 -
of=['field_name']:只锁指定字段所在的行(需配合select_related()或明确主表),避免锁扩散到关联表
示例:Order.objects.select_for_update(nowait=True).get(id=123),比无参数更可控。
事务边界没包住,锁就白加了
Django 的 select_for_update() 只在当前数据库事务提交或回滚时才释放锁。如果忘了用 @transaction.atomic 或手动 transaction.atomic().__enter__(),锁会在查询后立即释放 —— 看似加了锁,实则形同虚设。
- 常见错误:在视图里调用
select_for_update(),但没包在atomic块里,后续.save()时锁早没了 - 正确写法必须是:
@transaction.atomic def place_order(request): order = Order.objects.select_for_update().get(id=123) order.status = 'paid' order.save() # 此时 still under lock - 注意:
atomic嵌套时,只有最外层控制事务生命周期,内层不会新开事务
锁粒度失控:为什么查着查着把整张表锁死了
锁不是凭空来的,它依赖数据库执行计划。如果 select_for_update() 的查询条件没命中索引,MySQL 可能退化为锁全表(尤其 WHERE 字段没索引 + 使用 LIKE '%xxx'),PostgreSQL 则可能锁大量无关行。
- 务必检查
EXPLAIN输出:确认type是ref/range,不是ALL - 避免在锁查询里用
order_by()非索引字段,或distinct()、annotate()等触发临时表操作 - 关联查询慎用:
select_for_update()默认锁所有 JOIN 表的匹配行,加of=['myapp_order.id']可限定只锁主表
锁住不该锁的行,轻则拖慢其他请求,重则引发死锁 —— 而 Django 不会帮你检测或自动重试。











