微服务11-Java分布式事务:处理跨服务的事务

Java分布式事务:处理跨服务的事务

在微服务架构中,业务流程往往需要多个服务协同完成(如电商的下单流程涉及订单服务、库存服务、支付服务)。当这些服务操作不同的数据库时,如何保证数据一致性就成了核心挑战——这正是分布式事务要解决的问题。

与单体应用的本地事务(ACID特性)不同,分布式事务面临网络分区、服务故障、数据延迟等复杂问题,传统的事务机制难以直接适用。本文将从理论到实践,详细讲解Java分布式事务的解决方案,包含完整代码示例场景分析,帮助开发者在实际项目中落地可靠的跨服务事务处理方案。

一、分布式事务的核心挑战与理论基础

在深入技术方案前,需先理解分布式事务的本质问题和底层理论,为方案选择提供依据。

1.1 什么是分布式事务?

分布式事务是指跨多个服务或数据源的事务,要求所有参与方要么全部成功,要么全部失败,以保证数据一致性。例如:

  • 电商下单:订单服务创建订单、库存服务扣减库存、支付服务处理支付,三者必须同时成功或失败;
  • 转账业务:用户A的账户扣款、用户B的账户收款,两个操作需保持一致。

分布式事务的难点在于:

  • 服务间通过网络通信,存在超时、丢包等不可靠因素;
  • 每个服务管理独立的数据库,无法通过单库事务保证全局一致性;
  • 部分服务成功、部分失败时,需有机制进行回滚或补偿。

1.2 理论基础:CAP定理与BASE理论

分布式系统的特性由以下理论奠定,直接影响分布式事务方案的设计:

1.2.1 CAP定理

CAP定理指出,分布式系统无法同时满足以下三个特性,最多只能满足两个:

  • 一致性(Consistency):所有节点在同一时间看到的数据是一致的;
  • 可用性(Availability):任何请求(即使是部分节点故障)都能收到非错误响应;
  • 分区容错性(Partition Tolerance):网络分区(部分节点无法通信)时,系统仍能继续运行。

在微服务架构中,网络分区是不可避免的(P必须满足),因此只能在A和C之间权衡:

  • 优先保证一致性(C):适合金融交易等核心场景,但可能牺牲可用性;
  • 优先保证可用性(A):适合社交、内容平台等,允许短暂的数据不一致,最终通过补偿达到一致。
1.2.2 BASE理论

BASE理论是对CAP定理的补充,为分布式事务提供了实用的指导思想:

  • 基本可用(Basically Available):允许系统在故障时降级(如限流、返回缓存数据),保证核心功能可用;
  • 软状态(Soft State):允许数据存在中间状态(如“处理中”),不影响系统整体可用性;
  • 最终一致性(Eventually Consistent):不要求实时一致, but 保证经过一段时间后,数据最终达到一致状态。

BASE理论是多数微服务场景的选择:通过牺牲强一致性,换取高可用性,最终通过补偿机制实现数据一致。

1.3 分布式事务的一致性模型

根据一致性要求的强弱,分布式事务可分为:

  • 强一致性:所有操作完成后,数据立即一致(如2PC方案);
  • 最终一致性:允许短暂不一致,通过异步补偿最终达到一致(如TCC、Saga、事务消息等)。

实际项目中,最终一致性方案因性能优势被广泛采用,强一致性方案仅用于对数据一致性要求极高的场景(如金融核心交易)。

二、Java分布式事务解决方案详解

Java生态中有多种分布式事务解决方案,每种方案有其适用场景和实现复杂度。以下是最常用的5种方案,从原理、代码示例到优缺点进行全面解析。

2.1 两阶段提交(2PC, Two-Phase Commit)

2PC是最经典的强一致性方案,通过“准备阶段”和“提交阶段”协调所有参与者,保证要么全成功,要么全失败。

2.1.1 原理

