分布式事务场景

2020年某支付平台的转账系统出现了严重的资金不一致问题:用户A向用户B转账100元,扣款成功但收款失败。

技术团队排查后发现:转账业务涉及两个服务——账户服务(扣款)和通知服务(通知收款方)。账户服务扣款成功了,但通知服务调用失败,整个事务回滚时,账户服务没有回滚成功。

这次不一致导致平台损失了约1万元,因为扣款了但没通知用户。

分布式事务是分布式系统中最经典的问题之一。

【面试官手记】

分布式事务是生产环境最复杂的问题之一。我面试过的候选人里,能说清楚"2PC和3PC区别"的不超过30%,能说清楚"TCC和Saga区别"的不超过20%。分布式事务的关键是理解每种方案的trade-off

一、分布式事务的CAP理论 🔴

1.1 CAP三角

CAP理论:分布式系统最多只能同时满足两项

C (Consistency):一致性
- 所有节点在同一时刻看到相同的数据

A (Availability):可用性
- 每个请求都能在合理时间内得到响应

P (Partition tolerance):分区容错性
- 系统在网络分区时仍能运行

分布式系统必须满足P,因此只能在C和A之间选择:
- CP系统:优先保证一致性,如Zookeeper、HBase
- AP系统:优先保证可用性,如Cassandra、Eureka

1.2 BASE理论

BASE理论:对CAP中一致性和可用性的权衡

Basically Available:基本可用
- 允许系统在故障时降低可用性

Soft state:软状态
- 允许系统数据在中间状态存在不确定性

Eventually consistent:最终一致
- 允许系统在故障恢复后达到一致

1.3 面试追问

面试官:分布式事务有哪些解决方案?

候选人:主要有四种:

一是2PC/3PC:强一致,但性能差,有单点问题

二是TCC:补偿模式,性能好,但实现复杂

三是Saga:长事务模式,适合长流程

四是本地消息表:最终一致,实现简单

【面试官心理】

分布式事务的追问通常很深入。能回答出四种方案的候选人,说明知道全貌;能说出各方案trade-off的候选人,说明有深度理解。

二、两阶段提交(2PC)🔴

2.1 原理

2PC:Two-Phase Commit

第一阶段:准备阶段
- 协调者向所有参与者发送Prepare
- 参与者执行事务,但不提交
- 参与者返回成功/失败

第二阶段:提交阶段
- 协调者收到所有成功 → 发送Commit
- 协调者收到任何失败 → 发送Rollback
- 参与者收到Commit → 提交事务
- 参与者收到Rollback → 回滚事务

2.2 代码示例

// 协调者实现
public class TransactionCoordinator {

    public void commit(Transaction transaction) {
        // 第一阶段:准备
        List<Participant> participants = transaction.getParticipants();
        boolean allPrepared = true;

        for (Participant participant : participants) {
            boolean prepared = participant.prepare();
            if (!prepared) {
                allPrepared = false;
                break;
            }
        }

        // 第二阶段:提交
        if (allPrepared) {
            for (Participant participant : participants) {
                participant.commit();
            }
        } else {
            for (Participant participant : participants) {
                participant.rollback();
            }
        }
    }
}

2.3 问题

2PC的问题:

1. 同步阻塞
   - 准备阶段所有资源被锁定
   - 其他事务无法访问

2. 单点故障
   - 协调者挂了,参与者无法知道下一步
   - 需要超时机制解决

3. 数据不一致
   - 协调者发送Commit后,部分参与者收到失败
   - 导致数据不一致

三、TCC模式 🟡

3.1 原理

TCC:Try-Confirm-Cancel

Try:预留资源
- 检查资源是否可用
- 预留资源(冻结)

Confirm:确认执行
- 真正执行操作
- 使用预留的资源

Cancel:取消执行
- 释放预留的资源
- 回滚操作

3.2 代码示例

// TCC实现
@LocalTCC
public interface AccountTCCService {

