get_or_create 常被当作「并发安全的幂等创建」。它真的安全吗?这篇在 PostgreSQL 上让 8 个线程同时抢同一个名字,分别在有和没有唯一约束的表上重复 20 轮。
第一次(不存在,创建) 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。要修,得先清理重复行,再加唯一约束。
第一次: True 1 第二次: False 2 get_or_create 已存在时 defaults 不生效: False 2 8 线程同时 update_or_create 同一个键:行数 = 1 异常: 无
规则相同。另外注意 get_or_create 在对象已存在时 defaults 不生效——要「存在就更新」用 update_or_create。