【RocketMQ源码分析】事务消息

本文深入探讨了RocketMQ事务消息的原理与实践,通过具体案例分析了如何利用事务消息确保分布式系统中本地事务与消息发送的原子性。文章详细解释了事务消息的发送流程、提交机制以及回查流程,展示了如何在订单服务与商品服务间实现一致性的事务处理。

随着分布式服务架构的流行与普及,原来在单体应用中执行的多个逻辑操作,现在被拆分成了多个服务之间的远程调用。虽然服务化为我们的系统带来了水平伸缩的能力,然而随之而来挑战就是分布式事务问题。分布式事务目前主要的解决方案是:消息最终一致性、TCC等柔性事务的分布式事务方案,本文主要分析的是基于消息的最终一致性方案。

解决问题

拿下单送积分来说,生成订单记录 -> MQ -> 增加积分。
我们是应该先 创建订单记录,还是先 发送MQ消息 呢?

  • 先发送MQ消息:这个明显是不行的,因为如果消息发送成功,而订单创建失败的话是没办法把消息收回来的

  • 先创建订单记录:如果订单创建成功后MQ消息发送失败 抛出异常,因为两个操作都在本地事务中所以订单数据是可以 回滚 的

上面的 方式二 看似没问题,但是 网络是不可靠的!如果 MQ 的响应因为网络原因没有收到,所以在面对不确定的结果只好进行回滚;但是 MQ 端又确实是收到了这条消息的,只是回给客户端的 响应丢失 了!所以 事务消息 就是用来保证 本地事务 与 MQ消息发送 的原子性!

RocketMQ事务消息原理

在这里插入图片描述
主要的逻辑分为两个流程:

  • 事务消息发送及提交:
  1. 发送 half消息
  2. MQ服务端 响应消息写入结果
  3. 根据发送结果执行 本地事务(如果写入失败,此时half消息对业务 不可见,本地逻辑不执行)
  4. 根据本地事务状态执行 Commit 或者 Rollback(Commit操作生成消息索引,消息对消费者 可见)
  • 回查流程:
  1. 对于长时间没有 Commit/Rollback 的事务消息(pending 状态的消息),从服务端发起一次 回查
  2. Producer 收到回查消息,检查回查消息对应的 本地事务状态
    根据本地事务状态,重新 Commit 或者 Rollback
逻辑时序图

在这里插入图片描述

代码实现

下面以一个简单的场景模拟RocketMQ的事务消息:存在2个微服务,分别是订单服务和商品服务。订单服务进行下单处理,并发送消息给商品服务,对于下单成功的商品进行减库存。
订单服务:

public class OrderService {
  public static void main(String[] args) throws Exception {
    TransactionMQProducer producer = new TransactionMQProducer();
    producer.setNamesrvAddr(RocketMQConstants.NAMESRV_ADDR);
    producer.setProducerGroup(RocketMQConstants.TRANSACTION_PRODUCER_GROUP);

    //自定义线程池,执行事务操作
    ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 10L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20), (Runnable r) -> new Thread("Order Transaction Massage Thread"));
    producer.setExecutorService(executor);

    //设置事务消息监听器
    producer.setTransactionListener(new OrderTransactionListener());

    producer.start();

    System.err.println("OrderService Start");

    for (int i = 0;i < 10;i++){
      String orderId = UUID.randomUUID().toString();
      String payload = "下单,orderId: " + orderId;
      String tags = "Tag";
      Message message = new Message(RocketMQConstants.TRANSACTION_TOPIC_NAME, tags, orderId, payload.getBytes(RemotingHelper.DEFAULT_CHARSET));

      //发送事务消息
      TransactionSendResult result = producer.sendMessageInTransaction(message, orderId);
      System.err.println("发送事务消息,发送结果: " + result);
    }
  }
}

事务消息需要一个TransactionListener,主要进行本地事务的执行和事务回查,代码如下:

/**
 * @Description:订单事务消息监听器
 */
public class OrderTransactionListener implements TransactionListener {
  private static final Map<String, Boolean> results = new ConcurrentHashMap<>();

  @Override
  public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
    String orderId = (String) arg;

    //记录本地事务执行结果
    boolean success = persistTransactionResult(orderId);
    System.err.println("订单服务执行本地事务下单,orderId: " + orderId + ", result: " + success);
    return success ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
  }

  @Override
  public LocalTransactionState checkLocalTransaction(MessageExt msg) {
    String orderId = msg.getKeys();
    System.err.println("执行事务消息回查,orderId: " + orderId);
    return Boolean.TRUE.equals(results.get(orderId)) ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
  }

  private boolean persistTransactionResult(String orderId) {
    boolean success = Math.abs(Objects.hash(orderId)) % 2 == 0;
    results.put(orderId, success);
    return success;
  }
}


下面是商品服务及监听器:

/**
 * @Description:使用RocketMQ事务消息——商品服务接收下单的事务消息,如果消息成功commit则本地减库存
 */
public class ProductService {
  public static void main(String[] args) throws Exception {
    DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
    consumer.setNamesrvAddr(RocketMQConstants.NAMESRV_ADDR);
    consumer.setConsumerGroup(RocketMQConstants.TRANSACTION_CONSUMER_GROUP);
    consumer.subscribe(RocketMQConstants.TRANSACTION_TOPIC_NAME, "*");
    consumer.registerMessageListener(new ProductListener());
    consumer.start();
    System.err.println("ProductService Start");
  }
}

public class ProductListener implements MessageListenerConcurrently {
  @Override
  public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
    Optional.ofNullable(msgs).orElse(Collections.emptyList()).forEach(m -> {
      String orderId = m.getKeys();
      System.err.println("监听到下单消息,orderId: " + orderId + ", 商品服务减库存");
    });
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
  }
}

分别运行OrderService和ProductService,可以看出只有事务执行成功的订单才会通知商品服务进行减库存。

监听到下单消息,orderId: f25a7127-307e-45ce-8f83-6e0a922ebb94, 商品服务减库存
监听到下单消息,orderId: d960171d-97c0-4e13-aa4a-c2b96102de4b, 商品服务减库存
监听到下单消息,orderId: 63aedaa2-ce74-4cb7-bf58-fb6a73082a73, 商品服务减库存
监听到下单消息,orderId: 25764461-70b2-44db-8296-960211179e6e, 商品服务减库存
监听到下单消息,orderId: fb319fe7-c8be-4edf-ae4e-6108898068ca, 商品服务减库存
监听到下单消息,orderId: 4f61a61a-7254-458a-bc10-9d4006a9f581, 商品服务减库存

参考:
https://juejin.im/post/5d8882bdf265da03c9273821?utm_source=gold_browser_extension
https://juejin.im/post/5d88329af265da03951a3181?utm_source=gold_browser_extension

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值