前言
之前学习的消息中间件主要是RabbitMQ,但是实习的公司用的却是RocketMQ和Kafka,虽然说消息中间件主要的思想是大差不差的,但是每个消息中间件有自己的优势和适合自己的业务场景,就不会像spring一样一家独大。所以我这段时间学了一下🚀MQ,学习到了一个事务消息机制(感觉很有用)分享给大家。
🚀MQ事务消息机制的作用
首先,我们要知道🚀MQ事务消息机制的作用:确保在分布式系统中,消息的发送与业务操作能够保持一致,也就是解决分布式事务不一致问题。下面举个我们常见的下单例子:
用户支付订单这一核心操作后往往会涉及到下游服务的物流发货、清空购物车、积分变更、库存扣减等一系列的后续操作。
需要注意的是库存扣减这类关键业务操作通常有很多种方案,比如预扣库存机制,既能防止超卖,用户体验还好,这里实现起来比较复杂,我下面的例子就不带库存扣减了。
为什么使用🚀MQ事务消息
考虑到事务的安全性,既要保证相关的这几个业务一定是同时成功或者同时失败的。如果要将这几个服务一起作为一个分布式事务来控制,可以通过seata来做到,但是这种同步的方式,性能是很差的,下单这个业务要等这一系列的操作完了才能响应成功,这显然用户体验是很差的。
这个时候我们就可以考虑使用MQ的异步的方式将后续的这一系列操作串联起来,由于🚀MQ与消费者端有失败重试机制,所以只要消息成功发送到了MQ,那么就可以认为后续的这一系列操作是可以保证最终一致性的。这样一个复杂的分布式事务问题,就变成了用户下单和发异步消息这两个步骤的分布式事务问题了,岂不美哉!!!
如何实现🚀MQ事务消息(以上面的下单业务为例)
如下图所示:
- 发送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){
//物流发货、清空购物车、积分变更等一系列操作逻辑
...
}
}
以上代码可能有一些小瑕疵,还请大伙见谅。