直接用save()在高并发下出错,因其执行“读-改-写”非原子操作:多个请求同时读取同一值(如counter=5),各自加1后均写回6,导致结果丢失;f()通过将计算下推至数据库生成update counter = counter + 1语句,由数据库原子执行,彻底规避竞态。

为什么直接用 save() 在高并发下会出错?
多个请求同时读取同一行数据、各自加1再写回,最终结果只加了1而不是N次——这是典型的“读-改-写”竞态。Django默认的 save() 就是这种模式,底层先 SELECT,Python里算完再 UPDATE,中间没锁也没原子性。
比如:obj.counter += 1; obj.save(),两个并发请求都读到 counter=5,各自算出6,都写回去,结果还是6。
F() 是怎么绕过竞态的?
F() 把计算逻辑下推到数据库层,生成类似 UPDATE table SET counter = counter + 1 的SQL,整个操作由数据库引擎原子执行,不依赖Python层读取值。
实操建议:
- 用
from django.db.models import F - 更新必须走
QuerySet.update(),不能用实例的save()—— 后者仍会触发读取旧值 - 单字段递增:
MyModel.objects.filter(id=123).update(counter=F('counter') + 1) - 多字段联动:
.update(status=F('status'), updated_at=timezone.now())(注意:F()只能引用字段,不能混用Python变量或函数)
哪些场景不能只靠 F()?
F() 解决的是“纯字段运算”的原子性,但挡不住更复杂的业务逻辑冲突。比如“余额不能为负”这种校验,数据库没法在UPDATE时动态查另一张表或跑复杂规则。
常见坑点:
- 事务没包住:如果需要结合其他操作(如记录日志、调用外部API),
F()更新后仍得用transaction.atomic包裹整段逻辑 - 条件更新失败不报错:
.update()返回影响行数,要自己检查是否为0(比如ID不存在或WHERE不匹配) - 无法获取新值:UPDATE后拿不到更新后的
counter,得再查一次或用returning(PostgreSQL支持,但Django ORM不原生暴露)
真高并发时还得加什么?
F() 是必要但不充分的——它防住了单条UPDATE的竞态,但挡不住跨行/跨表逻辑冲突。比如“库存扣减+订单创建”,即使库存用 F() 更新成功,订单插入失败会导致数据不一致。
这时得组合使用:
- 数据库级别:PostgreSQL用
SELECT ... FOR UPDATE(Django的select_for_update()),MySQL用SELECT ... LOCK IN SHARE MODE或事务隔离级别调高 - 应用层兜底:幂等key、状态机校验、异步补偿任务
- 别迷信
F()能替代锁:它只保字段级原子性,不是万能锁
真正棘手的从来不是“怎么加1”,而是“加1之后要不要发消息、记流水、通知下游”——这些链条越长,越容易在某个环节断掉。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











