pytest-xdist并发测试因共享数据库唯一索引导致duplicate entry错误,根本原因是多进程同时插入相同键值;可靠解法是让每个worker使用独立数据(如带进程id的邮箱)、内存数据库隔离或原子upsert语句。

因为 pytest-xdist 启动多个独立进程并发执行测试,而这些进程默认共享同一套数据库连接配置、同一张表、同一个临时文件路径——但没做任何隔离,导致 INSERT 或 UPDATE 操作在高并发下撞上唯一索引。
pytest-xdist 并发写入触发 1062 Duplicate entry 错误
MySQL 报错 Duplicate entry 'xxx' for key 'uk_user_email' 或 PostgreSQL 报错 duplicate key value violates unique constraint,不是数据库坏了,是多个 worker 同时尝试插入相同唯一键值(比如测试用的固定邮箱 test@example.com)。
- 每个 worker 进程都运行相同的测试代码,比如
user = User(email="test@example.com")+session.add(user) - 它们不共享内存,但共用一个数据库实例和唯一索引,没有协调机制
- 哪怕只跑两个 worker,只要两个 test function 都试图插入同一 email,就必然冲突
- 错误通常在
session.commit()或engine.execute()阶段抛出,而不是 insert 语句执行时
别信“先查后插”,它在 xdist 下必崩
SELECT COUNT(*) FROM users WHERE email = 'test@example.com' 返回 0,紧接着 INSERT INTO users... —— 这个逻辑在单进程里看似安全,在 xdist 多进程下就是裸奔。两个 worker 几乎同时查到“不存在”,然后一起插,第二个必然失败。
- 加
SELECT ... FOR UPDATE也没用:若 WHERE 条件没走索引,InnoDB 可能锁整张表 - Redis 分布式锁只能缓解,不能根治:锁粒度难控、网络延迟、锁过期等问题依然存在
- 真正可靠的方案是让每条 SQL 自身具备原子性,而不是靠应用层串行化
真正有效的隔离方案只有三种
不是“避免冲突”,而是让冲突不发生或不报错——关键在于让每个 worker 拥有独立可区分的数据上下文。
- 用随机/进程级数据:生成邮箱时加后缀,如
f"test_{os.getpid()}@example.com"或用uuid.uuid4().hex[:8] - 用内存数据库:SQLite 的
sqlite:///file::memory:对每个 worker 完全隔离,无需改业务逻辑 - 用原子 upsert:MySQL 用
ON DUPLICATE KEY UPDATE,PostgreSQL 用ON CONFLICT (email) DO UPDATE SET ...,把“判断+写入”压进一条语句
fixture 生命周期没关连接,会放大冲突概率
连接池泄漏本身不直接导致唯一键冲突,但它会让多个 worker 复用旧连接、事务状态残留、甚至跨测试污染数据——间接加剧冲突发生的条件。
- 每个
@pytest.fixture(scope="function")必须配try/finally调用engine.dispose()或session.close() - 异步 fixture 必须
await pool.close(),不能只靠 event loop 结束自动清理 - 别依赖
yield后自动回收:如果 fixture 抛异常,yield后的 cleanup 代码根本不会执行
最常被忽略的点是:唯一索引只是检测器,不是保险丝。你得让每条 INSERT 的输入值天然不同,或者让每条 SQL 具备“存在即更新”的原子语义——靠 try/except 捕获 1062 再重试,只会让问题更隐蔽、更难复现。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











