布式事务详解:从理论到实践(RocketMQ + Seata)

分布式事务详解:从理论到实践(RocketMQ + Seata)

前言

在微服务架构日益普及的今天,分布式事务成为了每个后端开发者必须面对的挑战。本文将从理论到实践,深入探讨分布式事务的解决方案,并结合 RocketMQ 和 Seata 框架给出具体的代码示例。


一、为什么需要分布式事务?

1.1 单体架构 vs 微服务架构

lua 复制代码
+------------------+          +------------------+
|    单体应用       |          |    微服务架构     |
+------------------+          +------------------+
|                  |          |                  |
|  +------------+  |          |  +-----------+   |
|  |   订单     |  |          |  | 订单服务  |   |
|  +------------+  |          |  +-----------+   |
|        |         |          |       |          |
|  +------------+  |          |  +-----------+   |
|  |   库存     |  |          |  | 库存服务  |   |
|  +------------+  |          |  +-----------+   |
|        |         |          |       |          |
|  +------------+  |          |  +-----------+   |
|  |   账户     |  |          |  | 账户服务  |   |
|  +------------+  |          |  +-----------+   |
|        |         |          |       |          |
|  +------------+  |          |  +-----------+   |
|  |  单一数据库 |  |          |  | 各自数据库 |   |
|  +------------+  |          |  +-----------+   |
+------------------+          +------------------+
     本地事务                     分布式事务

1.2 典型业务场景

以电商下单为例,一个完整的下单流程涉及:

diff 复制代码
用户下单流程:
+--------+    +--------+    +--------+    +--------+
|  创建  | -> |  扣减  | -> |  扣减  | -> |  发送  |
|  订单  |    |  库存  |    |  余额  |    |  通知  |
+--------+    +--------+    +--------+    +--------+
    |             |             |             |
    v             v             v             v
+--------+    +--------+    +--------+    +--------+
| 订单DB |    | 库存DB |    | 账户DB |    | 消息队列|
+--------+    +--------+    +--------+    +--------+

问题来了: 如果扣减余额失败,但订单已创建、库存已扣减,如何保证数据一致性?

1.3 分布式事务的核心挑战

  1. 网络不可靠:服务间调用可能超时、失败
  2. 服务不可用:某个服务可能宕机
  3. 数据不一致:部分操作成功,部分失败
  4. 并发问题:多个事务同时操作同一资源

二、分布式事务理论基础

2.1 CAP 理论

markdown 复制代码
                    C (Consistency)
                   一致性
                    /\
                   /  \
                  /    \
                 /      \
                /   CAP  \
               /    定理   \
              /____________\
             /              \
   A (Availability)    P (Partition Tolerance)
      可用性                 分区容错性

CAP定理:三者只能同时满足两个
- CA:放弃分区容错(单机系统)
- CP:放弃可用性(强一致性系统)
- AP:放弃一致性(高可用系统)

2.2 BASE 理论

  • Basically Available(基本可用):允许损失部分可用性
  • Soft State(软状态):允许存在中间状态
  • Eventually Consistent(最终一致性):经过一段时间后达到一致

2.3 常见一致性模型对比

模型 描述 适用场景
强一致性 写入后立即可读 金融交易
弱一致性 不保证何时能读到 日志记录
最终一致性 一段时间后一致 电商订单

三、分布式事务解决方案

3.1 方案对比

diff 复制代码
+------------------+----------+----------+----------+----------+
|      方案        | 一致性   | 性能     | 复杂度   | 适用场景 |
+------------------+----------+----------+----------+----------+
| 2PC/3PC          | 强       | 低       | 中       | 数据库   |
| TCC              | 强       | 中       | 高       | 资金类   |
| 本地消息表       | 最终     | 高       | 中       | 通用     |
| 事务消息         | 最终     | 高       | 低       | 通用     |
| Saga             | 最终     | 高       | 中       | 长事务   |
| Seata AT         | 强       | 中       | 低       | 通用     |
+------------------+----------+----------+----------+----------+

3.2 两阶段提交(2PC)

