在一个数据库事务内,多个sql写操作可以保证强一致。而到了分布式场景,只能做到最终一致。
这就引出了BASE理论:基本可用、软状态、最终一致性,即允许系统存在中间不一致状态,只要最终达到一致即可。分布式事务方案大多在BASE理论指导下,放弃强一致,追求最终一致。

主流方案和最佳场景

下面每种方案都用一个独立的、最适合的业务场景来介绍。

1. TCC

  • 最佳场景:下单时扣库存、扣余额
    比如下单流程,涉及订单服务、库存服务、支付服务。需要同时创建订单、冻结库存、冻结余额,并发量较高,且对一致性要求严格。

  • 原理与实现机制
    TCC需要一个协调者,一般称为事务管理器,负责编排整个流程。但资源锁定做到了业务层、而非DB层,由应用代码显式实现Try、Confirm、Cancel三个接口。
    TM(事务管理器):负责调用各个服务的Try方法,根据结果决定全部Confirm或全部Cancel。需记录事务日志,处理重试和异常
    RM(资源管理器):各业务服务自身,需要提供Try/Confirm/Cancel三个接口,并保证幂等(可能会调多次)

  • 流程细节

    • TM依次调用每个服务的Try接口,预留业务资源(如冻结库存、冻结余额),不用上数据库行锁
    • 若所有Try成功,TM依次调用Confirm接口,将预留资源实际消耗或状态推进
    • 若任何Try失败或超时,TM调用所有已成功服务的Cancel接口,释放预留资源
    • 框架需要处理空回滚(Try未执行但Cancel被调用)、悬挂(Cancel比Try先到)、幂等(接口被重复调用)等问题,一般通过记录全局事务状态,并用全局事务ID查询状态来实现
  • 举例:库存服务Try/Confirm/Cancel

tcc

    // Try阶段:冻结库存、扣减可用库存
    @Transactional
    public boolean tryFreezeInventory(String productId, int qtyDelta, String txId) {
        // 冻结库存、扣减可用库存:update inventory set frozen_qty = frozen_qty + #{qtyDelta}, available_qty = available_qty - #{qtyDelta}
        // where product_id = #{productId} and available_qty - #{qtyDelta} > 0
        if (inventoryMapper.freeze(productId, qtyDelta) > 0) {
            // 冻结库存成功,记录库存变更明细,状态为TRY
            freezeRecordMapper.insert(txId, productId, qtyDelta);
            return true;
        } else{ 
            return false;
        }
    }

    // Confirm阶段:扣减冻结库存
    @Transactional
    public void confirm(String productId, int qtyDelta, String txId) {
        // 扣减冻结库存:update inventory set frozen_qty = frozen_qty - #{qtyDelta} where product_id = #{productId}
        inventoryMapper.deduct(productId, qtyDelta);
        // 更新库存变更明细为已完成
        freezeRecordMapper.updateStatus(txId, "CONFIRMED");
    }

    // Cancel阶段:释放冻结库存、可用库存
    @Transactional
    public void cancel(String productId, int qtyDelta, String txId) {
        FreezeRecord r = freezeRecordMapper.selectByTxId(txId);
        if (r == null) {
            return; // 空回滚
        }

        // 释放冻结库存、可用库存:update inventory set frozen_qty = frozen_qty - #{qtyDelta}, available_qty = available_qty + #{qtyDelta}
        // where product_id = #{productId}
        inventoryMapper.release(productId, qtyDelta);
        // 更新库存变更明细为已取消
        freezeRecordMapper.updateStatus(txId, "CANCELLED");
    }

TCC将所有资源操作变成“预留”而非直接扣减,避免了数据库行锁长时间持有,非常适合秒杀、支付等并发高、资源竞争激烈的场景。

2. Saga

