mongodb事务未提交会持续持有文档级写锁并升级意向锁,阻塞其他操作;可通过db.currentop()定位长时间事务,用db.killop()中止;模拟锁表存在竞态、无超时、不阻ddl等风险。

事务未提交导致的锁滞留现象
MongoDB 事务本身不会“锁定整个集合”,但未提交的事务会持续持有其操作范围内文档的写锁(排他锁),其他事务或读操作若尝试访问这些文档,就会被阻塞。更关键的是,mongod 在事务开始时会对涉及的集合加意向写锁(intent lock),这个锁本身不阻塞读,但会阻止对该集合执行 drop、rename 等 DDL 操作;而一旦事务内有实际写入,意向锁会升级为真实文档级锁,进而形成事实上的“集合不可写”感知。
常见表现包括:
-
db.currentOp()中看到大量"secs_running" > 60的活跃事务,状态为"inProgress" - 应用层报错类似
Transaction 12345 has exceeded the 60s timeout或InterruptedAtShutdown - 同一集合的后续写操作明显延迟,
db.collection.stats()显示"wiredTiger" → "concurrentTransactions" → "out" > 0
如何定位长时间运行的事务
直接查 db.currentOp() 是最有效的方式,但要注意过滤条件,否则容易淹没在系统内部操作里:
- 只看用户会话:
db.currentOp({ "secs_running": { "$gt": 30 }, "active": true, "ns": { "$regex": "^yourdb\." } }) - 排除空闲连接:
"secs_running"要大于阈值(如 30 秒),且"microsecs_running"确实非零 - 关注
"transactionNumber"和"lsid",可据此关联应用日志中发起该事务的服务实例 - 注意
"waitingForLock"字段为true的操作,说明它正卡在锁等待上,上游很可能就是那个未提交事务
别依赖 show processlist —— 这是 MySQL 命令,MongoDB 不提供等效 shell 命令。
事务超时与手动中止的实际操作
MongoDB 默认事务超时是 60 秒(maxTransactionTimeMS),但这个时间是从事务首次操作开始计时,不是从 startTransaction 调用起算。如果事务里混了网络请求、文件 IO 或重试逻辑,极易超时。
- 临时调高超时:在
startTransaction时传参,例如{ maxTransactionTimeMS: 300000 }(5 分钟) - 但更推荐主动中止:拿到阻塞事务的
"lsid"后,用db.killOp(<opid>)</opid>杀掉对应操作(注意:不是 kill session,而是 kill 当前正在运行的操作 ID) - 确认是否生效:再次运行
db.currentOp(),检查该"lsid"是否已消失;同时观察"wiredTiger.concurrentTransactions.out"是否回落 - 不要用
db.shutdownServer()或重启 mongod 来“解锁”——这只会让未提交事务回滚,但可能丢失数据,且无法解决根本问题
为什么“模拟锁表”方案在生产中危险
有些团队会在集合里加一个 locked: true 字段,靠应用层读写该字段来实现“逻辑锁表”。这种做法看似可控,实则埋下三类隐患:
- 它完全绕过 MongoDB 的事务锁机制,多个客户端可能同时读到
locked: false并并发设为true,造成竞态 - 没有自动超时和清理机制,一旦应用崩溃或网络中断,
locked字段将永久为true,整个集合被“软锁定” - 它无法阻止 DDL 操作(如
createIndex),而真实事务的意向锁可以——这点常被忽略
真正需要集合级互斥的场景,应优先考虑业务拆分或队列化,而不是在数据库层强行模拟锁表。MongoDB 的设计哲学是“用小事务 + 重试 + 版本控制”替代悲观锁,硬套关系型思维反而更容易出问题。











