select_for_update() 仅在事务中生效,需用 transaction.atomic() 包裹;锁粒度由 queryset 决定,mysql 无索引时可能升级表锁,postgresql 更严格;加 nowait=true 可快速失败;乐观锁必须将 version 检查下推至 sql where 子句并校验 update 返回值。

select_for_update 什么时候该用、怎么加才生效
它不是万能的“锁表”开关,只在数据库事务里起作用,且依赖底层数据库支持行级锁(PostgreSQL、MySQL InnoDB 可以,SQLite 不行)。没开事务就调用 select_for_update(),Django 会静默忽略,不报错也不锁——这是最常踩的坑。
实操建议:
- 必须包裹在
transaction.atomic()块里,否则锁无效 - 锁的粒度由 QuerySet 决定:
User.objects.filter(id=123).select_for_update()只锁这一行;User.objects.select_for_update()会锁全表(慎用) - MySQL 默认是
SELECT ... FOR UPDATE,但若查询没走索引,可能升级为表锁;PostgreSQL 更严格,只锁匹配行 - 加
nowait=True(如.select_for_update(nowait=True))可避免阻塞,遇到锁直接抛DatabaseError,适合做快速失败处理
乐观锁用 version 字段防覆盖,为什么不能只靠 Python 层判断
靠 Python 先查再改再比对 version,中间存在竞态窗口:两个请求几乎同时读到 version=5,各自算出新数据后都写回 version=6,后写入者会无声覆盖前者——这不是 Django 的问题,是所有应用层锁的通病。
实操建议:
- 必须把 version 检查下推到 SQL 的 WHERE 子句里,用原子更新:
User.objects.filter(id=123, version=5).update(name='new', version=6) - 检查
.update()返回值:返回 0 表示没更新成功(version 已变),此时应重试或提示冲突 - 别用
save()+ 自增 version:它先 SELECT 再 UPDATE,无法保证原子性;除非你手动写update(..., version=F('version') + 1)并校验影响行数 - 字段类型推荐
PositiveIntegerField或DateTimeField(后者更易调试,但要注意时钟精度和时区)
悲观锁 vs 乐观锁:选哪个不看理论,看场景特征
锁不是越“重”越好。高冲突、短事务、强一致性要求(比如库存扣减、订单支付)适合 select_for_update;低冲突、长操作、用户交互等待明显(比如编辑一篇博客草稿)更适合乐观锁。
实操建议:
- 如果业务逻辑涉及多次 DB 查询+计算+写入,且中间不能容忍状态变化,悲观锁更稳妥
- 如果写操作前要等用户输入(比如表单提交耗时几秒),用悲观锁会导致数据库连接长时间占用,容易拖垮连接池;这时乐观锁 + 冲突重试体验更好
- 注意 MySQL 的隔离级别:默认
REPEATABLE READ下,select_for_update锁的是“当前读”的快照,不是最新行;必要时加select_related()或显式select_for_update(of=('table_name',))控制锁定范围
ORM 层实现版本号更新,绕不开 raw SQL 的几个点
Django ORM 的 update() 虽然支持 F() 和条件过滤,但没法表达“仅当 version 匹配时才递增并更新其他字段”这种复合语义——因为 F('version') + 1 是服务端计算,而 version 匹配是 WHERE 条件,两者需严格同步。
实操建议:
- 最稳的方式是用
Model.objects.filter(id=x, version=old_v).update(version=old_v + 1, field1=..., field2=...),靠返回值判断是否成功 - 如果需要更复杂的逻辑(比如 version 递增但字段更新依赖函数),得用
connection.cursor().execute()执行原生 SQL,并确保在同一个 transaction 中 - 别在 signal 或
save()方法里自动 bump version:它不参与 update() 的原子条件,极易导致 version 和实际数据不同步 - 测试时务必模拟并发:用
threading.Thread或concurrent.futures启多个请求,验证冲突路径是否真被拦截
真正难的从来不是写对一行 select_for_update(),而是想清楚哪条数据在什么时刻会被谁修改、修改间隔多长、失败后用户能接受什么反馈。锁只是工具,场景理解不到位,加再多层也挡不住脏写。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











