🚀🚀🚀RocketMQ的事务消息机制你学废了吗?🚀🚀🚀

326 阅读5分钟

前言

之前学习的消息中间件主要是RabbitMQ,但是实习的公司用的却是RocketMQ和Kafka,虽然说消息中间件主要的思想是大差不差的,但是每个消息中间件有自己的优势和适合自己的业务场景,就不会像spring一样一家独大。所以我这段时间学了一下🚀MQ,学习到了一个事务消息机制(感觉很有用)分享给大家。

20220920224510_6fb14.gif

🚀MQ事务消息机制的作用

首先,我们要知道🚀MQ事务消息机制的作用:确保在分布式系统中,消息的发送与业务操作能够保持一致,也就是解决分布式事务不一致问题。下面举个我们常见的下单例子:

用户支付订单这一核心操作后往往会涉及到下游服务的物流发货、清空购物车、积分变更、库存扣减等一系列的后续操作。

需要注意的是库存扣减这类关键业务操作通常有很多种方案,比如预扣库存机制,既能防止超卖,用户体验还好,这里实现起来比较复杂,我下面的例子就不带库存扣减了。

为什么使用🚀MQ事务消息

考虑到事务的安全性,既要保证相关的这几个业务一定是同时成功或者同时失败的。如果要将这几个服务一起作为一个分布式事务来控制,可以通过seata来做到,但是这种同步的方式,性能是很差的,下单这个业务要等这一系列的操作完了才能响应成功,这显然用户体验是很差的。

这个时候我们就可以考虑使用MQ的异步的方式将后续的这一系列操作串联起来,由于🚀MQ与消费者端有失败重试机制,所以只要消息成功发送到了MQ,那么就可以认为后续的这一系列操作是可以保证最终一致性的。这样一个复杂的分布式事务问题,就变成了用户下单和发异步消息这两个步骤的分布式事务问题了,岂不美哉!!!

如何实现🚀MQ事务消息(以上面的下单业务为例)

如下图所示: Snipaste_2024-11-22_22-50-25.png

  • 发送half消息:交易系统在执行本地事务(如MySQL下单)之前,首先向RocketMQ发送一个half消息,这是一种预备消息,用于告知MQ系统事务的开始。
  • 回复half消息:RocketMQ接收到half消息后,会向交易系统回复一个确认,表示half消息已经成功接收。
  • 执行本地事务:交易系统在收到half消息的确认后,开始执行本地事务,如在MySQL中创建订单记录。
  • 返回本地事务状态:本地事务执行完毕后,交易系统会根据事务的执行结果返回一个状态给RocketMQ。如果本地事务执行成功,返回unknow状态;如果失败,则返回rollback状态。
  • 未确定状态的事务进行状态回查:如果交易系统在执行本地事务后返回了一个unknow状态,RocketMQ会根据配置的回查频率对支付系统进行状态回查,以确定本地事务的最终状态。
  • 检查本地事务状态:交易系统会定期检查本地事务的状态,这里是通过查询数据库来确认订单是否已经支付。
  • 返回本地事务检查状态:交易系统在检查本地事务状态后,会将结果返回给RocketMQ。如果事务状态为已支付,返回commit;如果事务状态为未支付,返回unknow,MQ会继续进行回查;如果事务状态超出回查次数了还一直是未支付,就会返回rollback,消息将被丢弃。
  • 事务消息提交成功后:其他各个下游服务的消费者消费消息,完成各自的操作逻辑。

那么具体的步骤了解了,下面是实现代码

1.下面是生产者示例:

@Service
public class TransactionProducer {

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    public void sendMessage(String message) {
        // 半消息发送
        SendResult sendResult = rocketMQTemplate.getProducer().sendMessageInTransaction("TransactionTopic",
                message.getBytes(), null, new Object[]{message});
        System.out.printf("%s%n", sendResult);
        //根据返回的结果做一些补救策略
        ...
     }
}

2.下面是监听器示例:

监听器是一个消费者的一部分,它负责监听特定主题上的消息。当消息到达时,监听器会触发并执行相应的回调方法来处理这些消息

注意:如果你的项目中要发多个事务消息,要有多个监听器容器,而这种用spring封装的api没有很好的区分不同的监听器,所以我们可以多创建一个RocketMQTemplate Bean对象,如下:

@ExtRocketMQTemplateConfiguration()
public class ExtRocketMQTemplate extends RocketMQTemplate{}

这样我们就可以在配置多个监听器时的@RocketMQTransactionListener()的注解中声明该监听器对应的rocketMQTemplate的BeanName属性了。这样我们在发送消息时区别不同的template就可以了,就可以很好的区分不同的监听器逻辑了

@Service
@RocketMQTransactionListener(rocketMQTemplateBeanName=“你对应rocketMQTemplate的BeanName”)
public class TransactionListener implements RocketMQLocalTransactionListener {

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message,Object arg) {
        // 这里模拟本地事务执行,保存订单到数据库
        try {
            // 假设保存订单成功
            System.out.println("保存订单成功,消息内容:" + message);
            // 返回unknow
            return LocalTransactionState.UNKNOW;
        } catch (Exception e) {
            // 异常,返回回滚事务
            System.out.println("保存订单失败,消息内容:" + message);
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        // 回查本地事务状态,检查订单支付状态
        String orderStatus = checkOrderStatus();
        if ("PAID".equals(orderStatus)) {
            // 如果订单已支付,返回提交事务,消息发送成功
            return LocalTransactionState.COMMIT_MESSAGE;
        } else if (orderStatus == null || "UNPAID".equals(orderStatus)) {
            // 如果订单未支付或状态未知,返回未知状态,稍后再次回查
            return LocalTransactionState.UNKNOW;
        } else {
            // 如果订单状态不是预期的,回滚事务
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }
}

2.下面是消费者示例:

@Component
@RocketMQMessageListener(consumerGroup="tansaction_consumer_group",topic="TransactionTopic",
messageModel=MessageModel.CLUSTERING
)
public class Listener implements RocketMQListener<String>{
       @Override
       public void onMessage(String message){
       //物流发货、清空购物车、积分变更等一系列操作逻辑
       ...
     }
}

以上代码可能有一些小瑕疵,还请大伙见谅。

d68289888877de874132f03753d481e1b65ce17012158d-Tm55nO_fw658.webp