MongoDB单事务仅支持副本集内多文档操作,无法跨分片、跨集群或跨服务协调,故不能支撑分布式业务;其天然受限于部署架构与事务边界。

为什么 MongoDB 单事务不足以支撑分布式业务
MongoDB 自 4.0 起支持多文档事务,但仅限于单副本集内 —— 这意味着 transaction 方法在分片集群(sharded cluster)中默认不可用,且跨数据库、跨服务的调用天然超出其事务边界。一旦涉及订单、库存、支付等分散在不同服务或不同 MongoDB 集群中的操作,原生事务就失效了。
典型失败场景包括:
- 订单服务写入
orders集合成功,但库存服务写入inventory集合因网络超时失败 - 事务提交阶段协调者(如 Laravel 应用)崩溃,MongoDB 会话未收到
commit或abort指令,进入“悬挂状态” - 副本集故障转移期间,会话丢失,
session.commitTransaction()报错TransientTransactionError
这些都不是代码逻辑错误,而是分布式环境固有的不确定性。此时靠重试 transaction 本身没用 —— 它只解决临时性连接抖动,不解决跨系统状态不一致。
什么时候该用 Saga 补偿而非重试 MongoDB 事务
Saga 是最贴近实际落地的最终一致性方案,尤其适合已使用 MongoDB 作为主存储、又需跨服务协作的 Laravel 项目。它不要求所有参与者都支持事务,只要每个服务能提供「正向操作」和对应的「反向补偿操作」即可。
关键判断点:
- 业务流程有明确线性顺序(如:创建订单 → 扣减库存 → 发起支付 → 更新履约状态)
- 每个步骤可独立提交本地事务(
Order::create()、Inventory::decrement()等本身已是原子操作) - 每个步骤都能定义确定性补偿(如“扣减库存”的补偿是“增加库存”,不是“查余额再决定加多少”)
- 你接受“中间态可见”(例如订单已创建但库存未扣减,前端可展示“处理中”)
对比 TCC:Try 阶段需预留资源并加锁,对 MongoDB 来说往往得额外建 inventory_locks 表、维护 TTL 索引,开发成本高;Saga 直接复用现有模型和字段,更轻量。
Laravel 中用队列 + 状态机实现 Saga 补偿
核心思路:把每一步操作及其补偿逻辑封装为可重入的 Job,并用一张 saga_logs 表记录当前执行到哪一步、是否成功、是否已补偿。
实操要点:
- 定义状态字段:
status(pending/confirmed/compensated/failed)、current_step('order_created'/'inventory_deducted'/'payment_initiated')、payload(JSON 存原始参数,用于重放) - 每个 Job 执行前先检查
saga_logs对应记录的status,避免重复执行;执行后更新该记录 - 补偿 Job 必须幂等:例如
CompensateInventoryDeductionJob先查当前库存值,再执行$inventory->increment('quantity', $amount),而不是无条件加 - 失败后触发补偿链:监听
JobFailed事件,自动派发上一步的补偿 Job,直到current_step === 'order_created'且状态设为compensated
示例伪代码:
// 创建订单后,记录 saga 日志并派发下一步
$saga = SagaLog::create([
'saga_id' => Str::uuid(),
'current_step' => 'order_created',
'status' => 'confirmed',
'payload' => json_encode($orderData),
]);
Dispatch(new DeductInventoryJob($saga->id));
// DeductInventoryJob 中:
$saga = SagaLog::where('id', $sagaId)->lockForUpdate()->first();
if ($saga->status !== 'confirmed') return; // 已被跳过或补偿
try {
Inventory::where('product_id', $productId)->decrement('quantity', $amount);
$saga->update(['current_step' => 'inventory_deducted', 'status' => 'confirmed']);
} catch (\Exception $e) {
$saga->update(['status' => 'failed']);
Dispatch(new CompensateOrderCreationJob($saga->id)); // 向前补偿
}
容易被忽略的悬挂事务与监控盲区
最危险的不是失败,而是“没失败也没成功”——比如 MongoDB 事务卡在 prepare 阶段,应用进程崩溃,会话未显式 abort,MongoDB 默认保留该会话最多 60 秒(由 transactionLifetimeLimitSeconds 控制),期间该会话持有的读写锁持续阻塞其他操作。
必须做两件事:
- 在 Laravel 的
AppServiceProvider中注册定时命令,扫描db.currentOp({ "secs_running": { "$gt": 30 } }),识别长时间运行的事务会话并记录告警 - 所有 Saga 流程必须带超时兜底:例如
saga_logs表加expires_at字段,配合 Laravel 的->delay(now()->addMinutes(5))和失败重试策略,确保任何分支在 5 分钟内必须进入终态(success/fail/compensated)
没有状态监控的 Saga 不是最终一致性,是最终不可知。日志里只存 saga_id 和 current_step 不够,必须关联到具体用户 ID、订单号、时间戳,否则出问题时连排查入口都找不到。


















