不能在事务里直接用findandmodify生成序列号,因为mongodb事务不支持跨文档读-改-写操作,而序列号生成需原子递增单文档;正确做法是事务外用findandmodify预分配,再于事务内使用该值。

为什么不能在事务里直接用 findAndModify 生成序列号
因为 MongoDB 事务中不支持跨文档的原子操作以外的“读-改-写”循环,而典型的序列号生成逻辑(如先查最大值、再+1、再插入)天然依赖两次独立操作。即使你在事务里手动 findOne + insertOne,中间仍可能被其他事务并发修改,导致重复或跳号。
真正可行的路径只有一条:把序列号状态收敛到单个文档里,靠 MongoDB 对单文档的原子更新能力保序。
用 findAndModify 在事务外预分配序列号文档
别试图在事务内生成序列号——把它提前准备好。建一个专用集合(比如 counters),每种业务序列(如订单号、工单号)占一条文档:
{ "_id": "order_seq", "seq": 12345 }
然后用 findAndModify 原子地递增并返回新值(注意必须在事务外调用):
db.counters.findAndModify({
query: { _id: "order_seq" },
update: { $inc: { seq: 1 } },
new: true,
upsert: true
})
这个操作是线程安全的,无论多少客户端并发调用,seq 都严格递增且不重复。
- 必须设
new: true才能拿到更新后的值 -
upsert: true确保首次调用自动初始化 - 不要在事务里调用它——事务内只允许
insertOne/updateOne等写操作,findAndModify不被允许
事务内如何安全使用预分配的序列号
拿到 findAndModify 返回的 seq 后,把它作为字段值传入事务操作。例如创建订单:
session.startTransaction();
try {
const seqDoc = db.counters.findAndModify({ /* 如上 */ });
const orderNo = `ORD-${seqDoc.seq.toString().padStart(6, '0')}`;
<p>db.orders.insertOne({
orderNo: orderNo,
items: [...],
status: "created"
}, { session });</p><p>db.auditLogs.insertOne({
action: "create_order",
orderNo: orderNo,
timestamp: new Date()
}, { session });</p><p>session.commitTransaction();
} catch (e) {
session.abortTransaction();
throw e;
}</p>
关键点:
- 序列号生成和事务执行是两个分离步骤,前者必须完成后再开启事务
- 事务内所有操作都基于已确定的
orderNo,不再做任何依赖状态的计算 - 如果事务提交失败,这个
seq已经消耗掉了——这是设计取舍:宁可跳号,也不重复
高并发下 findAndModify 的性能与替代方案
单文档 findAndModify 在万级 QPS 下会成为瓶颈,因为所有请求争抢同一个文档的写锁。实际项目中要注意:
- 不要为每个用户/租户单独建一条计数器文档(如
user_123_seq),这会导致集合膨胀和索引压力 - 可以按时间粒度分片,比如每天一个文档:
order_seq_20240520,用$dateToString动态拼接_id - 更激进的方案是预取一批号段(如一次
$inc加 100),缓存在应用内存里慢慢发,降低 DB 压力 - 如果业务允许“大致有序”,也可用
ObjectId自带的时间戳部分 + 随机后缀替代,彻底规避序列号问题
真正难的不是怎么写代码,而是想清楚:你的业务是否真的需要全局严格连续?跳号能否接受?审计要求是否允许号段式分配?这些判断比语法细节重要得多。