2PC引入事务协调者(Coordinator) 角色,负责协调多个事务参与者(Participant)

  1. 准备阶段:协调者向所有参与者发送“准备”请求,参与者执行本地事务(不提交),记录undo/redo日志,返回“就绪”或“失败”;
  2. 提交阶段
    • 若所有参与者返回“就绪”,协调者发送“提交”请求,参与者执行提交并释放资源;
    • 若任一参与者返回“失败”,协调者发送“回滚”请求,参与者根据undo日志回滚。
2.1.2 代码示例:基于Seata的AT模式(2PC的优化实现)

Seata是阿里开源的分布式事务框架,其AT模式是2PC的改进版(自动生成undo/redo日志,降低开发成本)。

步骤1:部署Seata Server(协调者)

  1. 下载Seata Server:Seata官方仓库
  2. 配置注册中心(如Nacos)和事务日志存储(如MySQL),启动Server。

步骤2:微服务集成Seata
以Spring Boot微服务为例,集成Seata实现分布式事务。

  1. 添加依赖(pom.xml):
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <version>2022.0.0.0-RC2</version>
</dependency>
  1. 配置Seata(application.yml):
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: my_test_tx_group # 事务分组,需与Seata Server配置一致

seata:
  registry:
    type: nacos # 注册中心类型
    nacos:
      server-addr: 127.0.0.1:8848 # Nacos地址
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

步骤3:实现分布式事务
以“下单扣库存”为例,订单服务(order-service)和库存服务(stock-service)参与分布式事务。

  1. 订单服务(发起方)
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StockFeignClient stockFeignClient; // 调用库存服务的Feign客户端

    // @GlobalTransactional:标记为全局事务,Seata自动协调
    @GlobalTransactional(rollbackFor = Exception.class)
    public void createOrder(Long productId, Integer count) {
        // 1. 创建订单(本地事务)
        Order order = new Order();
        order.setProductId(productId);
        order.setCount(count);
        order.setStatus(1); // 1-未支付
        orderMapper.insert(order);

        // 2. 调用库存服务扣减库存(远程事务)
        boolean deductSuccess = stockFeignClient.deductStock(productId, count);
        if (!deductSuccess) {
            throw new RuntimeException("库存不足,创建订单失败");
        }

        // 若此处抛出异常,Seata会协调所有参与者回滚
    }
}
  1. 库存服务(参与者)
@Service
public class StockService {
    @Autowired
    private StockMapper stockMapper;

    // 本地事务方法,由Seata代理
    public boolean deductStock(Long productId, Integer count) {
        // 扣减库存
        int rows = stockMapper.deductStock(productId, count);
        return rows > 0;
    }
}
  1. Feign接口(订单服务调用库存服务):
