直接obj.likes += 1在并发下丢数据,是因为该操作分“读-改-写”三步,未原子化;应使用F('likes')+1配合update()在数据库层原子执行。

为什么直接用 save() 在并发场景下会出错
多个请求同时读取同一记录、各自计算新值、再写回,会导致后写入的覆盖前写入的修改。比如两个请求都读到 views=100,各自加 1 后都设为 101,最终结果仍是 101 而非预期的 102。
根本原因是读-改-写(read-modify-write)不是原子操作。Django 的 save() 默认走这条路径,无法规避竞态。
- 典型错误现象:
views字段增长速度明显低于实际请求数 - 调试时可能发现数据库日志里大量重复的旧值读取
- 即使加了
select_for_update(),若逻辑复杂或事务范围控制不当,仍可能因锁等待或死锁引入性能问题
用 F() 实现原子递增/递减更新
F() 把计算逻辑推到数据库层,避免 Python 层读取中间值。数据库在单条 UPDATE 语句内完成字段自运算,天然原子。
示例:对文章浏览数安全递增
立即学习“Python免费学习笔记(深入)”;
提供使用 django-unfold (v0.56+) 构建高级 Django 管理界面的专业指导。当用户提及 django-unfold、unfold admin 等内容时使用此技能。
from django.db.models import F
from myapp.models import Article
<p>Article.objects.filter(id=123).update(views=F('views') + 1)
- 必须用
update()方法,不能用save()—— 后者会触发实例重载和完整字段校验,失去原子性 -
F()支持加减乘除、取负、甚至嵌套表达式(如F('a') + F('b')),但不支持跨表字段引用(除非用annotate()配合) - 若需获取更新后的值(如返回新
views数),得额外查一次,update()本身只返回影响行数
结合 select_for_update() 处理更复杂的并发逻辑
当更新逻辑不能仅靠 F() 表达(例如“余额大于 100 才扣减 50”),就必须加行锁,确保检查与更新在同一事务中完成。
关键点是:锁必须在 UPDATE 前获取,且整个操作包裹在事务中。
from django.db import transaction
from django.db.models import F, Q
from myapp.models import Account
<p>with transaction.atomic():
account = Account.objects.select_for_update().get(id=456)
if account.balance >= 100:
account.balance = F('balance') - 50
account.save(update_fields=['balance']) # 注意这里仍用 F,但 save 会生成 UPDATE 语句
-
select_for_update()必须在transaction.atomic()内使用,否则锁在查询后立即释放 - 上面例子中,
account.save(update_fields=['balance'])实际生成的是UPDATE ... SET balance = balance - 50,不是先读再写 - 如果条件判断涉及多个字段或外部状态,要小心锁粒度——
select_for_update()锁的是当前查询命中的行,别漏掉关联数据
注意 F() 在 filter() 和 update() 中的行为差异
F() 在不同上下文里含义不同:在 filter() 中用于比较,在 update() 中用于赋值。混用容易出逻辑 bug。
常见误写:
# ❌ 错误:filter 中用 F 做更新逻辑,实际只是筛选
Article.objects.filter(views=F('views') + 1).update(title='new')
<h1>✅ 正确:filter 用 F 做条件,update 用 F 做计算</h1><p>Article.objects.filter(views__lt=F('max_views')).update(views=F('views') + 1)
-
filter(views=F('views') + 1)等价于WHERE views = views + 1,永远为假(除非字段是 NULL) - 在
filter()中用F()主要是为了字段间比较(如created_at__lt=F('updated_at'))或动态阈值 - PostgreSQL 对
F()表达式索引支持较好,MySQL 8.0+ 也支持函数索引,但别指望F('x')+1能走x的普通索引
实际项目里最易忽略的是:把 F() 当万能解药,却忘了它解决不了“读取原始值做业务判断再更新”的场景。这时候要么拆成带锁的事务,要么改用数据库原生的 CASE WHEN 或存储过程——取决于你愿不愿意把逻辑从 Django 迁出去。

















