MongoDB事务中findAndModify无法保证流水号唯一性,真正可靠的是在serialNo字段建唯一索引+应用层生成候选号+捕获11000错误重试,且serialNo必须为字符串类型。

事务里直接用 findAndModify 无法保证唯一性
很多人以为在 MongoDB 事务中执行 findAndModify 更新一个计数器文档,再拼接前缀生成流水号,就能靠事务隔离避免重复。实际不行——findAndModify 在事务内仍是「语句级原子操作」,但事务本身不阻止其他并发事务同时读到同一个旧值并各自加 1。尤其当多个事务几乎同时启动、都读到序列号 00001,各自算出 BTA2026041000002,最后插入时才因唯一索引报错,这不是防重,是撞重后兜底。
真正起作用的是 createIndex + 唯一约束 + 重试逻辑
MongoDB 不提供「事务锁表」这种粗粒度控制,它依赖唯一索引强制排他写入。正确路径是:
- 提前在流水号字段(比如
serialNo)上建唯一索引:db.orders.createIndex({ serialNo: 1 }, { unique: true }) - 应用层生成候选流水号(如
BTA2026041000001),直接insertOne;不查不改不预占 - 捕获
11000错误(duplicate key),说明已被占用,立即重试生成下一个号(00002) - 重试次数建议设为 3–5 次,超过则抛异常,避免死循环
这个模式把冲突检测下推到存储层,比应用层锁或事务更轻量、更可靠。
为什么不用事务包裹 update + insert?
常见错误写法:事务里先 update 计数器文档,再用新值拼流水号 insert。问题在于:
- 计数器更新成功 ≠ 流水号插入成功;中间可能失败,导致计数器已进位但业务数据没落库,出现空号
- 如果两个事务同时更新计数器,MongoDB 会按写入顺序串行化,但它们各自生成的流水号仍可能因时间戳/随机因子相同而碰撞(尤其高并发短时密集场景)
- 事务生命周期越长,锁持有时间越久,吞吐直线下跌;而唯一索引冲突是瞬时判断,无锁等待
唯一索引建在哪?字段类型有讲究
流水号必须存为字符串(String),不能转成数字。原因:
- 前缀含字母(如
BTA),数字类型会截断或报错 - 补零需求(
00001)在数字类型里天然丢失,查出来变成1,排序和范围查询全乱 - MongoDB 字符串索引支持前缀匹配,
{ serialNo: 1 }可高效支撑serialNo: { $regex: "^BTA20260410" }类查询
另外,别在复合字段(如 { prefix: "BTA", date: "20260410", seq: 1 })上建唯一索引——这等于放弃流水号整体唯一性,只保局部不重,违背全局唯一前提。
真正难处理的不是生成逻辑,而是重试时序:当一天单据量逼近序列号上限(如 99999),必须在应用层做「日期切换」检测,否则跨日瞬间大量重试会打爆数据库。这个边界检查没法交给 MongoDB 自动完成。


