sql 复制代码
      协调者                    参与者A                  参与者B
        |                         |                        |
        |     1. Prepare请求      |                        |
        |------------------------>|                        |
        |                         |                        |
        |     1. Prepare请求      |                        |
        |------------------------------------------------->|
        |                         |                        |
        |     2. 准备就绪/失败    |                        |
        |<------------------------|                        |
        |                         |                        |
        |     2. 准备就绪/失败    |                        |
        |<-------------------------------------------------|
        |                         |                        |
        |     3. Commit/Rollback  |                        |
        |------------------------>|                        |
        |                         |                        |
        |     3. Commit/Rollback  |                        |
        |------------------------------------------------->|
        |                         |                        |

缺点:同步阻塞、单点故障、数据不一致风险

3.3 TCC(Try-Confirm-Cancel)

lua 复制代码
+----------------------------------------------------------+
|                        TCC 模式                           |
+----------------------------------------------------------+
|                                                          |
|   Try阶段          Confirm阶段        Cancel阶段          |
|   (资源预留)       (确认提交)         (回滚释放)           |
|                                                          |
|   +----------+     +----------+      +----------+        |
|   | 冻结库存 |     | 扣减库存 |      | 解冻库存 |        |
|   +----------+     +----------+      +----------+        |
|        |                |                 |              |
|   +----------+     +----------+      +----------+        |
|   | 冻结余额 |     | 扣减余额 |      | 解冻余额 |        |
|   +----------+     +----------+      +----------+        |
|                                                          |
+----------------------------------------------------------+

四、Seata 分布式事务框架

4.1 Seata 架构

lua 复制代码
+------------------------------------------------------------------+
|                         Seata 架构                                |
+------------------------------------------------------------------+
|                                                                   |
|                      +---------------+                            |
|                      |  TC (Server)  |                            |
|                      | 事务协调者    |                            |
|                      +---------------+                            |
|                       /             \                             |
|                      /               \                            |
|            +--------+                 +--------+                  |
|            |   TM   |                 |   RM   |                  |
|            | 事务   |                 | 资源   |                  |
|            | 管理器 |                 | 管理器 |                  |
|            +--------+                 +--------+                  |
|                |                           |                      |
|           +---------+                 +---------+                 |
|           | 业务服务 |                 | 数据库  |                 |
|           +---------+                 +---------+                 |
|                                                                   |
+------------------------------------------------------------------+

TC (Transaction Coordinator): 事务协调者,维护全局和分支事务状态
TM (Transaction Manager): 事务管理器,定义全局事务范围
RM (Resource Manager): 资源管理器,管理分支事务资源

4.2 Seata 四种模式

diff 复制代码
+--------+--------+--------+--------+
|   AT   |  TCC   |  Saga  |   XA   |
+--------+--------+--------+--------+
| 自动   | 手动   | 长事务 | 强一致 |
| 补偿   | 补偿   | 补偿   | 两阶段 |
| 简单   | 复杂   | 中等   | 简单   |
| 最常用 | 资金类 | 复杂流程| 数据库 |
+--------+--------+--------+--------+

4.3 Seata AT 模式实战

4.3.1 项目结构
bash 复制代码
seata-demo/
├── order-service/          # 订单服务
├── storage-service/        # 库存服务
├── account-service/        # 账户服务
└── business-service/       # 业务入口服务
4.3.2 Maven 依赖
xml 复制代码
<!-- pom.xml -->
<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Seata -->
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.7.0</version>
    </dependency>

    <!-- Seata 与 Spring Cloud Alibaba 集成 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2022.0.0.0</version>
    </dependency>

    <!-- MyBatis Plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3</version>
    </dependency>

    <!-- MySQL -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
</dependencies>
4.3.3 配置文件
yaml 复制代码
# application.yml
server:
  port: 8080

spring:
  application:
    name: business-service
  datasource:
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

# Seata 配置
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: ""
      group: SEATA_GROUP
  config:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: ""
      group: SEATA_GROUP
4.3.4 数据库表结构
sql 复制代码
-- 订单数据库
CREATE DATABASE seata_order;
USE seata_order;