Saga一词的本意是长篇故事,在分布式事务场景下是指一连串的事务。

  • 最佳场景:出行预订(机票+酒店+门票)
    比如在线旅行平台,用户一次性预订机票、酒店、门票,三个服务相互独立、流程长,并且每个预订都会调用外部第三方接口。因为无法控制外部服务的实现,只能通过Saga模式来实现调外部接口、长流程的正向事务、补偿事务。

  • 原理与实现机制
    Saga模式的核心思想是将一个长事务拆分为多个有序的本地事务,每个本地事务执行后直接提交,不锁定资源。如果后续步骤失败,则通过逆序调用之前的补偿事务来回滚。
    Saga一般采用编排式(Orchestration):存在一个中心协调者(Orchestrator),它负责调用每个步骤的本地事务,并在失败时按逆序调用补偿。逻辑集中,易于监控。协调者需要持久化Saga状态机,以便在自身宕机后能够恢复并继续执行或补偿。

  • 关键实现点

    • 每个正向事务都必须有一个对应的补偿事务,且补偿必须是幂等的,因为可能存在重试
    • 缺少隔离性:Saga不锁资源,中间状态可被读取到,因此业务设计上需要允许暂时的脏读(例如预订酒店后变为“酒店预订成功”,但因为机票出票失败后回滚为“酒店预订已取消”)
    • 补偿失败时需要告警并人工介入,因为回滚本身也可能失败

举例:出行预订的正向事务与补偿

saga

    // 正向事务:调用第三方接口预订酒店
    public Booking bookHotel(HotelReq req) {
        String bookingId = hotelApi.reserve(req);
        return bookingRepo.save(new Booking(bookingId, "RESERVED"));
    }

    // 补偿事务:取消预订
    public void cancelHotel(Booking booking) {
        hotelApi.cancel(booking.getBookingId());
        bookingRepo.updateStatus(booking.getId(), "CANCELLED");
    }

Saga不占用数据库锁,各服务完全独立,非常适合调用外部服务、流程长的业务。代价是需要实现补偿逻辑,且中间状态可见,业务必须能容忍短暂的不一致。

3. 可靠消息

  • 最佳场景:非核心链路的联动处理场景
    确保一定通知到、但可能多发消息,联动方需做好幂等。比如用户注册送积分场景,在用户注册后,需要发送欢迎邮件、初始化积分账户。这些动作可以异步完成,允许秒级延迟,但必须保证最终完成。

  • 原理与实现机制
    要实现可靠消息,需要通过本地消息表模式,即本地事务+消息队列实现最终一致。核心思想是将业务操作和消息持久化放在同一个DB事务中,然后通过后台任务将消息发送到MQ,下游消费时保证幂等。

  • 关键流程

    • 消息生产方:执行业务操作的同时,向本地DB的outbox表插入一条“待发送”消息,两者在同一数据库事务中提交。然后发消息到MQ,最后更新状态为“已发送”,这2步可能失败
    • 兜底定时任务:定时查询outbox表中超过一定时间仍处于“待发送”状态的记录,将消息发送到MQ,成功后更新状态为“已发送”
    • 消息消费方:监听消息,执行本地事务(如初始化积分),通过业务唯一键保证幂等

举例:注册用户时写入本地消息表、发消息

msg

-- 用户服务本地事务
BEGIN;
  INSERT INTO users(user_id, email, ...) VALUES(...);
  INSERT INTO outbox(message_id, topic, payload, status) VALUES('msg_reg_123', 'USER_REGISTERED', '{"userId":...}', 'PENDING');
COMMIT;

4. 两阶段提交(2PC,2 Phase Commit)

  • 最佳场景:内部低并发强一致转账
    比如公司内部财务系统,A账户向B账户转账,两个账户分别在不同数据库。并发量极低,但绝不允许出现金额不一致。

  • 原理与实现机制
    2PC需要一个全局事务协调者(Coordinator),通常由事务管理器(如JTA实现)充当。参与者是各个资源管理器(RM),如数据库。
    协调者:负责调度整个事务流程,发送指令,收集投票结果。
    参与者:实际持有资源的节点,执行预提交和最终提交/回滚。

  • 流程细节

  • 阶段一(准备/投票):协调者向所有参与者发送事务内容,询问是否可以提交。参与者各自锁定资源(如数据库行锁),执行事务操作并写入undo/redo日志,但不提交,然后返回Yes或No

  • 阶段二(提交/回滚):若所有参与者返回Yes,协调者发送提交指令;若任一返回No或超时,则发送回滚指令。参与者根据指令完成提交或回滚并释放锁。存在“协调者单点故障”问题,若阶段二指令未能送达所有参与者,部分参与者可能处于不确定状态,需要人工介入或日志恢复

举例:公司内部跨系统转账

2pc

更多推荐