@FeignClient(name = "stock-service")
public interface StockFeignClient {
    @PostMapping("/stock/deduct")
    boolean deductStock(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
2.1.3 优缺点与适用场景
  • 优点:强一致性,实现简单(Seata AT模式几乎无侵入);
  • 缺点
    • 性能差:准备阶段会锁定资源,协调者与参与者之间的网络通信可能导致长时间阻塞;
    • 协调者单点故障风险:若协调者在提交阶段崩溃,参与者可能处于“未知状态”;
  • 适用场景:短事务、一致性要求极高的场景(如金融支付),参与者数量少。

2.2 TCC模式(Try-Confirm-Cancel)

TCC是一种侵入式较强但性能优异的方案,通过业务逻辑拆分实现分布式事务,适合核心业务场景。

2.2.1 原理

TCC将分布式事务拆分为三个阶段,由业务代码显式实现:

  1. Try(尝试):检查资源并预留资源(如扣减库存前先冻结部分库存);
  2. Confirm(确认):确认执行业务操作(如将冻结的库存实际扣减),需保证幂等性;
  3. Cancel(取消):若业务失败,释放Try阶段预留的资源(如解冻冻结的库存),需保证幂等性。

TCC需要事务协调者记录各参与者的状态,在Try成功后触发Confirm,失败则触发Cancel。

2.2.2 代码示例:手动实现TCC

以“下单扣库存”为例,手动实现TCC的三个阶段。

步骤1:定义TCC接口
每个参与者需实现Try/Confirm/Cancel接口。

  1. 库存服务的TCC接口
public interface StockTccService {
    // Try:冻结库存
    @Transactional
    boolean tryFreezeStock(Long productId, Integer count, String xid);

    // Confirm:确认扣减库存(释放冻结)
    @Transactional
    boolean confirmDeductStock(String xid);

    // Cancel:取消冻结(解冻库存)
    @Transactional
    boolean cancelFreezeStock(String xid);
}
  1. 订单服务的TCC接口(创建订单):
public interface OrderTccService {
    // Try:创建订单(状态为"待确认")
    @Transactional
    boolean tryCreateOrder(Long productId, Integer count, String xid);

    // Confirm:确认订单(状态改为"已确认")
    @Transactional
    boolean confirmCreateOrder(String xid);

    // Cancel:取消订单(删除或标记为"已取消")
    @Transactional
    boolean cancelCreateOrder(String xid);
}

步骤2:实现TCC接口
库存服务实现类:

@Service
public class StockTccServiceImpl implements StockTccService {
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private StockFreezeMapper freezeMapper; // 库存冻结记录表

    // Try:冻结库存(记录冻结数量和事务ID)
    @Override
    public boolean tryFreezeStock(Long productId, Integer count, String xid) {
        // 1. 检查库存是否充足
        Stock stock = stockMapper.selectById(productId);
        if (stock == null || stock.getCount() < count) {
            return false;
        }

        // 2. 冻结库存(插入冻结记录)
        StockFreeze freeze = new StockFreeze();
        freeze.setProductId(productId);
        freeze.setFreezeCount(count);
        freeze.setXid(xid); // 事务ID,用于确认/取消时关联
        freeze.setStatus(0); // 0-未处理
        freezeMapper.insert(freeze);
        return true;
    }

    // Confirm:确认扣减(实际扣减库存,删除冻结记录)
    @Override
    public boolean confirmDeductStock(String xid) {
        // 1. 查询冻结记录
        StockFreeze freeze = freezeMapper.selectByXid(xid);
        if (freeze == null) {
            return true; // 幂等处理:已处理过
        }

        // 2. 扣减实际库存
        stockMapper.deductStock(freeze.getProductId(), freeze.getFreezeCount());

        // 3. 标记冻结记录为已处理
        freeze.setStatus(1);
        freezeMapper.updateById(freeze);
        return true;
    }

    // Cancel:取消冻结(解冻库存,删除冻结记录)
    @Override
    public boolean cancelFreezeStock(String xid) {
        // 1. 查询冻结记录
        StockFreeze freeze = freezeMapper.selectByXid(xid);
        if (freeze == null) {
            return true; // 幂等处理
        }

        // 2. 解冻库存(增加可用库存)
        stockMapper.increaseStock(freeze.getProductId(), freeze.getFreezeCount());

        // 3. 标记冻结记录为已取消
        freeze.setStatus(2);
        freezeMapper.updateById(freeze);
        return true;
    }
}

步骤3:事务协调者
协调者负责生成全局事务ID(xid),调用各参与者的Try方法,根据结果触发Confirm或Cancel:

@Service
public class TccCoordinator {
    @Autowired
    private OrderTccService orderTccService;
    @Autowired
    private StockTccService stockTccService;
    @Autowired
    private TransactionLogMapper logMapper; // 记录事务状态

    public void createOrderWithTcc(Long productId, Integer count) {
        // 1. 生成全局事务ID
        String xid = UUID.randomUUID().toString();

        boolean allTrySuccess = false;
        try {
            // 2. 调用所有参与者的Try方法
            boolean orderTry = orderTccService.tryCreateOrder(productId, count, xid);
            boolean stockTry = stockTccService.tryFreezeStock(productId, count, xid);

            allTrySuccess = orderTry && stockTry;
            if (!allTrySuccess) {
                throw new RuntimeException("Try阶段失败");
            }

            // 3. Try全部成功,调用Confirm
            orderTccService.confirmCreateOrder(xid);
            stockTccService.confirmDeductStock(xid);

            // 记录事务成功日志
            logMapper.insert(new TransactionLog(xid, "SUCCESS"));
        } catch (Exception e) {
            // 4. 任一Try失败或异常,调用Cancel
            if (allTrySuccess) {
                // Try成功后异常,需Confirm补偿(防止部分Confirm未执行)
                orderTccService.confirmCreateOrder(xid);
                stockTccService.confirmDeductStock(xid);
            } else {
                // Try失败,执行Cancel
                orderTccService.cancelCreateOrder(xid);
                stockTccService.cancelFreezeStock(xid);
            }
            logMapper.insert(new TransactionLog(xid, "FAIL"));
            throw e;
        }
    }
}
2.2.3 优缺点与适用场景
  • 优点
    • 性能好:Try阶段仅锁定必要资源,无全局锁;
    • 灵活性高:可根据业务定制资源预留和补偿逻辑;
  • 缺点
    • 侵入性强:需手动实现Try/Confirm/Cancel接口,开发成本高;
    • 需处理幂等性:Confirm/Cancel可能被重复调用(如网络重试);
  • 适用场景:核心业务(如支付、库存扣减),对性能要求高,参与者数量少。

2.3 Saga模式

Saga模式适用于长事务和跨多个服务的场景,将分布式事务拆分为一系列本地事务,通过补偿事务保证最终一致性。

2.3.1 原理

Saga模式将分布式事务分解为T1, T2, …, Tn 一系列本地事务,每个Ti对应一个补偿事务Ci(用于撤销Ti的影响):

  • 若所有Ti执行成功,事务完成;
  • 若某个Ti执行失败,按相反顺序执行已成功Ti的补偿事务(Cn, …, Ci+1, Ci)。

Saga有两种实现方式:

  • 编排式(Choreography):各服务通过事件通知协作,无中央协调者;
  • 编排式(Orchestration):由一个中央协调者调用各服务的本地事务和补偿事务。
2.3.2 代码示例:基于事件的编排式Saga

以电商下单流程(订单→库存→支付)为例,通过事件驱动实现Saga。

步骤1:定义事件与事件发布
使用Spring Cloud Stream发送事件(基于Kafka/RabbitMQ):

// 事件定义
@Data
public class OrderCreatedEvent {
    private String orderId;
    private Long productId;
    private Integer count;
    private LocalDateTime createTime;
}

public class StockDeductedEvent { /* 库存扣减事件 */ }
public class PaymentCompletedEvent { /* 支付完成事件 */ }
public class StockDeductFailedEvent { /* 库存扣减失败事件 */ }

// 事件发布服务
@Service
public class EventPublisher {
    @Autowired
    private StreamBridge streamBridge;

    // 发布订单创建事件
    public void publishOrderCreated(OrderCreatedEvent event) {
        streamBridge.send("orderCreatedChannel", event);
    }

    // 其他事件发布方法...
}

步骤2:各服务实现本地事务与补偿

  1. 订单服务
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private EventPublisher eventPublisher;

    // 创建订单(T1)
    @Transactional
    public void createOrder(Long productId, Integer count) {
        String orderId = UUID.randomUUID().toString();
        Order order = new Order();
        order.setId(orderId);
        order.setProductId(productId);
        order.setCount(count);
        order.setStatus(0); // 0-待处理
        orderMapper.insert(order);

        // 发布订单创建事件,触发库存扣减
        eventPublisher.publishOrderCreated(new OrderCreatedEvent(orderId, productId, count, LocalDateTime.now()));
    }

    // 补偿事务:取消订单(C1)
    @Transactional
    public void cancelOrder(String orderId) {
        Order order = orderMapper.selectById(orderId);
        if (order != null) {
            order.setStatus(-1); // -1-已取消
            orderMapper.updateById(order);
        }
    }
}
  1. 库存服务(监听订单创建事件,执行扣减):
@Service
public class StockService {
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private EventPublisher eventPublisher;