CREATE TABLE `t_order` (
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户ID',
    `product_id` BIGINT(11) DEFAULT NULL COMMENT '商品ID',
    `count` INT(11) DEFAULT NULL COMMENT '数量',
    `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
    `status` INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完成',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- Seata AT 模式需要的 undo_log 表
CREATE TABLE `undo_log` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `branch_id` BIGINT(20) NOT NULL,
    `xid` VARCHAR(100) NOT NULL,
    `context` VARCHAR(128) NOT NULL,
    `rollback_info` LONGBLOB NOT NULL,
    `log_status` INT(11) NOT NULL,
    `log_created` DATETIME NOT NULL,
    `log_modified` DATETIME NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 库存数据库
CREATE DATABASE seata_storage;
USE seata_storage;

CREATE TABLE `t_storage` (
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT,
    `product_id` BIGINT(11) DEFAULT NULL COMMENT '商品ID',
    `total` INT(11) DEFAULT NULL COMMENT '总库存',
    `used` INT(11) DEFAULT NULL COMMENT '已用库存',
    `residue` INT(11) DEFAULT NULL COMMENT '剩余库存',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO t_storage VALUES(1, 1, 100, 0, 100);

-- 账户数据库
CREATE DATABASE seata_account;
USE seata_account;

CREATE TABLE `t_account` (
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT,
    `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户ID',
    `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
    `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',
    `residue` DECIMAL(10,0) DEFAULT NULL COMMENT '剩余额度',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO t_account VALUES(1, 1, 1000, 0, 1000);
4.3.5 核心代码实现

订单实体类

java 复制代码
@Data
@TableName("t_order")
public class Order {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status;
}

订单服务接口

java 复制代码
public interface OrderService {
    /**
     * 创建订单
     */
    void createOrder(Long userId, Long productId, Integer count, BigDecimal money);
}

订单服务实现

java 复制代码
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private StorageClient storageClient;

    @Autowired
    private AccountClient accountClient;

    @Override
    public void createOrder(Long userId, Long productId, Integer count, BigDecimal money) {
        log.info("========== 开始创建订单 ==========");

        // 1. 创建订单
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setMoney(money);
        order.setStatus(0);
        orderMapper.insert(order);

        // 2. 扣减库存
        log.info("========== 订单服务调用库存服务,扣减库存 ==========");
        storageClient.decrease(productId, count);

        // 3. 扣减账户余额
        log.info("========== 订单服务调用账户服务,扣减余额 ==========");
        accountClient.decrease(userId, money);

        // 4. 修改订单状态
        log.info("========== 修改订单状态 ==========");
        order.setStatus(1);
        orderMapper.updateById(order);

        log.info("========== 订单创建完成 ==========");
    }
}

库存服务 Feign 客户端

java 复制代码
@FeignClient(value = "storage-service")
public interface StorageClient {

    @PostMapping("/storage/decrease")
    void decrease(@RequestParam("productId") Long productId,
                  @RequestParam("count") Integer count);
}

账户服务 Feign 客户端

java 复制代码
@FeignClient(value = "account-service")
public interface AccountClient {

    @PostMapping("/account/decrease")
    void decrease(@RequestParam("userId") Long userId,
                  @RequestParam("money") BigDecimal money);
}

库存服务实现

java 复制代码
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

    @Autowired
    private StorageMapper storageMapper;

    @Override
    public void decrease(Long productId, Integer count) {
        log.info("========== 扣减库存开始 ==========");
        storageMapper.decrease(productId, count);
        log.info("========== 扣减库存结束 ==========");
    }
}

账户服务实现

java 复制代码
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("========== 扣减账户余额开始 ==========");

        // 模拟超时异常,测试分布式事务回滚
        // try {
        //     TimeUnit.SECONDS.sleep(20);
        // } catch (InterruptedException e) {
        //     e.printStackTrace();
        // }

        accountMapper.decrease(userId, money);
        log.info("========== 扣减账户余额结束 ==========");
    }
}

业务入口 - 使用 @GlobalTransactional 开启全局事务

java 复制代码
@RestController
@RequestMapping("/business")
@Slf4j
public class BusinessController {

    @Autowired
    private OrderService orderService;

    /**
     * 下单接口
     * @GlobalTransactional 开启 Seata 全局事务
     */
    @PostMapping("/purchase")
    @GlobalTransactional(name = "purchase-order", rollbackFor = Exception.class)
    public String purchase(@RequestParam Long userId,
                           @RequestParam Long productId,
                           @RequestParam Integer count,
                           @RequestParam BigDecimal money) {
        log.info("========== 开始全局事务,XID: {} ==========", RootContext.getXID());

        orderService.createOrder(userId, productId, count, money);

        return "下单成功";
    }
}

