F() 表达式被说成「并发安全的计数」。到底安全在哪?不用它会丢多少?这篇在真实的 PostgreSQL 上用 8 个线程同时加 1600 次,把丢失的更新数出来。
读出来再 +1 再 save():
UPDATE "lab_counter" SET "name" = 'c', "n" = 1 WHERE "lab_counter"."id" = 1
update(n=F("n") + 1):
UPDATE "lab_counter" SET "n" = ("lab_counter"."n" + 1) WHERE "lab_counter"."id" = 1
对象上 c.n = F("n") + 1; c.save():
UPDATE "lab_counter" SET "name" = 'c', "n" = ("lab_counter"."n" + 1) WHERE "lab_counter"."id" = 1 RETURNING "lab_counter"."n"
save 之后 c.n 的类型: int → refresh_from_db() 之后: 3F() 的本质是把「加一」写进 SQL 里由数据库执行,而不是先读出值再写回具体数值。数据库在执行 UPDATE … SET n = n + 1 时对这行加锁,并发的更新会排队,每次都基于最新值。
读-改-写(c.n + 1; save) 最终 n = 335(期望 1600),丢失 1265 次,用时 0.63 s,线程错误 0
update(n=F("n") + 1) 最终 n = 1600(期望 1600),丢失 0 次,用时 0.33 s,线程错误 0用 threading.Barrier 让 8 个线程同时起跑。读-改-写丢了绝大多数更新——每个线程读到旧值,各自写回,互相覆盖。
每轮丢失: [1260, 1263, 1273, 1263, 1260]
丢失数量在 5 轮里几乎一样,不是偶发。(丢失的具体数字和机器速度、线程数有关,别当作通用值。)
update() 返回受影响行数 = 1 更新不存在的行,返回 = 0 库里现在的值 = 5 n = n*2 + n → 15
update() 返回的是受影响行数,不是新值。想要新值:PostgreSQL 上对象 save() 用了 RETURNING(本次实测),其它数据库没有测;通用做法是 refresh_from_db()。F() 只解决「在数据库里做运算」,不解决「基于旧值做判断」(比如「库存够才扣」需要 select_for_update 或带条件的 update,见第 46 篇)。