1 什么是事务消息
RocketMQ中事务消息主要是解决分布式场景下各业务系统事务一致性问题,常见的分布式事务解决方案有传统XA事务方案、TCC、本地消息表、MQ事务等。今天我们基于RocketMQ事务消息解决预付卡系统资金账户子系统和会员积分子系统、短信子系统分布式事务业务场景
1.1 事务消息功能原理
事务消息是 RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。
事务消息处理流程
- 生产者将半事务消息发送至RocketMQ服务端。
- RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。
- 生产者开始执行本地事务逻辑。
- 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
- 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
- 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
- 生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
2 分布式事务的应用场景
分布式系统调用的特点为一个核心业务逻辑的执行,同时需要调用多个下游业务进行处理。因此,如何保证核心业务和多个下游业务的执行结果完全一致,是分布式事务需要解决的主要问题。接下来我们基于预付卡系统-资金账户子系统和会员积分子系统、短信子系统业务流程来对分布式应用场景进行分析
- 预付卡系统-资金账户子系统为整个分布式应用场景中的上游系统,上游系统资金账户的变动会影响下游系统(会员积分子系统,短信子系统)事务的执行
- 在上游系统事务执行成功后会提交事务消息到下游系统,这时下游系统(会员积分和短信子系统)执行自己的业务逻辑
- 如果上游系统未执行成功最终状态为rollback则不会发送消息到下游系统,这时下游系统就不会执行相关业务逻辑保证了事务的一致性
3 基于RocketMQ事务消息解决卡系统分布式事务
- 预付卡系统-资金账户模块向RocketMQ服务端发送半事务消息,该消息暂时不会投递下游系统
- RocketMQ服务端向资金账户返回接收成功
- 执行资金账户本地事务
- 向RocketMQ二次提交事务执行结果(commit或rollback)
- 如果RocketMQ服务端出现异常情况会在资金账户系统(生产者)回查状态再执行第4步
- 检查资金账户本地事务最终执行结果
- 根据资金账户本地事务最终状态再次进行二次提交
- 如果事务状态最终为commit则会交付消息到会员积分子系统,短信子系统(消息订阅者),如果状态为rollback则不交付消息到订阅者
4 RocketMQ事务消息核心代码实现
本技术文档采用SpringCloud2021.x和RocketMQ5.0进行代码实现
Spring Cloud Alibaba Version | Spring Cloud Version | Spring Boot Version |
---|---|---|
2021.x | Spring Cloud 2021.x | 2.7.18 |
4.1 在SpringCloud中集成RocketMQ流程
4.1.1 引入RocketMQ Stream Starter
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
4.1.2 修改application.properties配置文件
server.port=1001
spring.application.name=ecard-tx-pay
spring.cloud.stream.function.definition=consumer;
spring.cloud.stream.rocketmq.binder.name-server=localhost:9876
spring.cloud.stream.rocketmq.bindings.producer-out-0.producer.group=output_1
spring.cloud.stream.rocketmq.bindings.producer-out-0.producer.transactionListener=ecardTransactionListener
spring.cloud.stream.rocketmq.bindings.producer-out-0.producer.producerType=Trans
spring.cloud.stream.bindings.producer-out-0.destination=tx
spring.cloud.stream.bindings.consumer-in-0.destination=tx
spring.cloud.stream.bindings.consumer-in-0.group=tx-group
logging.level.org.springframework.context.support=debug
4.1.3 代码实现
1 事务消息生产者
package cn.itbeien.ecard.tx;
import java.util.function.Consumer;
import cn.itbeien.ecard.common.SimpleMsg;
import com.alibaba.cloud.stream.binder.rocketmq.constant.RocketMQConst;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeTypeUtils;
/**
* @author beien
* @date 2024-06-20 22:15
* Copyright© 2024 beien
* 发送半事务消息到RocketMQ服务端
*/
@Service
@Slf4j
public class EcardTXService {
@Autowired
private StreamBridge streamBridge;
//生产半事务消息
@Bean
public void producer() {
String orderId = "10001";
MessageBuilder builder = MessageBuilder.withPayload(new SimpleMsg("Hello Tx msg " + orderId));
builder.setHeader("test", String.valueOf(i))
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON);
builder.setHeader(RocketMQConst.USER_TRANSACTIONAL_ARGS, "binder");
Message<SimpleMsg> msg = builder.build();
streamBridge.send("producer-out-0", msg);
log.info("send Msg:" + msg.toString());
}
}
2 事务消息监听
package cn.itbeien.ecard.tx;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;
/**
* @author beien
* @date 2024-06-20 22:09
* Copyright© 2024 beien
* 执行本地事务和事务状态检查
*/
@Component("ecardTransactionListener")
@Slf4j
public class TransactionListenerImpl implements TransactionListener {
/**
* @param msg messages
* @param arg message args
* @return Transaction state
* 执行本地事务方法 对应事务消息执行流程中的第3步
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
Object num = msg.getProperty("test");
if ("1".equals(num)) {
log.info("executer: " + new String(msg.getBody()) + " unknown");
return LocalTransactionState.UNKNOW;
}
else if ("2".equals(num)) {
log.info("executer: " + new String(msg.getBody()) + " rollback");
return LocalTransactionState.ROLLBACK_MESSAGE;
}
log.info("executer: " + new String(msg.getBody()) + " commit");
return LocalTransactionState.COMMIT_MESSAGE;
}
/**
* @param msg messages
* @return Transaction state
* 检查本地事务方法,根据事务执行结果返回事务状态
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
log.info("check: " + new String(msg.getBody()));
return LocalTransactionState.COMMIT_MESSAGE;
}
}
3 事务消息订阅(消费)者
package cn.itbeien.pay.tx;
import java.util.function.Consumer;
import cn.itbeien.ecard.common.SimpleMsg;
import com.alibaba.cloud.stream.binder.rocketmq.constant.RocketMQConst;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeTypeUtils;
/**
* @author beien
* @date 2024-06-20 22:20
* Copyright© 2024 beien
* 该部分代码为会员积分子系统、短信子系统用来处理预付卡系统账户扣款状态的方法
*/
@Service
@Slf4j
public class PayConsumerService {
@Bean
public Consumer<Message<SimpleMsg>> consumer() {
/**
*会员积分子系统、短信子系统根据订阅自动执行此业务逻辑
* 根据消费金额记录积分,下发短信通知用户预付卡余额变更或下发消息到小程序
*/
return msg -> {
Object arg = msg.getHeaders();
log.info(Thread.currentThread().getName() + " Receive New Messages: " + msg.getPayload().getMsg() + " ARG:"
+ arg.toString());
};
}
}
5 关注我
欢迎关注我的视频号和公众号,视频号有相关技术和业务视频可学习支付业务/文旅行业数字化。探讨技术(系统架构、微服务、容器化、云原生、分布式事务),支付系统实战。