一、事务消息与普通消息区别
1. 事务消息
事务消息是一种支持事务性操作的消息,主要用于满足分布式事务的一致性需求。事务消息的发送者可以将一系列的操作包装在一个事务中,如果整个事务的操作都成功执行,则消息被发送到消息队列,如果有任何一个操作失败,则消息被丢弃。事务消息的消费者可以通过回查机制来保证消息的最终一致性,即通过询问消息发送者的本地事务状态来判断是否需要提交或回滚。
2. 普通消息:
普通消息是一种简单的消息模式,发送者发送消息到消息队列后,消费者可以立即消费这些消息。普通消息没有事务性的特点,发送者和消费者之间没有强一致性的要求。
因此,事务消息和普通消息的主要区别在于:
- 事务消息支持事务性操作,发送者可以将一系列的操作包装在一个事务中,并通过回查机制来保证消息的最终一致性。
- 普通消息没有事务性的特点,发送者和消费者之间没有强一致性的要求,消息发送后可以立即被消费。
二、消息流程
RocketMQ事务消息的处理步骤可以分为以下几个阶段:
1、系统业务方发送半事务消息
系统业务方在执行本地事务之前,首先会向消息队列发送一条半事务消息(表示事务未完成)。
2、Broker存储半事务消息
Broker收到半事务消息后,将其存储,并向业务方返回发送结果。
3、系统业务方执行本地事务
业务方根据实际业务逻辑执行本地事务,如订单创建、库存扣减等。
4、业务方提交事务状态
业务方在本地事务处理完毕后,会发送事务状态(提交或回滚)的确认消息给Broker。
5、Broker回查本地事务
如果Broker在设定的时间内(默认60秒)未收到事务确认消息,它会向业务方发送回查请求,询问本地事务的状态。
6、业务方处理回查请求
业务方根据本地事务的实际执行结果,响应Broker的回查请求。
7、Broker处理回查结果
根据业务方返回的回查结果,Broker决定是否提交或回滚该事务消息,并将处理后的消息更新到实际的消息队列中,供消费者消费。
RocketMQ通过两阶段提交(2PC)+ 事后补偿机制,确保消息的一致性,避免因网络或系统故障导致的消息丢失或重复消费问题。
三、事务消息的内部机制
RocketMQ在处理事务消息时,涉及到以下特殊的Topic:
RMQ_SYS_TRANS_HALF_TOPIC:存储半事务消息的Topic。
RMQ_SYS_TRANS_OP_HALF_TOPIC:已处理的半事务消息Topic,记录半事务消息的状态(如消息位移),便于后续处理。
事务消息的具体流程:
1、发送半事务消息
系统业务方使用TransactionMQProducer发送事务消息至TopicA。在发送时,消息的属性中会添加TRAN_MSG=true的标记,表明这是事务消息。
2、Broker存储半事务消息
Broker收到事务消息后,检查TRAN_MSG属性,如果为true,则将消息的原始Topic(TopicA)保存在消息属性REAL_TOPIC中,并将消息的Topic替换为RMQ_SYS_TRANS_HALF_TOPIC,存储消息并返回结果。
3、业务方处理本地事务
业务方收到消息发送结果后,执行本地事务操作,如完成订单创建。
4、发送事务结果
业务方根据本地事务的执行情况,发送END_TRANSACTION消息给Broker,通知事务提交或回滚。
5、Broker提交或回滚消息
Broker根据收到的END_TRANSACTION消息判断事务结果:
如果是COMMIT,则恢复消息的原始Topic(TopicA),并将消息发送到实际的队列中,供消费者消费。
如果是ROLLBACK,则丢弃消息,不做进一步处理。
6、保存处理结果
Broker在处理完成后,将半事务消息状态存储到RMQ_SYS_TRANS_OP_HALF_TOPIC中,标记该消息已处理。
7、事务回查机制
Broker在启动时会启动一个定时回查服务,定期检查未完成的半事务消息。如果发现有超时未处理的半事务消息(UNKNOWN状态),则发起回查请求(CHECK_TRANSACTION_STATE)给业务方,询问本地事务的执行结果。
8、处理回查请求
业务方收到回查请求后,查询本地事务状态并回复Broker,Broker根据返回结果进行消息的提交或回滚操作。
四、代码示例
SpringBoot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
</parent>
RocketMq版本
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
自定义RocketMqTemplate
/**
* 自定义的RocketMQTemplat 发送事务消息
*/
@ExtRocketMQTemplateConfiguration(group = "${rocketmq.group}")
@Component
public class MyDiyMQTemplate extends RocketMQTemplate {
}
发送消息
@Autowired
private MyDiyMQTemplate myDiyMQTemplate;
public void sendMessage(){
JSONObject message= new JSONObject()
//发送消息
myDiyMQTemplate.sendMessageInTransaction(
"Topic:Tag",
MessageBuilder.withPayload(message).build(),
""
);
}
配置事务消息监听器
package com.chat.app.stream.listener;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.transaction.annotation.Transactional;
import java.nio.charset.StandardCharsets;
/**
* 一个事物监听器对应一个事物流程,采取对应的rocketMQTemplate 发送事务消息
*/
@RocketMQTransactionListener(rocketMQTemplateBeanName = "myDiyMQTemplate")
@Slf4j
public class PurchaseDressUpTransactionListener implements RocketMQLocalTransactionListener {
/**
* 执行本地事务
*
* @param msg
* @param arg
* @return
*/
@Override
@Transactional
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
//消息转换
org.apache.rocketmq.common.message.Message rocketMsg = RocketMQUtil.convertToRocketMessage(new StringMessageConverter(), "UTF-8", TopicConstants.TAG_PAY_EVENT_PURCHASE_DRESS, msg);
JsonObject message= JSONObject.parseObject(new String(rocketMsg.getBody(), StandardCharsets.UTF_8), JsonObject .class);
log.info("执行本地事务--》:{}", message);
.......
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
e.printStackTrace();
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 检查本地事务状态
*
* @param msg
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
try {
//消息转换
org.apache.rocketmq.common.message.Message rocketMsg = RocketMQUtil.convertToRocketMessage(new StringMessageConverter(), "UTF-8", TopicConstants.TAG_PAY_EVENT_PURCHASE_DRESS, msg);
JsonObject message= JSONObject.parseObject(new String(rocketMsg.getBody(), StandardCharsets.UTF_8), JsonObject .class);
log.info("检查本地事务状态--》:{}", message);
........
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
e.printStackTrace();
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
接受事务消息
package com.chat.app.stream.consumer;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RocketMQMessageListener(topic ="topic",
consumerGroup = "group",
enableMsgTrace = true,
selectorExpression = "tag")
@Slf4j
@Component
public class MyTransactionMessageConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String msg) {
log.info("收到事务消息:{}", msg);
JSONObject message = JSONObject.parseObject(msg);
}
}