    // 监听订单创建事件,执行库存扣减(T2)
    @Bean
    public Consumer<OrderCreatedEvent> handleOrderCreated() {
        return event -> {
            try {
                // 扣减库存
                int rows = stockMapper.deductStock(event.getProductId(), event.getCount());
                if (rows > 0) {
                    // 扣减成功,发布库存扣减事件,触发支付
                    eventPublisher.publishStockDeducted(new StockDeductedEvent(event.getOrderId()));
                } else {
                    // 扣减失败,发布失败事件,触发订单取消
                    eventPublisher.publishStockDeductFailed(new StockDeductFailedEvent(event.getOrderId()));
                }
            } catch (Exception e) {
                // 异常处理,发布失败事件
                eventPublisher.publishStockDeductFailed(new StockDeductFailedEvent(event.getOrderId()));
            }
        };
    }

    // 补偿事务:恢复库存(C2)
    @Transactional
    public void restoreStock(Long productId, Integer count) {
        stockMapper.increaseStock(productId, count);
    }
}
  1. 支付服务(监听库存扣减事件,执行支付):
@Service
public class PaymentService {
    @Autowired
    private PaymentMapper paymentMapper;
    @Autowired
    private EventPublisher eventPublisher;
    @Autowired
    private StockFeignClient stockFeignClient; // 调用库存服务补偿

