多数据源 × 事务交互

目标

本页说明在 Juice 中,数据源切换(``engine.With(…)``)事务(``Transaction/Tx``) 如何组合,避免跨库事务误用。

核心结论

  • engine.With("name") 会返回绑定到目标数据源的新 Engine 视图。

  • juice.Transaction 的事务只覆盖**当前 engine 对应的数据源连接**。

  • NestedTransaction 只处理“是否已在事务中”,不负责跨数据源一致性。

  • Juice 不提供分布式事务(2PC/XA)抽象;跨库一致性建议由业务侧策略保证。

交互矩阵

场景

是否单事务覆盖

风险

建议

单一数据源内 Transaction

风险低

直接使用

事务外先 With("slave")Transaction

是(仅该 slave)

写入/只读职责混淆

明确读写策略后使用

事务回调内临时切换到另一数据源执行

否(通常变成两个独立事务域)

数据不一致、部分提交

避免;改为 saga/outbox

同请求内多次切换数据源并写入

无法原子提交

拆分步骤并设计补偿

推荐模式

模式一:先选数据源,再开事务

base := juice.ContextWithManager(context.Background(), engine)

writeEngine, err := engine.With("master")
if err != nil {
    return err
}

ctx := juice.ContextWithManager(base, writeEngine)
return juice.Transaction(ctx, func(ctx context.Context) error {
    _, err := repo.CreateOrder(ctx, req)
    return err
})

模式二:读写分离下,写路径固定主库事务

  • 查询可按策略走从库(无事务或只读事务)。

  • 下单、扣减库存、记账等写路径统一走主库事务。

模式三:跨数据源一致性用业务模式兜底

可选策略:

  • outbox + 异步投递

  • saga(补偿事务)

  • 幂等键 + 重试

反例(不推荐)

// 外层在 master 开事务
_ = juice.Transaction(ctx, func(ctx context.Context) error {
    if _, err := repoA.WriteOnMaster(ctx, a); err != nil {
        return err
    }

    // 在同一回调里切到 slave/other 再写
    other, _ := engine.With("other")
    otherCtx := juice.ContextWithManager(ctx, other)
    _, err := repoB.WriteOnOther(otherCtx, b)
    return err
})

上面代码通常不能保证两个写操作原子一致。

排障建议

  • 若出现“部分成功部分失败”,先检查是否在一个业务动作里写了多个数据源。

  • 若事务似乎“没生效”,检查调用链是否混入了另一个 engine.With(...) 的上下文。

  • 在日志中打印 EnvID(若你在中间件里可拿到)有助于快速定位数据源漂移问题。