「读出来加一再存回去」是最经典的并发 bug。这篇在 PostgreSQL 上用 10 个线程做真正的并发计数,量出丢了多少,再对比几种正确写法的结果和耗时。
get → n += 1 → save() 最终 n = 347 / 期望 2000 丢失 1653 745 ms
同上,外面包 atomic() 最终 n = 250 / 期望 2000 丢失 1750 809 ms
filter().update(n=F("n") + 1) 最终 n = 2000 / 期望 2000 丢失 0 414 ms
atomic + select_for_update().get → +1 → save 最终 n = 2000 / 期望 2000 丢失 0 810 ms
get → c.n = F("n") + 1 → save() 最终 n = 2000 / 期望 2000 丢失 0 850 ms丢失更新的原因:两个线程各自读到同一个旧值 n,各自加一再写回,后写的覆盖先写的。atomic() 只保证「一组语句要么都提交要么都回滚」,PostgreSQL 的默认隔离级别 read committed 下并不会让另一个事务等你。F() 把计算放到数据库里做,select_for_update() 先把行锁住再读。
save() 之后 c.n 的类型: int 库里的值: 6 refresh_from_db() 之后 c.n = 6
这一段的结果只在本环境(PostgreSQL、Django 6.1.1)观察到:save() 之后内存里的值已经是整数。别的后端不一定同样,稳妥的做法还是 refresh_from_db()。
单条 update(F) 最快(414 ms),因为只需要一次往返;select_for_update 与 save() + F() 都要至少两次往返(810 / 850 ms)。这些数字来自 fsync=off 的本机临时 PostgreSQL,线程数与次数都很小,只用来比较相对关系。