4.4 Seata TCC 模式实战

java 复制代码
/**
 * TCC 接口定义
 */
@LocalTCC
public interface AccountTccService {

    /**
     * Try: 冻结金额
     */
    @TwoPhaseBusinessAction(name = "accountTcc", commitMethod = "confirm", rollbackMethod = "cancel")
    boolean tryFreeze(@BusinessActionContextParameter(paramName = "userId") Long userId,
                      @BusinessActionContextParameter(paramName = "money") BigDecimal money);

    /**
     * Confirm: 确认扣款
     */
    boolean confirm(BusinessActionContext context);

    /**
     * Cancel: 解冻金额
     */
    boolean cancel(BusinessActionContext context);
}

/**
 * TCC 实现
 */
@Service
@Slf4j
public class AccountTccServiceImpl implements AccountTccService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    @Transactional
    public boolean tryFreeze(Long userId, BigDecimal money) {
        log.info("========== Try: 冻结金额 userId={}, money={} ==========", userId, money);

        // 检查余额是否充足
        Account account = accountMapper.selectByUserId(userId);
        if (account.getResidue().compareTo(money) < 0) {
            throw new RuntimeException("余额不足");
        }

        // 冻结金额:residue -= money, frozen += money
        accountMapper.freeze(userId, money);
        return true;
    }

    @Override
    @Transactional
    public boolean confirm(BusinessActionContext context) {
        Long userId = Long.valueOf(context.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(context.getActionContext("money").toString());

        log.info("========== Confirm: 确认扣款 userId={}, money={} ==========", userId, money);

        // 确认扣款:frozen -= money, used += money
        accountMapper.confirmDecrease(userId, money);
        return true;
    }

    @Override
    @Transactional
    public boolean cancel(BusinessActionContext context) {
        Long userId = Long.valueOf(context.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(context.getActionContext("money").toString());

        log.info("========== Cancel: 解冻金额 userId={}, money={} ==========", userId, money);

        // 解冻金额:frozen -= money, residue += money
        accountMapper.cancelFreeze(userId, money);
        return true;
    }
}

五、RocketMQ 事务消息

5.1 事务消息原理

sql 复制代码
+-----------------------------------------------------------------------+
|                     RocketMQ 事务消息流程                               |
+-----------------------------------------------------------------------+
|                                                                        |
|   生产者                         Broker                    消费者       |
|     |                              |                          |        |
|     |  1. 发送Half消息             |                          |        |
|     |----------------------------->|                          |        |
|     |                              |                          |        |
|     |  2. 返回发送结果             |                          |        |
|     |<-----------------------------|                          |        |
|     |                              |                          |        |
|     |  3. 执行本地事务             |                          |        |
|     |----+                         |                          |        |
|     |    |                         |                          |        |
|     |<---+                         |                          |        |
|     |                              |                          |        |
|     |  4. 提交/回滚事务状态        |                          |        |
|     |----------------------------->|                          |        |
|     |                              |                          |        |
|     |                              |  5. 投递消息(Commit)     |        |
|     |                              |------------------------->|        |
|     |                              |                          |        |
|     |  回查本地事务状态(超时未确认)  |                          |        |
|     |<-----------------------------|                          |        |
|     |                              |                          |        |
+-----------------------------------------------------------------------+

5.2 事务消息状态

sql 复制代码
+------------------+------------------+------------------+
|    COMMIT        |    ROLLBACK      |    UNKNOW        |
+------------------+------------------+------------------+
|  提交消息        |  回滚消息        |  未知状态        |
|  消费者可见      |  消息被删除      |  触发回查        |
+------------------+------------------+------------------+

5.3 Maven 依赖

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.3</version>
</dependency>

5.4 配置文件

yaml 复制代码
rocketmq:
  name-server: localhost:9876
  producer:
    group: tx-producer-group
    send-message-timeout: 3000

5.5 事务消息生产者

java 复制代码
@Service
@Slf4j
public class TransactionProducerService {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 发送事务消息
     */
    public void sendTransactionMessage(OrderDTO orderDTO) {
        String transactionId = UUID.randomUUID().toString();

        Message<OrderDTO> message = MessageBuilder
                .withPayload(orderDTO)
                .setHeader("transactionId", transactionId)
                .build();

        // 发送事务消息
        TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
                "order-topic:create",  // topic:tag
                message,
                orderDTO  // arg 参数,传递给本地事务执行器
        );

        log.info("事务消息发送结果: {}, transactionId: {}",
                result.getLocalTransactionState(), transactionId);
    }
}

