分片键设计不当导致事务路由失败:事务内操作必须路由至同一分片,否则报IllegalOp;未分片集合写入直接终止事务;maxTimeMS仅限协调分片主操作,不控全程超时;logicalSessionCache刷新滞后引发TransactionTooOld。

分片键导致事务路由失败:事务操作被拆到多个分片
开启分片后事务“失效”,最常见的原因是事务内写操作涉及的文档**无法被路由到同一个分片**。MongoDB 跨分片事务要求所有读写操作必须落在**单一分片**(或明确可定位到同一分片)上,否则 startTransaction 会直接报 IllegalOp 或 TransactionNotSupported。这和分片键设计强相关——如果事务里用 find({ status: "pending" }) 这类无分片键过滤的查询,mongos 就无法确定目标分片,只能拒绝执行。
检查方法:连接 mongos 执行 sh.status(),确认集合已正确分片,并用 sh.findShard("db.collection", { _id: ObjectId("...") }) 验证关键文档是否确实落在同一分片;对事务中所有查询/更新语句,确保都带全分片键(如 { tenant_id: "abc", order_id: 123 }),且分片键字段类型、值存在性与实际文档一致。
- 分片键含数组或 null 值时,文档可能被路由到不同分片,事务必然失败
- 复合分片键中,只提供前缀字段(如只传
{ a: 1 }却没传b)会导致范围不可定界,mongos 拒绝路由 - 使用哈希分片键时,
$eq查询可用,但$in、$gt等范围操作仍可能跨分片,需严格测试
事务内混入未分片集合:一个 insertOne 就让整个事务崩掉
只要事务里出现对**未启用分片的集合**(包括 config 库、local 库、system.* 集合,或虽在分片集群但未执行 sh.enableSharding("db") 和 sh.shardCollection 的集合)的写操作,MongoDB 会立刻终止事务启动,返回 IllegalOp 错误。这不是延迟报错,而是解析阶段就拦截。
典型踩坑场景:日志写入临时表、审计记录插入非分片库、或误将 admin 库命令(如 db.runCommand({setParameter: 1, ...}))塞进事务体。Java 或 Python 客户端里若复用全局 collection 实例而没确认其所属数据库是否已分片,极易触发。
- 用
db.getSiblingDB("db_name").getCollectionNames()检查目标库下所有集合是否均已分片(结果中应含shardKey字段) - 事务代码中避免硬编码库名;改用
database.getCollection("coll")并提前断言coll.stats().sharded === true - 配置中心或 ORM 层若自动创建集合,务必关掉
autoCreate,改由 DBA 统一预分片
maxTimeMS 在分片事务里根本不管用:别指望它救超时
给 session.startTransaction({ maxTimeMS: 5000 }) 设个值,不代表整个事务会在 5 秒内结束。这个参数仅约束**协调分片(coordinator shard)上主操作的执行时间**,不覆盖 prepare 广播、其他分片响应、网络往返、commit 协调等环节。真实场景中,一个分片因锁等待卡住 8 秒,maxTimeMS 完全无感,最终等到 transactionLifetimeLimitSeconds(默认 60 秒)才 abort,客户端却还在等 commit 返回。
- 应用层必须自己起定时器,在调用
session.commitTransaction()前强制中断,例如用 Java 的CompletableFuture.orTimeout()或 Python 的asyncio.wait_for() - 不要全局调大
transactionLifetimeLimitSeconds,它影响所有事务的 GC 周期,容易堆积 stale transaction - 用
db.currentOp({ secs_running: { $gt: 10 } })定期抓长事务,重点看secs_running和waitingForLock字段
logicalSessionCacheRefreshPeriodMS 同步滞后:事务号倒退引发 TransactionTooOld
分片集群中各 shard 的事务号(txnNumber)必须全局单调递增。如果某个 shard 的逻辑会话缓存长期未刷新(默认每 5 分钟一次),它的 txnNumber 就会落后于其他分片。此时发起事务,mongos 收到该分片的响应后判定“事务号太旧”,直接抛 TransactionTooOld ——现象是事务连 startTransaction 都过不去,更别说执行。
验证方式:逐个直连各 shard(绕过 mongos),执行 db.runCommand({ getLastError: 1 }),比对返回中 txnNumber 是否明显偏低(差值 > 100 就危险)。生产环境建议把 logicalSessionCacheRefreshPeriodMS 从默认 300000 改为 30000,并滚动重启分片确认生效。
- 执行
db.adminCommand({ refreshLogicalSessionCacheNow: 1 })可立即刷新,但不是原子操作,需逐个分片调用 - 该问题在低流量时段更易复现——因为缓存更新靠心跳驱动,冷分片几乎不触发刷新
- 监控项应加入:
shard.logicalSessionCache.refreshIntervalMillis和shard.logicalSessionCache.entries,异常时告警
事务真正卡住的位置,往往不在你写的那几行 update 里,而在 mongos 到 shard 的 prepare 阶段、或某个分片上未释放的写锁。分片键选得再准,也救不了一个没加索引的事务内查询。


