    // 监听库存扣减事件,执行支付(T3)
    @Bean
    public Consumer<StockDeductedEvent> handleStockDeducted() {
        return event -> {
            try {
                // 模拟支付处理
                Payment payment = new Payment();
                payment.setOrderId(event.getOrderId());
                payment.setAmount(calculateAmount(event.getOrderId())); // 计算金额
                payment.setStatus(1); // 1-支付成功
                paymentMapper.insert(payment);

                // 发布支付完成事件,订单服务更新状态
                eventPublisher.publishPaymentCompleted(new PaymentCompletedEvent(event.getOrderId()));
            } catch (Exception e) {
                // 支付失败,调用库存服务补偿(恢复库存)
                stockFeignClient.restoreStock(event.getProductId(), event.getCount());
                // 发布支付失败事件,订单服务取消订单
                eventPublisher.publishPaymentFailed(new PaymentFailedEvent(event.getOrderId()));
            }
        };
    }
}

步骤3:处理补偿逻辑
订单服务监听失败事件,执行补偿:

@Service
public class OrderEventHandler {
    @Autowired
    private OrderService orderService;

    // 监听库存扣减失败事件,取消订单
    @Bean
    public Consumer<StockDeductFailedEvent> handleStockDeductFailed() {
        return event -> orderService.cancelOrder(event.getOrderId());
    }

    // 监听支付失败事件,取消订单
    @Bean
    public Consumer<PaymentFailedEvent> handlePaymentFailed() {
        return event -> orderService.cancelOrder(event.getOrderId());
    }
}
2.3.3 优缺点与适用场景
  • 优点
    • 适合长事务和多服务场景(如电商下单涉及5+服务);
    • 无锁阻塞,性能好,可用性高;
  • 缺点
    • 实现复杂:需设计事件交互和补偿逻辑;
    • 中间状态可见:用户可能看到“订单创建中→库存扣减中→支付中”等中间状态;
  • 适用场景:长事务、多服务协同(如电商订单、物流跟踪),可接受中间状态。

2.4 本地消息表(Local Message Table)

本地消息表是一种基于数据库的可靠消息方案,通过在本地事务中记录消息,保证消息的可靠发送,从而实现最终一致性。

2.4.1 原理
  1. 本地事务:在发起方服务的本地事务中,同时完成业务操作和消息记录(写入本地消息表);
  2. 消息发送:通过定时任务扫描本地消息表,将“待发送”状态的消息发送到消息队列;
  3. 消息消费:接收方消费消息并执行本地事务,完成后通知发起方更新消息状态;
  4. 失败重试:若消息发送或消费失败,定时任务重复重试,直到成功。
2.4.2 代码示例:基于MySQL本地消息表

以“订单创建后通知库存扣减”为例,实现本地消息表方案。

步骤1:创建本地消息表
在订单服务的数据库中创建消息表:

CREATE TABLE `local_message` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `message_id` varchar(64) NOT NULL COMMENT '消息唯一ID',
  `content` text NOT NULL COMMENT '消息内容(JSON)',
  `topic` varchar(128) NOT NULL COMMENT '消息队列主题',
  `status` tinyint NOT NULL COMMENT '状态:0-待发送,1-已发送,2-已消费,3-失败',
  `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数',
  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_message_id` (`message_id`)
) ENGINE=InnoDB COMMENT='本地消息表';

