MongoDB多文档事务必须在副本集或分片集群中使用,单节点不支持;事务需包裹选座、下单、扣库存全过程;锁粒度为文档级,须确保查询走索引;超时锁通过TTL索引自动清理。

事务必须用在 replica set 或 sharded cluster 上
MongoDB 的多文档事务不支持单节点部署,本地测试时直接启用 mongod --replSet rs0 并运行 rs.initiate() 是硬性前提。跳过这步会遇到 Transaction numbers are only allowed on a replica set member or mongos 错误,且无法通过配置文件绕过——必须是真正的副本集(哪怕只有一台机器)。
分片集群下事务有额外限制:所有操作必须落在同一个分片内,因此选座和支付关联的集合(如 seats、orders)需共用相同的分片键,比如 showId。否则事务会直接报 Transaction cannot span multiple shards。
选座与支付必须包裹在同一事务中,不能拆成两阶段提交
用户点击“确认选座并支付”那一刻,座位状态变更(从 "available" → "locked")、生成订单、扣减库存,三者必须在同一个 session.withTransaction() 内完成。任何把“先锁座再异步支付”的做法都会导致超卖或脏读——比如两个用户同时选同一座位,第一个锁成功但支付失败,第二个却因未读到锁状态而也锁成功。
实操建议:
- 用
findOneAndUpdate原子更新座位,条件包含{ status: "available", seatId: "A12" },更新为{ $set: { status: "locked", lockedAt: new Date(), userId: "u1001" } } - 若更新匹配数为 0,说明已被占,事务直接 abort
- 后续插入
orders文档时,必须校验该座位当前仍为"locked"状态,防止中间被人工释放或超时清理干扰
锁粒度要细,避免用集合级写锁
MongoDB 事务默认使用文档级锁,但若查询没走索引,会升级为集合锁,导致并发下降明显。比如按 showTime 查询场次下的空座,若 showTime 字段没建索引,事务执行期间整个 seats 集合会被阻塞。
必须确保所有事务内查询字段都有支撑索引:
-
seats集合建复合索引:{ showId: 1, seatId: 1, status: 1 } -
orders集合建索引:{ showId: 1, userId: 1, createdAt: -1 },方便查用户历史订单 - 避免在事务中做
$text搜索、正则全表扫描等重操作
超时与回滚后状态清理不能靠人工干预
事务默认 60 秒超时(transactionLifetimeLimitSeconds),超时后自动 abort,但已锁的座位不会自动释放——因为锁是文档 update 的一部分,abort 会回滚,但前提是 session 还活着。如果客户端断连或进程崩溃,锁可能残留为 "locked" 状态。
解决方案不是加定时任务扫表,而是用 TTL 索引配合业务逻辑:
- 给
seats加字段lockedExpiresAt: Date,事务中设为new Date(Date.now() + 5 * 60 * 1000)(5 分钟) - 建 TTL 索引:
db.seats.createIndex({ lockedExpiresAt: 1 }, { expireAfterSeconds: 0 }) - 用户进入支付页时,先查
{ status: "locked", userId: "u1001", lockedExpiresAt: { $gt: new Date() } },若无结果,说明锁已过期,可重新选座
真正麻烦的是支付回调成功但事务 commit 失败的边界情况——这时订单已创建,但座位没改状态。这种必须依赖幂等回调+补偿查询,而不是指望事务兜底。


