5.6 事务监听器

java 复制代码
@RocketMQTransactionListener
@Slf4j
public class OrderTransactionListener implements RocketMQLocalTransactionListener {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private TransactionLogMapper transactionLogMapper;

    /**
     * 执行本地事务
     */
    @Override
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String transactionId = (String) msg.getHeaders().get("transactionId");
        OrderDTO orderDTO = (OrderDTO) arg;

        log.info("========== 执行本地事务,transactionId: {} ==========", transactionId);

        try {
            // 1. 创建订单
            Order order = new Order();
            order.setUserId(orderDTO.getUserId());
            order.setProductId(orderDTO.getProductId());
            order.setCount(orderDTO.getCount());
            order.setMoney(orderDTO.getMoney());
            order.setStatus(0);
            orderMapper.insert(order);

            // 2. 记录事务日志(用于回查)
            TransactionLog txLog = new TransactionLog();
            txLog.setTransactionId(transactionId);
            txLog.setOrderId(order.getId());
            txLog.setStatus(1);  // 1: 已提交
            transactionLogMapper.insert(txLog);

            log.info("本地事务执行成功,提交消息");
            return RocketMQLocalTransactionState.COMMIT;

        } catch (Exception e) {
            log.error("本地事务执行失败,回滚消息", e);
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    /**
     * 回查本地事务状态
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String transactionId = (String) msg.getHeaders().get("transactionId");

        log.info("========== 回查本地事务状态,transactionId: {} ==========", transactionId);

        // 查询事务日志
        TransactionLog txLog = transactionLogMapper.selectByTransactionId(transactionId);

        if (txLog != null && txLog.getStatus() == 1) {
            log.info("本地事务已提交");
            return RocketMQLocalTransactionState.COMMIT;
        }

        log.info("本地事务未提交,回滚消息");
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

5.7 事务消息消费者

java 复制代码
@Service
@RocketMQMessageListener(
        topic = "order-topic",
        selectorExpression = "create",
        consumerGroup = "order-consumer-group"
)
@Slf4j
public class OrderTransactionConsumer implements RocketMQListener<OrderDTO> {

    @Autowired
    private StorageService storageService;

    @Autowired
    private AccountService accountService;

    @Override
    public void onMessage(OrderDTO orderDTO) {
        log.info("========== 收到订单消息: {} ==========", orderDTO);

        try {
            // 扣减库存
            storageService.decrease(orderDTO.getProductId(), orderDTO.getCount());

            // 扣减账户余额
            accountService.decrease(orderDTO.getUserId(), orderDTO.getMoney());

            log.info("订单处理完成");

        } catch (Exception e) {
            log.error("订单处理失败", e);
            // 消费失败会自动重试
            throw new RuntimeException("订单处理失败", e);
        }
    }
}

六、本地消息表方案

6.1 架构图

lua 复制代码
+------------------------------------------------------------------+
|                      本地消息表方案                                |
+------------------------------------------------------------------+
|                                                                   |
|   服务A                                      服务B                 |
|   +------------------+                      +------------------+  |
|   |    业务操作      |                      |    业务操作      |  |
|   +--------+---------+                      +--------+---------+  |
|            |                                         ^            |
|            v                                         |            |
|   +------------------+                      +------------------+  |
|   |   本地消息表     |                      |   消息消费       |  |
|   +--------+---------+                      +--------+---------+  |
|            |                                         ^            |
|            |         消息队列                        |            |
|            |    +-----------------+                  |            |
|            +--->|     MQ          |------------------+            |
|                 +-----------------+                               |
|                         ^                                         |
|                         |                                         |
|                 +-----------------+                               |
|                 |   定时任务      |                               |
|                 |  (扫描未发送)   |                               |
|                 +-----------------+                               |
|                                                                   |
+------------------------------------------------------------------+

6.2 消息表结构

sql 复制代码
CREATE TABLE `local_message` (
    `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
    `message_id` VARCHAR(64) NOT NULL COMMENT '消息ID',
    `message_body` TEXT NOT NULL COMMENT '消息内容',
    `message_topic` VARCHAR(128) NOT NULL COMMENT '消息主题',
    `message_tag` VARCHAR(64) DEFAULT NULL COMMENT '消息标签',
    `status` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '状态: 0-待发送, 1-已发送, 2-已确认',
    `retry_count` INT(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_message_id` (`message_id`),
    KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

6.3 核心代码实现

java 复制代码
@Service
@Slf4j
public class LocalMessageService {

    @Autowired
    private LocalMessageMapper messageMapper;

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 保存本地消息(与业务操作在同一事务中)
     */
    @Transactional
    public void saveMessage(String topic, String tag, Object payload) {
        LocalMessage message = new LocalMessage();
        message.setMessageId(UUID.randomUUID().toString());
        message.setMessageTopic(topic);
        message.setMessageTag(tag);
        message.setMessageBody(JSON.toJSONString(payload));
        message.setStatus(0);
        message.setRetryCount(0);

        messageMapper.insert(message);
    }

    /**
     * 发送消息
     */
    public void sendMessage(LocalMessage message) {
        try {
            String destination = message.getMessageTag() != null
                    ? message.getMessageTopic() + ":" + message.getMessageTag()
                    : message.getMessageTopic();

            rocketMQTemplate.syncSend(destination, message.getMessageBody());

            // 更新状态为已发送
            message.setStatus(1);
            messageMapper.updateById(message);

            log.info("消息发送成功: {}", message.getMessageId());

        } catch (Exception e) {
            log.error("消息发送失败: {}", message.getMessageId(), e);

            // 增加重试次数
            message.setRetryCount(message.getRetryCount() + 1);
            messageMapper.updateById(message);
        }
    }

    /**
     * 确认消息(消费成功后调用)
     */
    public void confirmMessage(String messageId) {
        LocalMessage message = messageMapper.selectByMessageId(messageId);
        if (message != null) {
            message.setStatus(2);
            messageMapper.updateById(message);
        }
    }
}

6.4 定时任务扫描发送

java 复制代码
@Component
@Slf4j
public class MessageSendTask {

