bigywater.tech
Django · 35

get_or_create() 的并发问题

get_or_create 常被当作「并发安全的幂等创建」。它真的安全吗?这篇在 PostgreSQL 上让 8 个线程同时抢同一个名字,分别在有和没有唯一约束的表上重复 20 轮。

有 vs 没有
有唯一约束:20 轮每轮 1 行;没有:20/20 轮出现重复行
MultipleObjectsReturned
重复行一旦产生,之后的 get_or_create 永久失败
前提
get_or_create / update_or_create 的并发安全依赖数据库层面的唯一约束

它做了什么

实测 · 本机执行输出
第一次(不存在,创建) created = True
   SELECT "lab_utag"."id", "lab_utag"."name", "lab_utag"."extra" FROM "lab_utag" WHERE "lab_utag"."name" = 'a' LIMIT 21
   BEGIN
   INSERT INTO "lab_utag" ("name", "extra") VALUES ('a', 'e') RETURNING "lab_utag"."id"
   COMMIT
第二次(已存在) created = False
   SELECT "lab_utag"."id", "lab_utag"."name", "lab_utag"."extra" FROM "lab_utag" WHERE "lab_utag"."name" = 'a' LIMIT 21

典型的「先查后写」:先 SELECT,没有再 INSERT。两个线程都没查到,就会都去 INSERT——能不能拦住,只靠数据库的唯一约束。

并发实验

实测 · 本机执行输出
name 有 unique=True :每轮(返回 created=True 的线程数, 表里该名字的行数) = [(1, 1)]  抛出的异常: 无
name 没有唯一约束    :每轮(返回 created=True 的线程数, 表里该名字的行数) = [(2, 2), (3, 3), (4, 4), (5, 5), (6, 6)]  抛出的异常: ['MultipleObjectsReturned']
没有唯一约束时表里出现重复行的轮数: 20 / 20

用 Barrier 让 8 个线程同时起跑。有唯一约束时,后到的 INSERT 触发 IntegrityError,Django 内部捕获并重新查一次,所以调用方看到的仍是「一个创建、其余获取」。没有约束时,Django 无能为力。

重复行的后果

实测 · 本机执行输出
get_or_create(name="dup") → MultipleObjectsReturned: get() returned more than one PTag -- it returned 2!

一次并发竞态造成的脏数据,会让以后所有同名的 get_or_create 抛 MultipleObjectsReturned。要修,得先清理重复行,再加唯一约束。

update_or_create

实测 · 本机执行输出
第一次: True 1
第二次: False 2
get_or_create 已存在时 defaults 不生效: False 2
8 线程同时 update_or_create 同一个键:行数 = 1  异常: 无

规则相同。另外注意 get_or_create 在对象已存在时 defaults 不生效——要「存在就更新」用 update_or_create。

分步演示:竞态与唯一约束

get_or_create 并发
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。PostgreSQL 16.15(本机临时实例,端口 9402),READ COMMITTED,8 线程 × 20 轮
    • 没有验证:MySQL / SQLite 下的行为、REPEATABLE READ 或 SERIALIZABLE、多列联合唯一约束、在外层事务(atomic)里使用 get_or_create 时的保存点行为(第 44 篇)、get_or_create 内部的 SELECT 之后另一个事务回滚的时序
    京ICP备2026067502号-1京公网安备11010502063487号