步骤2:订单服务实现(发起方)

  1. 业务逻辑与消息记录
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private LocalMessageMapper messageMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 创建订单并记录消息(本地事务)
    @Transactional
    public void createOrder(Long productId, Integer count) {
        // 1. 创建订单
        String orderId = UUID.randomUUID().toString();
        Order order = new Order();
        order.setId(orderId);
        order.setProductId(productId);
        order.setCount(count);
        orderMapper.insert(order);

        // 2. 记录消息到本地消息表(与订单创建在同一事务)
        String messageId = UUID.randomUUID().toString();
        StockDeductMessage message = new StockDeductMessage(orderId, productId, count);
        LocalMessage localMessage = new LocalMessage();
        localMessage.setMessageId(messageId);
        localMessage.setContent(JSON.toJSONString(message));
        localMessage.setTopic("stock-deduct-topic");
        localMessage.setStatus(0); // 待发送
        localMessage.setCreateTime(LocalDateTime.now());
        localMessage.setUpdateTime(LocalDateTime.now());
        messageMapper.insert(localMessage);
    }

    // 定时任务:发送未成功的消息
    @Scheduled(cron = "0 */1 * * * ?") // 每分钟执行一次
    public void sendPendingMessages() {
        // 查询状态为0(待发送)且重试次数<3的消息
        List<LocalMessage> messages = messageMapper.selectPendingMessages(3);
        for (LocalMessage msg : messages) {
            try {
                // 发送消息到RabbitMQ
                rabbitTemplate.convertAndSend(msg.getTopic(), msg.getContent());
                // 发送成功,更新状态为1(已发送)
                msg.setStatus(1);
                msg.setRetryCount(msg.getRetryCount() + 1);
                msg.setUpdateTime(LocalDateTime.now());
                messageMapper.updateById(msg);
            } catch (Exception e) {
                // 发送失败,更新重试次数
                msg.setRetryCount(msg.getRetryCount() + 1);
                if (msg.getRetryCount() >= 3) {
                    msg.setStatus(3); // 标记为失败
                }
                msg.setUpdateTime(LocalDateTime.now());
                messageMapper.updateById(msg);
            }
        }
    }
}

步骤3:库存服务实现(接收方)

@Service
public class StockService {
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private ConsumedMessageMapper consumedMsgMapper; // 记录已消费消息(防重复)

    // 监听消息并扣减库存
    @RabbitListener(queues = "stock-deduct-queue")
    public void handleStockDeduct(String messageJson) {
        StockDeductMessage message = JSON.parseObject(messageJson, StockDeductMessage.class);
        String messageId = message.getMessageId();

        // 1. 幂等性检查:判断消息是否已消费
        if (consumedMsgMapper.exists(messageId)) {
            return;
        }

        // 2. 执行库存扣减(本地事务)
        try {
            int rows = stockMapper.deductStock(message.getProductId(), message.getCount());
            if (rows > 0) {
                // 扣减成功,记录已消费消息
                consumedMsgMapper.insert(new ConsumedMessage(messageId));
                // 发送消息确认(可选,通知订单服务)
                rabbitTemplate.convertAndSend("message-confirm-topic", messageId);
            }
        } catch (Exception e) {
            // 消费失败,消息会被RabbitMQ重新投递(需配置重试机制)
            throw new AmqpRejectAndDontRequeueException("库存扣减失败,拒绝重新入队", e);
        }
    }
}
2.4.3 优缺点与适用场景
  • 优点
    • 实现简单:基于数据库和消息队列,无需引入复杂框架;
    • 可靠性高:本地事务保证消息必被记录,定时任务保证消息必被发送;
  • 缺点
    • 侵入性:需在业务表外额外维护消息表;
    • 性能:定时任务扫描表可能影响数据库性能;
  • 适用场景:中小规模应用,对一致性要求不高但需可靠消息传递(如通知、日志同步)。