    @Autowired
    private LocalMessageMapper messageMapper;

    @Autowired
    private LocalMessageService messageService;

    /**
     * 每隔10秒扫描一次待发送消息
     */
    @Scheduled(fixedRate = 10000)
    public void scanAndSendMessage() {
        // 查询待发送的消息(status=0 且重试次数小于5)
        List<LocalMessage> messages = messageMapper.selectPendingMessages(5);

        for (LocalMessage message : messages) {
            log.info("扫描到待发送消息: {}", message.getMessageId());
            messageService.sendMessage(message);
        }
    }
}

七、最佳实践与选型建议

7.1 方案选型决策树

lua 复制代码
                        开始选型
                           |
                           v
                    +-------------+
                    | 是否需要    |
                    | 强一致性?   |
                    +------+------+
                           |
              +------------+------------+
              |                         |
              v                         v
             是                        否
              |                         |
              v                         v
      +-------+-------+         +-------+-------+
      | 业务能否      |         | 考虑最终      |
      | 接受性能损耗? |         | 一致性方案    |
      +-------+-------+         +-------+-------+
              |                         |
      +-------+-------+         +-------+-------+
      |               |         |               |
      v               v         v               v
     是              否      事务消息      本地消息表
      |               |         |               |
      v               v         v               v
  Seata AT        Seata TCC   RocketMQ      可靠性要求高

7.2 各方案适用场景

方案 适用场景 典型案例
Seata AT 常规业务、对一致性要求高 订单创建、库存扣减
Seata TCC 资金类业务、需要精确控制 转账、支付
RocketMQ事务消息 异步处理、最终一致性 订单通知、积分发放
本地消息表 可靠性要求高、不依赖MQ 跨系统数据同步
Saga 长事务、复杂业务流程 机票+酒店+租车预订

7.3 生产环境注意事项

css 复制代码
+------------------------------------------------------------------+
|                    生产环境 Checklist                             |
+------------------------------------------------------------------+
|                                                                   |
|  [ ] TC Server 高可用部署(至少3节点)                             |
|  [ ] 配置合理的事务超时时间                                        |
|  [ ] 监控全局事务状态                                              |
|  [ ] 处理悬挂事务                                                  |
|  [ ] 幂等性设计                                                   |
|  [ ] 异常重试机制                                                 |
|  [ ] 分布式锁防并发                                               |
|  [ ] 日志记录完整                                                 |
|                                                                   |
+------------------------------------------------------------------+

7.4 幂等性设计

java 复制代码
@Service
@Slf4j
public class IdempotentOrderService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private OrderMapper orderMapper;