    @TwoPhaseBusinessAction(
        name = "deduct",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryDeduct(
        @BusinessActionContextParameter(paramName = "accountId") Long accountId,
        @BusinessActionContextParameter(paramName = "amount") BigDecimal amount
    );

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

@Service
public class AccountTCCServiceImpl implements AccountTCCService {

    @Override
    public boolean tryDeduct(Long accountId, BigDecimal amount) {
        // 1. 检查余额是否充足
        Account account = accountDAO.selectById(accountId);
        if (account.getBalance().compareTo(amount) < 0) {
            return false;  // 余额不足
        }

        // 2. 冻结金额(扣除可用余额,增加冻结金额)
        account.setBalance(account.getBalance().subtract(amount));
        account.setFrozenAmount(account.getFrozenAmount().add(amount));
        accountDAO.update(account);

        return true;
    }

    @Override
    public boolean confirm(BusinessActionContext context) {
        Long accountId = context.getActionContext().get("accountId");
        BigDecimal amount = context.getActionContext().get("amount");

        // 确认:扣除冻结金额
        Account account = accountDAO.selectById(accountId);
        account.setFrozenAmount(account.getFrozenAmount().subtract(amount));
        accountDAO.update(account);

        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext context) {
        Long accountId = context.getActionContext().get("accountId");
        BigDecimal amount = context.getActionContext().get("amount");

        // 取消:解冻金额
        Account account = accountDAO.selectById(accountId);
        account.setFrozenAmount(account.getFrozenAmount().subtract(amount));
        account.setBalance(account.getBalance().add(amount));
        accountDAO.update(account);

        return true;
    }
}

四、Saga模式 🟡

4.1 原理

Saga模式:把长事务拆成多个短事务

正向补偿:
T1 → T2 → T3 → ... → Tn
   ↓   ↓   ↓       ↓
  C1   C2   C3     Cn

反向补偿:
Tn执行失败 → Cn回滚 → ... → C2回滚 → C1回滚

适用场景:长流程业务
- 订单 → 支付 → 发货 → 收货 → 完成

4.2 代码示例

// Saga编排器
@Service
public class OrderSaga {

    @Autowired
    private OrderService orderService;
    @Autowired
    private PaymentService paymentService;
    @Autowired
    private InventoryService inventoryService;

    public void createOrder(Order order) {
        try {
            // 1. 创建订单(正向)
            orderService.create(order);

            // 2. 扣减库存(正向)
            inventoryService.deduct(order.getItems());

            // 3. 扣款(正向)
            paymentService.deduct(order.getUserId(), order.getAmount());

        } catch (InventoryException e) {
            // 2失败,回滚1
            orderService.cancel(order.getId());
            throw e;

        } catch (PaymentException e) {
            // 3失败,回滚1和2
            inventoryService.refund(order.getItems());
            orderService.cancel(order.getId());
            throw e;
        }
    }
}

五、方案对比 🟡

5.1 方案对比

方案一致性性能复杂度适用场景
2PC强一致低并发短事务
3PC强一致低并发短事务
TCC最终一致高并发短事务
Saga最终一致长流程事务
本地消息表最终一致异步处理

5.2 选型建议

选型决策树:

事务时长 < 1秒?
  是 → 考虑2PC/TCC
  否 → Saga

一致性要求100%?
  是 → 2PC
  否 → TCC/Saga

参与者数量 < 3个?
  是 → TCC
  否 → Saga

业务是长流程?
  是 → Saga
  否 → 本地消息表

六、生产避坑 🟡

6.1 分布式事务的五大坑

坑1:TCC幂等性

问题:Confirm/Cancel执行多次,导致重复扣款
场景:网络抖动导致重试
解决方案:
- 每个操作都要幂等
- 用事务ID去重

坑2:悬挂问题

问题:Try成功但Confirm/Cancel超时
场景:资源被预留但无法释放
解决方案:
- 定时任务清理超时的事务
- 设置事务超时时间

坑3:服务间循环依赖

问题:服务A依赖B,B依赖A
场景:转账业务中,A扣款B增加
解决方案:
- 解耦:引入第三方服务
- 或打破循环:延迟依赖

坑4:补偿逻辑复杂

问题:正向和反向逻辑不一致
场景:业务逻辑变化后忘记更新补偿
解决方案:
- 自动化生成补偿代码
- 或使用事件驱动

坑5:超时机制不完善

问题:事务等待超时,但没有回滚
场景:网络分区
解决方案:
- TCC框架负责超时回滚
- Saga需要自己实现超时处理

七、真实面试回放 🟡

面试官:2PC和TCC的区别是什么?

候选人(小张):两个区别:

一是执行位置不同。2PC是在数据库层面,由TM和RM协调;TCC是在业务层面,由业务代码实现。

二是资源锁定不同。2PC锁数据库资源,时间长;TCC锁的是业务预留的资源,时间短。

面试官:TCC的Try-Confirm-Cancel分别做什么?

小张:Try是预留资源,比如冻结金额;Confirm是确认使用资源;Cancel是释放资源。

扣款的例子:Try阶段检查余额并冻结,Confirm阶段确认扣除冻结金额,Cancel阶段解冻金额。

面试官:Saga模式适合什么场景?

小张:长流程业务。比如创建订单 → 扣款 → 发货 → 收货 → 完成。

每个步骤都是独立的服务,失败了再逐一回滚。

【面试官手记】

小张这场面试的亮点:

  1. 知道2PC和TCC的区别:数据库层面 vs 业务层面

  2. 知道TCC的Try/Confirm/Cancel语义

  3. 知道Saga适合长流程

分布式事务是P7工程师必备技能,能完整回答的候选人,说明有架构设计能力。

分布式事务的核心是根据业务场景选择合适方案。记住三个要点:

  1. 2PC/TCC:适合短事务,CP优先
  2. Saga:适合长流程,AP优先
  3. 本地消息表:最终一致,实现简单

没有最好的方案,只有最适合业务场景的方案。