用 update_one + upsert=true 替代 insert_one 可避免重复写入,需指定唯一查询字段、使用 $set 更新、加唯一索引兜底,并注意 find_one_and_update 的原子性及哈希去重等细节。

用 update_one + upsert=True 替代 insert_one
直接调用 insert_one 是重复写入的根源——它不检查是否存在,只管插入。MongoDB 本身不强制唯一约束(除非你建了唯一索引),所以靠代码逻辑防重更可靠。update_one 配合 upsert=True 是最常用且轻量的解法:按条件查,有就更新,没就插入。
常见错误是漏写 upsert=True,结果变成纯更新,查不到就什么都不做;或者查询条件太宽(比如只用 {"name": "xxx"}),导致不同业务数据被误覆盖。
- 必须指定明确的唯一查询字段,比如
{"email": "a@b.com"}或{"order_id": "ORD-123"} - 更新部分用
{"$set": {...}},避免意外覆盖其他字段 - 如果业务要求“首次写入才生效”,
upsert=True后需检查返回的result.upserted_id是否为None来判断是否新建
给关键字段加唯一索引,让数据库兜底
应用层逻辑可能出错,但 MongoDB 的唯一索引是原子级保障。哪怕并发写入,第二个请求会直接抛出 duplicate key error,而不是静默覆盖或重复。
不建索引时,靠代码判断再写入存在竞态条件(check-then-act 问题):两个请求同时查到“不存在”,然后都执行插入——这是重复写入最隐蔽的来源。
- 用
collection.create_index({"email"}, unique=True)建索引,注意提前处理已有重复数据,否则建索引失败 - 捕获异常时,要区分
duplicate key error(error code 11000)和其他错误,别把网络超时也当成“已存在” - 复合唯一索引也支持,比如
{"user_id": 1, "date": 1},适合“每人每天只能提交一次”的场景
用 find_one_and_update 做原子性读写
当需要“先读原值、再基于它计算新值、最后写入”时(比如计数器+1、状态流转),update_one 不够用。find_one_and_update 在服务端原子执行,彻底规避并发冲突。
容易踩的坑是默认返回旧文档(return_document=False),如果你依赖返回值做后续判断,得显式传 return_document=ReturnDocument.AFTER。
- 必须设
upsert=True才能兼容首次写入,否则查不到就返回None - 更新操作要用
{"$inc": {"count": 1}}这类原子操作符,别用{"$set": {"count": old_count + 1}} - 配合
projection参数只取必要字段,减少网络和内存开销
避免用时间戳或随机 ID 当去重依据
有人用 {"created_at": datetime.now()} 或 {"uuid": str(uuid4())} 当查询条件来“假装唯一”,这完全无效——每次都是新值,根本查不到旧记录,等价于每次都 insert_one。
真正能去重的字段必须业务语义唯一:订单号、手机号、身份证号、第三方回调里的 external_id 等。临时生成的字段只适合做辅助标识,不能当主键用。
- 如果上游没提供稳定 ID,考虑用内容哈希(如
hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest())生成摘要,但要注意哈希碰撞概率和性能开销 - 不要在应用层维护“已写入 ID 缓存”,分布式环境下缓存不同步会让它失效
"user.email" 却实际存的是 {"user": {"Email": ...}}。上线前务必用真实数据跑一遍重复写入测试。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