    private static final String IDEMPOTENT_KEY_PREFIX = "order:idempotent:";

    /**
     * 幂等性创建订单
     */
    @Transactional
    public Order createOrderIdempotent(String requestId, OrderDTO orderDTO) {
        String key = IDEMPOTENT_KEY_PREFIX + requestId;

        // 1. 检查是否已处理
        Boolean setResult = redisTemplate.opsForValue()
                .setIfAbsent(key, "processing", Duration.ofMinutes(30));

        if (Boolean.FALSE.equals(setResult)) {
            // 已存在,查询已有订单返回
            log.info("重复请求,requestId: {}", requestId);
            Order existOrder = orderMapper.selectByRequestId(requestId);
            if (existOrder != null) {
                return existOrder;
            }
            throw new RuntimeException("订单处理中,请稍后查询");
        }

        try {
            // 2. 创建订单
            Order order = new Order();
            order.setRequestId(requestId);
            order.setUserId(orderDTO.getUserId());
            order.setProductId(orderDTO.getProductId());
            order.setCount(orderDTO.getCount());
            order.setMoney(orderDTO.getMoney());
            order.setStatus(0);
            orderMapper.insert(order);

            // 3. 更新 Redis 状态
            redisTemplate.opsForValue().set(key, "completed", Duration.ofHours(24));

            return order;

        } catch (Exception e) {
            // 失败则删除 key,允许重试
            redisTemplate.delete(key);
            throw e;
        }
    }
}

八、总结

分布式事务是微服务架构中的核心难题,没有银弹方案。选择合适的方案需要权衡:

  1. 一致性要求:强一致性选 Seata,最终一致性选事务消息
  2. 性能要求:高性能选异步方案,低延迟选 AT 模式
  3. 业务复杂度:简单业务用 AT,复杂业务用 TCC 或 Saga
  4. 运维成本:本地消息表最简单,Seata 需要维护 TC

核心原则:

  • 能不用分布式事务就不用:合理拆分服务边界
  • 优先考虑最终一致性:大多数业务可以接受
  • 做好幂等设计:分布式环境必备
  • 完善监控告警:及时发现和处理异常事务

希望本文能帮助你在实际项目中更好地应对分布式事务挑战!


相关推荐
i***48611 小时前
微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
java·spring cloud·微服务
zzlyx991 小时前
用C#采用Avalonia+Mapsui在离线地图上插入图片画信号扩散图
java·开发语言·c#
Aevget1 小时前
MyEclipse全新发布v2025.2——AI + Java 24 +更快的调试
java·ide·人工智能·eclipse·myeclipse
一 乐1 小时前
购物|明星周边商城|基于springboot的明星周边商城系统设计与实现(源码+数据库+文档)
java·数据库·spring boot·后端·spring
笃行客从不躺平1 小时前
线程池监控是什么
java·开发语言
y1y1z1 小时前
Spring框架教程
java·后端·spring
曾经的三心草2 小时前
基于正倒排索引的Java文档搜索引擎3-实现Index类-实现搜索模块-实现DocSearcher类
java·python·搜索引擎
l***46682 小时前
SSM与Springboot是什么关系? -----区别与联系
java·spring boot·后端
稚辉君.MCA_P8_Java2 小时前
Gemini永久会员 快速排序(Quick Sort) 基于分治思想的高效排序算法
java·linux·数据结构·spring·排序算法