2.5 事务消息(Transactional Message)

事务消息是消息队列提供的高级特性(如RocketMQ、阿里云ONS),通过消息队列本身保证消息的最终一致性,简化分布式事务实现。

2.5.1 原理

事务消息引入“半消息”概念,解决消息发送与本地事务的原子性问题:

  1. 发送半消息:发起方发送“半消息”到消息队列(此时接收方无法消费);
  2. 执行本地事务:发起方执行本地业务逻辑;
  3. 确认消息
    • 本地事务成功:通知消息队列将半消息标记为“可消费”,接收方可消费;
    • 本地事务失败:通知消息队列删除半消息,接收方不会收到;
  4. 回查机制:若消息队列未收到确认,会定时回查发起方的事务状态,决定消息是否可消费。
2.5.2 代码示例:基于RocketMQ事务消息

以“订单创建后扣减库存”为例,使用RocketMQ的事务消息功能。

步骤1:添加RocketMQ依赖

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.3</version>
</dependency>

步骤2:配置RocketMQ

rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: order-producer-group # 生产者组

步骤3:订单服务(发起方)实现

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    // 创建订单并发送事务消息
    public void createOrder(Long productId, Integer count) {
        // 1. 准备消息内容
        String orderId = UUID.randomUUID().toString();
        StockDeductMessage message = new StockDeductMessage(orderId, productId, count);
        
        // 2. 发送事务消息:topic + tag,消息内容,额外参数(用于回查)
        rocketMQTemplate.sendMessageInTransaction(
            "stock-deduct-topic:deduct", // topic:tag
            MessageBuilder.withPayload(message).build(),
            orderId // 额外参数:订单ID,用于本地事务和回查
        );
    }

    // 实现RocketMQ事务监听器:执行本地事务和回查
    @RocketMQTransactionListener
    public class OrderTransactionListener implements RocketMQLocalTransactionListener {
        @Autowired
        private OrderMapper orderMapper;

        // 执行本地事务
        @Override
        public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
            String orderId = (String) arg;
            StockDeductMessage message = JSON.parseObject(new String((byte[]) msg.getPayload()), StockDeductMessage.class);
            
            try {
                // 创建订单(本地事务)
                Order order = new Order();
                order.setId(orderId);
                order.setProductId(message.getProductId());
                order.setCount(message.getCount());
                orderMapper.insert(order);
                // 本地事务成功,提交消息
                return RocketMQLocalTransactionState.COMMIT;
            } catch (Exception e) {
                // 本地事务失败,回滚消息
                return RocketMQLocalTransactionState.ROLLBACK;
            }
        }

        // 回查事务状态(消息队列未收到确认时调用)
        @Override
        public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
            StockDeductMessage message = JSON.parseObject(new String((byte[]) msg.getPayload()), StockDeductMessage.class);
            // 检查订单是否存在(判断本地事务是否成功)
            Order order = orderMapper.selectById(message.getOrderId());
            if (order != null) {
                return RocketMQLocalTransactionState.COMMIT;
            } else {
                return RocketMQLocalTransactionState.ROLLBACK;
            }
        }
    }
}

步骤4:库存服务(接收方)实现

@Service
public class StockService {
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private ConsumedMessageMapper consumedMsgMapper;

    // 监听事务消息,扣减库存
    @RocketMQMessageListener(
        topic = "stock-deduct-topic",
        consumerGroup = "stock-consumer-group",
        selectorExpression = "deduct"
    )
    public class StockConsumer implements RocketMQListener<StockDeductMessage> {
        @Override
        public void onMessage(StockDeductMessage message) {
            // 幂等性检查
            if (consumedMsgMapper.exists(message.getMessageId())) {
                return;
            }

            // 扣减库存
            int rows = stockMapper.deductStock(message.getProductId(), message.getCount());
            if (rows > 0) {
                consumedMsgMapper.insert(new ConsumedMessage(message.getMessageId()));
            } else {
                // 库存不足,需人工干预或触发补偿(如取消订单)
                throw new RuntimeException("库存不足");
            }
        }
    }
}
2.5.3 优缺点与适用场景
  • 优点
    • 可靠性高:消息队列保证消息不丢失,回查机制解决超时问题;
    • 侵入性低:无需手动维护消息表,框架层处理事务协调;
  • 缺点
    • 依赖特定消息队列:仅RocketMQ等少数队列支持事务消息;
    • 回查机制增加复杂度:需实现事务状态回查接口;
  • 适用场景:中大规模应用,已使用RocketMQ等支持事务消息的队列(如电商、支付系统)。

