欢迎光临
我们一直在努力

从原理到落地:本地消息表 + RocketMQ 分布式事务方案

如今的微服务架构中,分布式事务是保证跨服务数据一致性的核心难题。

本文采用本地消息表与RocketMQ 实现最终一致性分布式事务,解决电商下单场景中,订单创建和库存扣减两个不同服务分布式调用时,保证要么全部成功,要么全部失败的问题,全程不依赖复杂中间件,架构简单、生产可用,实现业务落地,其他的业务可以参考。


一、什么是本地消息表模式?

本地消息表(Local Message Table)是实现分布式事务的经典模式之一,也被称为事务消息模式,其核心思想可以概括为一句话:

用本地事务保证“业务操作 + 消息记录”的原子性,再通过消息重试机制实现跨服务的最终一致性。

1. 核心流程拆解

我们以“订单创建 + 库存扣减”场景为例,完整流程分为以下几步:

  • 订单服务开启本地事务:同时执行两个操作
    • 业务操作:创建订单,订单状态标记为“待确认”;
    • 消息操作:向本地消息表插入一条待发送的消息,记录库存扣减的关键信息,消息状态标记为“待发送”。
  • 本地事务提交:订单创建和消息插入同时成功或同时失败,确保订单存在时,对应的消息一定存在。
  • 事务提交后发送消息:订单服务向消息队列(如Kafka、RocketMQ)发送库存扣减消息,发送成功后更新本地消息表状态为“已发送”。
  • 库存服务消费消息:库存服务监听消息队列,收到消息后执行库存扣减操作,并向订单服务发送处理结果(成功/失败)。
  • 结果反馈与状态更新:订单服务收到库存扣减结果,更新订单状态和本地消息状态;若库存扣减失败,可触发订单回滚或人工补偿流程。
  • 定时任务兜底重试:启动定时任务,定期扫描本地消息表中“待发送”或“发送失败”的消息,重新投递,避免因网络波动导致的消息丢失。
  • 2. 模式核心优势

    • 无强依赖:不依赖复杂的分布式事务中间件(如Seata、XA),基于关系型数据库和消息队列即可实现,架构简单易落地;
    • 最终一致性保障:通过本地事务 + 消息重试 + 结果反馈的闭环,确保跨服务数据最终一致;
    • 高可用:即使消息队列短暂宕机、网络分区,定时任务兜底机制也能保证消息最终被处理;
    • 解耦性强:订单服务与库存服务通过消息异步通信,无需直接耦合调用,提升系统扩展性。

    二、相关表

    1. 订单表

    CREATE TABLE `t_order` (
    `id` bigint NOT NULL AUTO_INCREMENT,
    `order_no` varchar(64) NOT NULL COMMENT '订单编号',
    `user_id` bigint NOT NULL,
    `product_id` bigint NOT NULL,
    `quantity` int NOT NULL,
    `amount` decimal(10,2) NOT NULL,
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待确认 1已完成 2已取消',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_order_no` (`order_no`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

    2. 本地消息表

    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(64) NOT NULL COMMENT 'RocketMQ主题',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待发送 1已发送 2发送失败 3处理成功 4处理失败',
    `retry_count` int DEFAULT 0,
    `next_retry_time` datetime NOT NULL,
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    UNIQUE KEY `uk_message_id` (`message_id`),
    KEY `idx_status_retry` (`status`,`next_retry_time`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地消息表';


    三、代码实现

    1. 订单服务:创建订单 + 保存消息(事务保证原子性)

    @Service
    @Transactional(rollbackFor = Exception.class)
    public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private LocalMessageMapper localMessageMapper;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    private static final String TOPIC = "inventory-deduct-topic";

    @Override
    public void createOrder(OrderCreateDTO dto) {
    // 1. 创建订单
    Order order = buildOrder(dto);
    orderMapper.insert(order);

    // 2. 构造库存扣减消息
    InventoryMessage message = new InventoryMessage();
    message.setOrderId(order.getId());
    message.setOrderNo(order.getOrderNo());
    message.setProductId(dto.getProductId());
    message.setQuantity(dto.getQuantity());

    // 3. 插入本地消息(事务内)
    LocalMessage localMessage = buildLocalMessage(message);
    localMessageMapper.insert(localMessage);

    // 4. 事务提交后异步发送消息
    String messageId = localMessage.getMessageId();
    TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
    @Override
    public void afterCommit() {
    sendMessageAfterCommit(messageId, message);
    }
    }
    );
    }
    }

    2. 发送消息到 RocketMQ

    @Async
    public void sendMessageAfterCommit(String messageId, InventoryMessage message) {
    try {
    rocketMQTemplate.syncSend(TOPIC, MessageBuilder.withPayload(message).setHeader("messageId", messageId).build());
    // 更新消息状态为已发送
    localMessageMapper.updateStatus(messageId, 1);
    } catch (Exception e) {
    // 发送失败,等待定时任务重试
    localMessageMapper.updateStatus(messageId, 2);
    }
    }

    3. 库存服务:消费 RocketMQ 消息 + 扣库存(保证幂等)

    @Service
    @RocketMQMessageListener(topic = "inventory-deduct-topic", consumerGroup = "inventory-group")
    public class InventoryConsumer implements RocketMQListener<InventoryMessage> {

    @Autowired
    private InventoryService inventoryService;

    @Override
    public void onMessage(InventoryMessage message) {
    // 幂等判断:根据 messageId / orderId 避免重复扣减
    if (inventoryService.isProcessed(message.getMessageId())) {
    return;
    }
    // 扣减库存
    inventoryService.deductStock(message.getProductId(), message.getQuantity());
    // 标记已处理
    inventoryService.markProcessed(message.getMessageId());
    }
    }

    4. 定时任务:兜底重试失败消息

    @Component
    @Scheduled(cron = "0 */2 * * * ?")
    public class MessageRetryTask {

    @Autowired
    private LocalMessageMapper localMessageMapper;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void retrySend() {
    // 查询待重试消息
    List<LocalMessage> list = localMessageMapper.selectRetryMessages();
    for (LocalMessage msg : list) {
    try {
    rocketMQTemplate.syncSend(msg.getTopic(), msg.getContent());
    localMessageMapper.updateStatus(msg.getMessageId(), 1);
    } catch (Exception e) {
    localMessageMapper.increaseRetry(msg.getMessageId());
    }
    }
    }
    }


    四、生产避坑

    1. 幂等性问题:如何避免消息重复处理?

    本地消息表模式中,消息重试、网络波动都可能导致库存服务重复收到同一条消息,必须实现幂等性,保证一条消息无论被消费多少次,结果都只生效一次:

    • 方案1:基于 messageId 做幂等:库存服务处理消息前,先根据messageId查询处理状态,已处理过的消息直接返回成功;
    • 方案2:基于业务主键做幂等:库存扣减时,根据orderId判断是否已处理过该订单的扣减请求;
    • 方案3:使用分布式锁:处理消息时加分布式锁,保证同一消息同一时间只有一个线程在处理。

    示例代码(库存服务幂等处理):

    public boolean deductStock(Long productId, int quantity, Long orderId) {
    // 1. 幂等校验:查询该订单是否已扣减过库存
    if (inventoryDeductRecordMapper.existsByOrderId(orderId)) {
    log.info("订单{}已扣减过库存,无需重复处理", orderId);
    return true;
    }

    // 2. 扣减库存(乐观锁实现)
    int updateCount = inventoryMapper.deductStockWithLock(productId, quantity);
    if (updateCount == 0) {
    log.warn("商品{}库存不足,扣减失败", productId);
    return false;
    }

    // 3. 记录扣减记录,作为幂等标识
    InventoryDeductRecord record = new InventoryDeductRecord();
    record.setOrderId(orderId);
    record.setProductId(productId);
    record.setQuantity(quantity);
    record.setCreateTime(new Date());
    inventoryDeductRecordMapper.insert(record);
    return true;
    }

    2. 消息丢失问题:如何保证消息不丢?

    基于 RocketMQ 的分布式事务消息架构,想要做到零消息丢失,需要从三端同时保证:

    • 订单服务端:业务数据 + 本地消息在同一个本地事务中,确保消息一定落地;即使发送失败,定时任务也会持续重试,保证消息最终发出。
    • RocketMQ 服务端:使用同步刷盘 + 主从同步,消息必须落盘并同步到从节点才算发送成功,避免 Broker 宕机丢消息。
    • 消费端:使用 RocketMQ 集群消费模式 + 手动 ACK,只有业务真正执行成功后才返回 CONSUME_SUCCESS,消费异常时返回 RECONSUME_LATER,让消息重新进入队列重试,绝对不会丢消息。

    3. 消息堆积问题:如何处理大量未处理消息?

    在高并发场景下,若库存服务处理速度跟不上发送速度,会出现 RocketMQ 消息堆积,可通过以下方案解决:

    • 提高消费并行度:调整消费者线程数consumeThreadMin/consumeThreadMax,提升消费速度;
    • 重试策略优化:使用指数退避重试,避免消息频繁重试压垮服务;
    • 死信队列处理:消息重试超过 16 次(RocketMQ 默认最大重试次数)后自动进入死信队列,不再影响正常消息,由人工/补偿系统处理;
    • 优化消费逻辑:将耗时长的逻辑异步化,避免阻塞消费线程。

    4. 性能瓶颈优化

    基于 本地消息表 + RocketMQ 的架构,可从以下方向做生产级性能优化:

    • 本地消息表索引优化:为status和next_retry_time建立联合索引,大幅提升定时任务扫描效率;
    • 消息批量消费:开启 RocketMQ 批量消费,减少IO与数据库交互次数;
    • 异步发送消息:订单创建后使用异步线程发送,不阻塞主线程,提升接口吞吐量;
    • 消息发送超时控制:设置合理的sendMsgTimeout,避免发送阻塞导致业务线程等待。

    五、模式对比与适用场景

    1. 本地消息表 vs 其他分布式事务方案

    方案优点缺点适用场景
    本地消息表 实现简单、无强依赖、高可用 依赖定时任务兜底、存在最终一致性延迟 对实时性要求不高、对一致性要求较高的业务
    Seata AT模式 无侵入、性能好 依赖Seata Server、存在全局锁性能瓶颈 中低并发、对一致性要求高的业务
    TCC模式 性能高、一致性强 侵入性强、开发成本高 高并发、核心交易业务
    XA模式 强一致性 性能差、阻塞时间长 低并发、强一致性要求的场景

    2. 适用场景

    本地消息表模式特别适合以下场景:

    • 电商下单、订单支付、库存扣减等异步化业务场景;
    • 对实时性要求不高(秒级延迟可接受),但对数据一致性要求较高的业务;
    • 不想引入复杂分布式事务中间件,希望基于现有技术栈实现一致性的场景。

    六、总结与扩展

    本文基于本地消息表模式,完整实现了分布式事务解决方案,核心思路可以总结为:

  • 本地事务原子性:订单创建和消息记录在同一事务中,确保“订单存在则消息必存在”;
  • 消息异步通信:通过消息队列实现跨服务解耦,库存服务异步处理扣减操作;
  • 重试与结果反馈:定时任务兜底重试 + 库存处理结果反馈,确保消息最终被处理;
  • 幂等性保障:通过消息ID或业务主键实现幂等,避免重复处理导致的数据问题。
  • ← 上一篇 别再愁Java项目没亮点!普通 CRUD 项目其实也能征服面试官!!!

    记得点赞、关注、收藏哦!

    下一篇 JUC小册——公平锁和非公平锁 →
    赞(0)
    未经允许不得转载:171主机测评 » 从原理到落地:本地消息表 + RocketMQ 分布式事务方案
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址