直接用 save() 在高并发下会丢数据,因其执行“读-改-写”三步操作,导致竞态条件;正确做法是用 queryset.update() 配合 f() 表达式实现原子更新。
为什么直接用 save() 在高并发下会丢数据?
mysql 默认隔离级别是 repeatable read,但 django 的 save() 是“读-改-写”三步操作:先查出当前值,python 里算完新值,再 update。两个请求同时读到旧值,各自加1后写回,结果只+1而不是+2——这是典型的竞态条件(race condition)。f 表达式把计算逻辑下推到数据库层,避免中间状态暴露给 python。
F() 怎么写才真正原子?
关键不是用不用 F(),而是它是否被包裹在原子事务里,且不混用普通字段赋值。常见错误是:obj.field = F('field') + 1 然后调 save() ——这仍会触发 SELECT,因为 Django 不知道你改的是哪个字段。正确做法是用 update() 直接发 SQL:
from django.db.models import F
MyModel.objects.filter(id=123).update(counter=F('counter') + 1)
- 必须用
QuerySet.update(),不能用实例的save() -
F()只能用于update()或filter()中的查询条件,不能出现在普通字段赋值里 - 如果需要获取更新后的值(比如返回新计数),得额外查一次或用
SELECT ... FOR UPDATE
MySQL 的 FOR UPDATE 和 F() 能一起用吗?
能,但没必要。F 表达式本身已由 MySQL 在行级锁下执行,UPDATE ... SET counter = counter + 1 天然带锁。加 select_for_update() 反而可能扩大锁范围、降低吞吐。只有当你需要先读再基于多个字段做复杂计算(比如“余额减去金额,但不能为负”),才用:
with transaction.atomic():
obj = MyModel.objects.select_for_update().get(id=123)
if obj.balance >= amount:
obj.balance -= amount
obj.save() # 这里 save 才安全,因为已加锁
-
select_for_update()锁的是 SELECT 到 COMMIT 之间的行,不是单条语句 - 在
transaction.atomic()外调用select_for_update()会报错 - MySQL 中,
FOR UPDATE对未命中索引的查询会升级为表锁,务必确保 WHERE 条件走索引
哪些场景不能只靠 F()?
涉及跨字段约束、条件分支、或需调用数据库函数时,F() 力不从心。例如:“若 status 为 'pending',则将 counter 加 1,否则不加”。Django 不支持 SQL 的 CASE WHEN 直接映射到 update(),得手写原生 SQL 或拆成两步:
# 方案一:原生 SQL(最直接)
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(
"UPDATE myapp_mymodel SET counter = counter + 1 "
"WHERE id = %s AND status = 'pending'",
[obj_id]
)
<h1>方案二:先 filter 再 update(更 Django 风格)</h1><p>MyModel.objects.filter(id=obj_id, status='pending').update(counter=F('counter') + 1)</p>
注意第二步其实等价于第一种——Django 生成的 SQL 就是带 WHERE 的 UPDATE。真正容易忽略的是:MySQL 的 UPDATE 语句在匹配不到行时不会报错,也不会影响 affected_rows,所以业务上要检查 update() 返回值是否为 0,判断条件是否满足。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