三、分布式事务方案对比与选择

不同分布式事务方案各有优劣,选择时需结合业务场景、性能要求、开发成本等因素综合判断。

方案一致性性能开发成本适用场景典型框架/组件
2PC(Seata AT)强一致性短事务、一致性要求极高(金融支付)Seata
TCC最终一致性核心业务、性能要求高(库存扣减)Seata TCC、Hmily
Saga最终一致性长事务、多服务协同(电商下单)Axon、Seata Saga
本地消息表最终一致性中小应用、消息可靠传递(通知、日志)自定义实现(MySQL+MQ)
事务消息最终一致性已使用RocketMQ的场景(电商、支付)RocketMQ、阿里云ONS

选择建议

  1. 优先通过业务设计避免分布式事务(如合并服务、异步化非核心流程);
  2. 一致性要求极高(如金融交易):选2PC(Seata AT);
  3. 核心业务、性能优先(如库存扣减):选TCC;
  4. 多服务长事务(如电商下单):选Saga;
  5. 已集成RocketMQ:选事务消息;
  6. 中小应用、快速实现:选本地消息表。

四、分布式事务最佳实践

无论选择哪种方案,都需注意以下实践原则,避免常见问题:

4.1 保证幂等性

分布式事务中,由于网络重试、消息重投等,同一个操作可能被执行多次,必须保证幂等性(多次执行结果一致)。

实现方式:

  • 唯一标识:为每个操作生成唯一ID(如订单ID、消息ID),通过数据库唯一索引确保重复操作不生效;
  • 状态机设计:将业务状态设计为有限状态机(如订单:待支付→支付中→已支付),避免重复处理;
  • 防重放令牌:调用方生成令牌,服务方验证令牌有效性,使用后失效。

4.2 处理超时与重试

网络超时是分布式系统的常态,需设计合理的重试机制:

  • 重试策略:使用指数退避重试(如1s、2s、4s),避免重试风暴;
  • 重试上限:设置最大重试次数(如3次),超过后标记为失败,人工干预;
  • 异步重试:非核心流程使用异步重试(如定时任务),不阻塞主流程。

4.3 日志与监控

分布式事务问题排查复杂,需完善日志和监控:

  • 全链路日志:记录全局事务ID(xid),串联各服务日志,便于追踪完整流程;
  • 状态监控:监控事务成功率、重试次数、失败率,设置告警阈值(如失败率>1%告警);
  • 补偿监控:监控补偿事务的执行情况,确保失败的事务被及时补偿。

4.4 避免大事务

分布式事务范围越大,失败概率越高,应尽量拆分:

  • 拆分事务:将大事务拆分为多个小事务,减少参与方;
  • 异步化:非核心步骤异步处理(如订单创建后异步发送通知);
  • 缩短事务时间:避免在事务中执行耗时操作(如远程调用、大量计算)。

五、总结

分布式事务是微服务架构中的核心难题,没有“银弹”方案,需根据业务场景选择合适的实现方式:

  • 强一致性方案(如2PC)适合对数据一致性要求极高的场景,但牺牲性能;
  • 最终一致性方案(如TCC、Saga、事务消息)性能更优,是微服务的主流选择,需接受短暂的数据不一致并通过补偿机制保证最终一致。

实际项目中,优先通过业务设计减少分布式事务(如单库多表、异步化),必须使用时,需结合幂等性、重试机制、监控告警等保障措施,确保系统的可靠性和可维护性。

随着云原生技术的发展,Service Mesh(如Istio)也开始提供分布式事务解决方案,未来分布式事务的实现将更加透明化、低侵入化,但核心思想仍离不开本文介绍的理论和实践原则。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值