Springboot&RocketMQ生产者发送事务消息
前置文章
RocketMQ环境准备
https://blog.csdn.net/guojing99/article/details/142262658
SpringBoot整合RocketMQ项目
https://blog.csdn.net/guojing99/article/details/142263509
Springboot&RocketMQ生产者发送消息
https://blog.csdn.net/guojing99/article/details/142264068
事务消息介绍
在一些对数据一致性有强需求的场景,可以用 Apache RocketMQ 事务消息来解决,从而保证上下游数据的一致性。
使用普通消息和订单事务无法保证一致的原因,本质上是由于普通消息无法像单机数据库事务一样,具备提交、回滚和统一协调的能力。 而基于 RocketMQ 的分布式事务消息功能,在普通消息基础上,支持二阶段的提交能力。将二阶段提交和本地事务绑定,实现全局提交结果的一致性。
事务消息发送分为两个阶段:
第一阶段会发送一个半事务消息,半事务消息是指暂不能投递的消息,生产者已经成功地将消息发送到了 Broker,但是Broker 未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,如果发送成功则执行本地事务,并根据本地事务执行成功与否,向 Broker 半事务消息状态(commit或者rollback),半事务消息只有 commit 状态才会真正向下游投递。
如果由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,Broker 端会通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback)。这样最终保证了本地事务执行成功,下游就能收到消息,本地事务执行失败,下游就收不到消息。总而保证了上下游数据的一致性。
整个事务消息的详细交互流程如下图所示:
事务消息步骤详解
事务消息发送步骤如下:
1.生产者将半事务消息发送至 RocketMQ Broker。
2.RocketMQ Broker 将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息暂不能投递,为半事务消息。
3.生产者开始执行本地事务逻辑。
4.生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
5.在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
6.:::note 需要注意的是,服务端仅仅会按照参数尝试指定次数,超过次数后事务会强制回滚,因此未决事务的回查时效性非常关键,需要按照业务的实际风险来设置 :::
事务消息回查步骤如下:
7.生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
8. 生产者根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
生产者编码实现过程
配置业务自定义RocketMQTemplate
重点:为每一个独立的业务,单独配置RocketMQTemplate
重点:自定义RocketMQTemplate与自定义RocketMQLocalTransactionListener,要成对编写
首先我们需要配置自定义的 RocketMQTemplate,最重要的是设置 TransactionMQProducer 的生产者组名称
@ExtRocketMQTemplateConfiguration:用于标识一个类是用于配置扩展的 RocketMQTemplate。
通过在这个类上使用 @ExtRocketMQTemplateConfiguration 注解,可以实现自定义的 RocketMQTemplate 配置。
group: 这是 @ExtRocketMQTemplateConfiguration 注解的一个参数,用于指定 RocketMQ 生产者组的名称。
上面分别指定的是名称为orderRocketMQTemplate
package com.example.mqtools.demos.message.acl;
import org.apache.rocketmq.spring.annotation.ExtRocketMQTemplateConfiguration;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
@ExtRocketMQTemplateConfiguration(group = "orderRocketMQTemplate")
public class OrderRocketMQTemplate extends RocketMQTemplate {
}
编写业务自定义监听器
监听业务代码要点:
重点:为每一个独立的业务,单独配置RocketMQLocalTransactionListener
重点:自定义RocketMQTemplate与自定义RocketMQLocalTransactionListener,要成对编写
业务的事务真实控制逻辑,对RocketMQLocalTransactionListener接口进行实现
@RocketMQTransactionListener,绑定rocketMQTemplateBeanName值,并指定对应的RocketMQ生产者组
生产者组是在rocketMQTemplateBeanName的注解中配置的。
@RocketMQTransactionListener(rocketMQTemplateBeanName="orderRocketMQTemplate")
public class OrderTransactionListenerImpl implements RocketMQLocalTransactionListener {
Logger logger = LoggerFactory.getLogger(OrderTransactionListenerImpl.class);
// 本地事务状态管理器
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<String, Integer>();
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object o) {
String transId = (String)msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
logger.info("#### executeLocalTransaction is executed, msgTransactionId="+transId);
logger.info("事务消息Headers为:{}", msg.getHeaders());
String payload = new String((byte[]) msg.getPayload());
User user = JSON.parseObject(payload,User.class);
logger.info("事务消息为:{}", payload);
// 具体的业务操作
int status = processBiz(user.getDivisor());
// 事务管理,缓存状态
localTrans.put(transId, status);
// 判断业务操作的结果,0 成功, 1 失败,2 其它
if (status == 0) {
// 业务执行成功,提交消息
logger.info("业务操作成功:{}", user.getDivisor());
return RocketMQLocalTransactionState.COMMIT;
}else if (status == 1){
// 业务执行失败,回滚消息
logger.info("业务操作失败:{}", user.getDivisor());
return RocketMQLocalTransactionState.ROLLBACK;
}else {
// 未知情况,抛异常了,一般放到 catch 中
logger.info("业务操作异常:{}", user.getDivisor());
return RocketMQLocalTransactionState.UNKNOWN;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String transId = (String)msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
RocketMQLocalTransactionState retState = RocketMQLocalTransactionState.COMMIT;
Integer status = localTrans.get(transId);
if (null != status) {
switch (status) {
case 0:
retState = RocketMQLocalTransactionState.COMMIT;
break;
case 1:
retState = RocketMQLocalTransactionState.ROLLBACK;
break;
case 2:
retState = RocketMQLocalTransactionState.ROLLBACK;
break;
}
}
logger.info("------ !!! checkLocalTransaction is executed once,msgTransactionId="+transId+", TransactionState="+retState+" status="+status);
return retState;
}
}
编写模拟的业务逻辑代码
测试业务代码要点:
本测试业务逻辑是一个除法计算公式。
通过控制divisor 的值为正数数 执行正确,模拟状态0
通过控制divisor 的值为负数 执行失败,业务规则不允许为负数,模拟状态1
通过控制divisor 的值为0 抛异常,数据规则错误除数不能为0,模拟状态2
在executeLocalTransaction代码中,执行此业务代码
/**
* 业务处理
* 返回处理结果 0 成功, 1 失败,2 其它
* divisor 除数
* @return
*/
public Integer processBiz(int divisor){
int status = 0;
try{
// 计算a % b 的结果
// 通过控制divisor 的值为正数数 执行正确,模拟状态0
// 通过控制divisor 的值为负数 抛异常,模拟状态1
// 通过控制divisor 的值为0 抛异常,模拟状态2
int a = 100;
if(divisor>0){
int c = a / divisor;
}else if(divisor<0){
// 业务只允许 除数为正数,返回 失败的状态
status = 1;
}else{
int c = a / divisor;
}
}catch (Exception e){
status = 2;
logger.error("processBiz error;",e);
}
return status;
}
事务消息生产者
事务消息生产者
事务类消息,需要自定义配置RocketMQTemplate与RocketMQLocalTransactionListener
消费者方法使用sendMessageInTransaction
package com.example.mqtools.demos.message;
import com.example.mqtools.demos.message.acl.OrderRocketMQTemplate;
import com.example.mqtools.demos.web.User;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.Random;
/**
* 事务消息消费者
* 事务类消息,需要自定义配置RocketMQTemplate与RocketMQLocalTransactionListener
* 消费者方法使用sendMessageInTransaction
*/
@Controller
public class OrderProducerController {
Logger logger = LoggerFactory.getLogger(OrderProducerController.class);
/**
* 事务类消息,需要自定义配置RocketMQTemplate与RocketMQLocalTransactionListener
*/
@Resource
private OrderRocketMQTemplate orderRocketMQTemplate;
@Value("${rocketmq.producer.simple.demo.topic-order}")
private String topic;
/**
* 发送订单消息,带上事务
* @param name
* @return
*/
@RequestMapping("/producerSendOrder")
@ResponseBody
public String producerSendOrder(@RequestParam(name="name",defaultValue = "unknown user") String name,@RequestParam(name="divisor",defaultValue = "1") Integer divisor){
logger.info("OrderProducerController.producerSendOrder:name="+name);
User user = new User();
user.setName(name);
user.setAge(new Random().nextInt());
user.setDivisor(divisor);
MessageBuilder<User> messageBuilder = MessageBuilder.withPayload(user);
messageBuilder.setHeader(RocketMQHeaders.KEYS,user.getAge().toString());
messageBuilder.setHeader(RocketMQHeaders.TRANSACTION_ID,"key"+user.getAge().toString());
Message<User> message = messageBuilder.build();
// 设置消息的 tag,使用时按照业务直接在属性定义上,直接拼接上tag
String topicWithTag = topic+":tag1";
SendResult sendResult = orderRocketMQTemplate.sendMessageInTransaction(topicWithTag,message,null);
logger.info("OrderProducerController.producerSendOrder:sendResult="+sendResult);
return sendResult.toString();
}
}
测试
通过请求参数divisor来模拟三种情况。具体可以细看业务逻辑代码。
业务执行正常:http://localhost:8888//producerSendOrder?name=user001&divisor=1
业务执行失败:http://localhost:8888//producerSendOrder?name=user001&divisor=-1
业务执行异常:http://localhost:8888//producerSendOrder?name=user001&divisor=0
注意事项
事务消息与分布式事务是两个概念容易混淆
- 事务消息通常指的是在消息中间件中支持的一种消息模式,它确保了消息发送与本地事务处理之间的原子性。具体来说,事务消息允许应用程序在本地数据库执行一系列操作(事务)后,再将这些操作的结果作为消息发送到消息队列。这个过程中,消息发送的操作被视为本地事务的一部分,只有本地事务成功提交后,消息才会被确认发送到消息队列;如果本地事务失败并回滚,那么对应的消息发送也会被取消。
- 分布式事务涉及在多个分布式节点(可能是多个数据库、多个服务、多个应用实例等)上执行的操作,这些操作需要作为一个整体进行提交或回滚。在分布式系统中,由于网络延迟、系统崩溃等不可预测因素的存在,使得分布式事务的处理变得复杂。
分布式事务的目标是确保多个节点上的操作要么全部成功,要么全部失败,以维护系统数据的一致性和完整性。然而,由于分布式系统的复杂性,实现分布式事务的代价往往很高,包括性能损失、复杂度增加等。
一个系统多种事务消息如何处理
主要是两种方式:
- 在发送消息的时候在头部添加区分业务的属性,然后在监听器取出来,执行不同的业务代码。注意哦,group不同就用不了此方式,局限性比较大。
- 本文所写方式,每种事务,单独写RocketMQTemplate和RocketMQLocalTransactionListener 。