错误码1790是MongoDB在使用多文档事务时比较常见的一种锁异常,它通常伴随TransientTransactionError标签出现,错误信息一般形如Oops, this is a transient exception或lock timeout / deadlock detected。本质上,这个错误并不是MongoDB出现了严重故障,而是事务在获取锁资源时与另一个事务发生了冲突,其中一方被强制中止以打破僵局。理解它的产生机制,是解决问题的关键。

一、错误码1790的产生原理
MongoDB从4.0版本开始支持多文档事务,事务内部采用乐观并发控制加悲观锁的混合策略。当一个事务需要对某个文档加写锁时,如果该文档已经被另一个活跃事务锁定,当前事务就会进入等待状态。MongoDB内部有一个死锁检测器,当它发现两个或多个事务形成环形等待关系,即事务A持有锁1等待锁2,而事务B持有锁2等待锁1时,会主动牺牲其中一个事务,向它抛出错误码1790,让客户端重试。
需要注意的是,1790属于TransientTransactionError分类,含义是这类错误是暂时性的,客户端捕获后应该重新执行整个事务,而不是把它当成致命错误。很多初学者看到错误就慌了,直接回滚放弃,实际上只要重试策略设计得当,绝大多数1790错误都能自动恢复。
与单文档原子操作不同,多文档事务的锁持有时间横跨整个事务生命周期。事务从执行第一条操作到提交或回滚之间,所有被它修改过的文档都会保持锁定状态。这就意味着事务越长、涉及文档越多、并发越高,发生1790的概率就越大,这也是为什么官方一直建议事务要尽量短小。
二、常见触发场景与错误日志解读
最容易触发1790的场景是两个事务以相反的顺序更新同一批文档。假设事务A先更新文档X再更新文档Y,而事务B先更新文档Y再更新文档X,当二者并发执行时,就极可能形成环形等待。可以用下面的代码模拟这个死锁过程:
// 会话1:先更新 X,再延迟后更新 Y
const session1 = client.startSession();
session1.startTransaction();
db.collection.updateOne({_id: 'X'}, {$inc: {count: 1}}, {session: session1});
setTimeout(() => {
db.collection.updateOne({_id: 'Y'}, {$inc: {count: 1}}, {session: session1});
session1.commitTransaction();
}, 3000);
// 会话2:先更新 Y,再延迟后更新 X(顺序相反)
const session2 = client.startSession();
session2.startTransaction();
db.collection.updateOne({_id: 'Y'}, {$inc: {count: 1}}, {session: session2});
setTimeout(() => {
db.collection.updateOne({_id: 'X'}, {$inc: {count: 1}}, {session: session2});
session2.commitTransaction();
}, 3000);
// 其中一个会话将收到错误码 1790 的 TransientTransactionError
除了顺序相反,还有几类高发场景值得关注。一是事务内部包含了耗时的计算或外部调用,比如在事务中查询接口、拼接大量数据后再写入,导致锁持有时间被人为拉长;二是热点文档竞争,多个事务频繁更新同一个计数器文档或配置文档;三是事务内的查询没有走索引,扫描范围扩大,间接增加了与其他事务接触的机会;四是在事务中执行了createIndex、collMod等DDL操作,DDL会请求集合级别的排他锁,与普通事务冲突非常频繁。
当1790发生时,mongod日志中通常会有类似LockTimeout或aborting transaction的记录,并附带事务ID和冲突的锁资源信息。通过db.serverStatus().transactions可以查看当前回滚的事务统计,currentOp配合waitForLock过滤条件则能找到正在等待锁的操作,这些是定位问题的第一手资料。
三、排查与解决方案
第一步是统一写操作顺序。团队约定所有事务内的更新都按照文档_id升序(或其他固定规则)执行,可以从根本上消除环形等待。这个改动成本很低,但效果显著,是解决死锁问题的首选方案。
第二步是缩短事务范围。把与数据库操作无关的业务逻辑移到事务外部,事务内只保留必要的读写。如果一批操作可以拆成多个独立的小事务,或者能用单文档原子操作(如findOneAndUpdate)替代,就优先使用后者,因为单文档操作不需要多文档事务级别的锁协调。
第三步是完善重试机制。1790属于暂时性错误,正确的处理方式是捕获后整体重试整个事务。官方驱动一般提供了回调式事务API,自动处理重试逻辑:
// 使用回调式事务,驱动会自动处理 TransientTransactionError 重试
const session = client.startSession();
try {
session.withTransaction(async () => {
await db.collection('account').updateOne(
{_id: 1}, {$inc: {balance: -100}}, {session});
await db.collection('account').updateOne(
{_id: 2}, {$inc: {balance: 100}}, {session});
}, {
readConcern: {level: 'snapshot'},
writeConcern: {w: 'majority'}
});
} finally {
session.endSession();
}
此外还可以从索引和文档设计入手优化:确保事务内查询字段都有合适的索引,避免集合扫描;对于热点计数器,可以考虑拆分文档或改用批量异步刷新;控制事务默认超时时间(transactionLifetimeLimitSeconds),避免长事务长期占用锁资源拖垮整体并发。对于分片集群,还要注意尽量让一个事务所涉及的文档落在同一个分片上,减少跨分片协调带来的额外锁开销。
总结来看,1790错误是MongoDB事务并发控制下的正常保护机制,而不是数据库缺陷。核心应对思路就是三点:让事务短小、让顺序统一、让重试兜底。做到这三点,生产环境中的锁冲突问题基本可以得到有效控制。
MongoDB故障码1790事务锁冲突MongoDB死锁修改时间:2026-09-03 00:50:50