我的springboot的版本是2.1.3 rocketmq-spring-boot-starter版本是2.2.0
之前我的博文手把手带你 SpringBoot 2.X 整合 RocketMq 实现了rocketmq的异步消息生产消费和顺序消息生产和消费 今天来学习一下RocketMQ事务消息
的发送消费。
RocketMQ的事务消息分为3种状态,分别是提交状态
、回滚状态
、中间状态
:
TransactionStatus.CommitTransaction: 提交事务,它允许消费者消费此消息。
TransactionStatus.RollbackTransaction: 回滚事务,它代表该消息将被删除,不允许被消费。
TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
事务消息在解决分布式事务的场景中感觉还是很有用的,虽然我们现在项目的分布式事务是通过Seata
来实现的,但是通过事务消息或者消息的最终一次性也是可以的。
事务消息总共分为3个阶段:发送Prepared消息
、执行本地事务
、发送确认消息
。这三个阶段是前后关联的,只有发送Prepared消息成功,才会执行本地事务,本地事务返回的状态是提交,那么就会发送最终的确认消息。如果在结束消息事务时,本地事务状态失败,那么Broker回查线程定时(默认1分钟)扫描每个存储事务状态的表格文件,如果是已经提交或者回滚的消息直接跳过,如果是Prepared状态则会向生产者发起一个检查本地事务的请求。
增加了 数据库的连接,mapper,service等
数据库创建两个表
CREATE TABLE `order_entity` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_type` varchar(50) DEFAULT NULL COMMENT '订单类型',
`order_no` varchar(50) DEFAULT NULL COMMENT '订单编号',
`price` decimal(10,2) DEFAULT NULL COMMENT '订单总价',
`user_id` int(11) DEFAULT NULL COMMENT '下单用户id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT '姓名',
`age` int(10) DEFAULT NULL COMMENT '年龄',
`account` decimal(10,2) DEFAULT NULL COMMENT '账户余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
用户实体
package com.example.entity;
/**
* @author guog
* @create 2021-04-21 17:39
*/
public class User {
String name;
Integer id;
Integer age;
Double account;// 用户账户
public Double getAccount() {
return account;
}
public void setAccount(Double account) {
this.account = account;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
UserMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
namespace="com.example.mapper.UserMapper">
</mapper>
订单实体
package com.example.entity;
/**
* @author guog
* @create 2021-04-22 11:32
*/
public class OrderEntity {
Integer id;
Integer userId;//下单用户id
String orderType;
String orderNo;
Double price;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getOrderType() {
return orderType;
}
public void setOrderType(String orderType) {
this.orderType = orderType;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
}
订单mapper
@Mapper
public interface OrderMapper extends BaseMapper<OrderEntity> {
}
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
namespace="com.example.mapper.OrderMapper">
</mapper>
在之前的testController 中加一个增加订单的方法
/**
* 模拟事务消息
*
* 步骤一 创建订单
* 功能:发送事务消息
*/
@GetMapping("addOrder")
public String addOrder() {
OrderEntity order=new OrderEntity();
order.setOrderNo("11111");
order.setUserId(1);
order.setPrice(200.00);
TransactionSendResult sendResult= rocketMqHelper.transactionSend("TX_ORDER_ADD",MessageBuilder.withPayload(order).build(),order);
String sendStatus = sendResult.getSendStatus().name();
String localTXState = sendResult.getLocalTransactionState().name();
System.out.println("sendStatus---"+sendStatus);
System.out.println("localTXState---"+localTXState);
return "success";
}
rocketMqHelper 工具类增加发送事务消息方法 transactionSend
/**
* 发送事务消息
*
* @param topic
* @param message
* @param arg
*/
public TransactionSendResult transactionSend(String topic, Message<?> message,Object arg) {
LOG.info("发送事务消息,topic:" + topic );
TransactionSendResult sendResult=rocketMQTemplate.sendMessageInTransaction(topic,message,arg);
return sendResult;
}
接下来我们创建一个消息的监听器(消费者),这个和普通消息的监听器一样我只是加了操作数据库的代码,代码如下:
/**
* @author guog
* @create 2021-04-21 17:40
*/
@Component
@RocketMQMessageListener(consumerGroup = "${rocketmq.producer.groupName1}", topic = "TX_ORDER_ADD",consumeMode = ConsumeMode.ORDERLY)
public class OrderMqListener implements RocketMQListener<OrderEntity> {
@Autowired
private UserMapper userMapper;
@Override
@Transactional
public void onMessage(OrderEntity order) {
System.out.println("接收到消息,开始消费..OrderType:" + order.getOrderType() + ",OrderNO:" + order.getOrderNo());
// 一般真实环境这里消费前,得做幂等性判断,防止重复消费
// 方法一:如果你的业务中有某个字段是唯一的,有标识性,如订单号,那就可以用此字段来判断
// 方法二:新建一张消费记录表t_mq_consumer_log,字段consumer_key是唯一性,能插入则表明该消息还未消费,往下走,否则停止消费
// 我个人建议用方法二,根据你的项目业务来定义key,这里我就不做幂等判断了,因为此案例只是模拟,重在分布式事务
User user=userMapper.selectById(order.getUserId());
user.setAccount(user.getAccount()-order.getPrice());
userMapper.updateById(user);
}
}
除了消费者之外,我们还需要创建事务消息生产者端的消息监听器,注意是生产者,不是消费者,我们需要实现的是RocketMQLocalTransactionListener接口,代码如下:
/**
* 订单事务消息生产监听
* @author guog
* @create 2021-04-21 17:40
*/
@Component
@RocketMQTransactionListener
public class OrderTXMsgListener implements RocketMQLocalTransactionListener {
/**
* 日志
*/
private static final Logger log = LoggerFactory.getLogger(OrderTXMsgListener.class);
private static final Gson GSON = new Gson();
@Autowired
private OrderService orderService;
/**
* 步骤二:
* 描述:mq收到事务消息后,开始执行本地事务
* @Transactional:开启本地事务
* @param msg
* @param arg
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info(">>>> TX message listener execute local transaction, message={},args={} <<<<",msg,arg);
// 执行本地事务
RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
try {
String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
OrderEntity order = GSON.fromJson(jsonString, OrderEntity.class);
orderService.addOrder(order);
} catch (Exception e) {
log.error(">>>> exception message={} <<<<",e.getMessage());
result = RocketMQLocalTransactionState.UNKNOWN;
}
return result;
}
/**
* 步骤四
* 描述:mq回调检查本地事务执行情况
* @param msg
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
log.info(">>>> TX message listener check local transaction, message={} <<<<",msg.getPayload());
// 检查本地事务
RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
try {
String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
OrderEntity order = GSON.fromJson(jsonString, OrderEntity.class);
List<OrderEntity> list = orderService.selectOrder(order);
if(list.size()<=0){
result = RocketMQLocalTransactionState.UNKNOWN;
}
} catch (Exception e) {
// 异常就回滚
log.error(">>>> exception message={} <<<<",e.getMessage());
result = RocketMQLocalTransactionState.ROLLBACK;
}
return result;
}
}
写了个订单的OrderService
/**
* @author guog
* @create 2021-04-26 16:18
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void addOrder(OrderEntity order) {
orderMapper.insert(order);
}
public List<OrderEntity> selectOrder(OrderEntity order) {
QueryWrapper<OrderEntity> query=new QueryWrapper<>();
query.in("order_no", order.getOrderNo());
List<OrderEntity> list = orderMapper.selectList(query);
return list;
}
}
application.yml
server:
port: 8088
#rocketmq配置
rocketmq:
name-server: 127.0.0.1:9876
# 生产者配置
producer:
isOnOff: on
# 发送同一类消息的设置为同一个group,保证唯一
group: user-rocketmq-group
groupName: user-rocketmq-group
group1: order-rocketmq-group
groupName1: order-rocketmq-group
# 服务地址
namesrvAddr: 127.0.0.1:9876
# 消息最大长度 默认1024*4(4M)
maxMessageSize: 4096
# 发送消息超时时间,默认3000
sendMsgTimeout: 3000
# 发送消息失败重试次数,默认2
retryTimesWhenSendFailed: 2
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://127.0.0.1:3306/rocket_deom?useUnicode=true&useSSL=false&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2b8
username: root
password: 123456
hikari:
connection-test-query: SELECT 1
driver-class-name: com.mysql.cj.jdbc.Driver
connection-timeout: 60000
minimum-idle: 5
maximum-pool-size: 15
idle-timeout: 600000
max-lifetime: 1200000
auto-commit: true
mybatis-plus:
mapper-locations:
- classpath*:com/example/mapper/*Mapper.xml
type-aliases-package: com.sxmpx.entity
global-config:
db-config:
db-type: mysql
id-type: auto
field-strategy: default
capital-mode: true
生成一个订单号为11111的订单 订单总价为200元 并且下单用户是上面创建的用户 user_id为1
首先还是正常的启动项目,访问addOrder方法,在执行本地事务方法中正常情况下返回的值是COMMIT,即提交事务,这种情况下消费者会直接消费消息,而略过检查本地事务的方法。调用该接口,项目日志输出如下:
通过日志分析可以看出,在执行完本地事务方法之后,返回的本地事务状态是COMMIT_MESSAGE,接着消费者消费消息,和我们的预期是一样的。
2021-04-27 14:06:59.198 INFO 8436 --- [nio-8088-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-04-27 14:06:59.198 INFO 8436 --- [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-04-27 14:06:59.203 INFO 8436 --- [nio-8088-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
2021-04-27 14:06:59.229 INFO 8436 --- [nio-8088-exec-1] com.example.utils.RocketMqHelper : 发送事务消息,topic:TX_ORDER_ADD
2021-04-27 14:06:59.442 INFO 8436 --- [nio-8088-exec-1] com.example.Listener.OrderTXMsgListener : >>>> TX message listener execute local transaction, message=GenericMessage [payload=byte[71], headers={rocketmq_TOPIC=TX_ORDER_ADD, rocketmq_FLAG=0, id=b896a47c-d3df-d955-bd36-d551d80bc400, contentType=application/json;charset=UTF-8, rocketmq_TRANSACTION_ID=7F00000120F418B4AAC288ECC9210000, timestamp=1619503619442}],args=com.example.entity.OrderEntity@3498d027 <<<<
2021-04-27 14:06:59.450 INFO 8436 --- [nio-8088-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-04-27 14:06:59.539 INFO 8436 --- [nio-8088-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
sendStatus---SEND_OK
localTXState---COMMIT_MESSAGE
接收到消息,开始消费..OrderType:null,OrderNO:11111
简单改造下执行本地事务的方法,让直接返回 RocketMQLocalTransactionState.UNKNOWN
/**
* 步骤二:
* 描述:mq收到事务消息后,开始执行本地事务
* @Transactional:开启本地事务
* @param msg
* @param arg
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info(">>>> TX message listener execute local transaction, message={},args={} <<<<",msg,arg);
// 执行本地事务
RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
try {
String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
OrderEntity order = GSON.fromJson(jsonString, OrderEntity.class);
orderService.addOrder(order);
} catch (Exception e) {
log.error(">>>> exception message={} <<<<",e.getMessage());
result = RocketMQLocalTransactionState.UNKNOWN;
}
return RocketMQLocalTransactionState.UNKNOWN;//改造这里直接返回UNKNOWN
}
这样因为发生异常,该方法返回的结果是UNKNOWN,根据上文的分析,执行本地事务方法之后应该会执行检查本地事务方法,如果查询到有这条订单的order_no的数据 返回COMMIT 消费者继续消费信息
/**
* 步骤四
* 描述:mq回调检查本地事务执行情况
* @param msg
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
log.info(">>>> TX message listener check local transaction, message={} <<<<",msg.getPayload());
// 检查本地事务
RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
try {
String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
OrderEntity order = GSON.fromJson(jsonString, OrderEntity.class);
List<OrderEntity> list = orderService.selectOrder(order);
if(list.size()<=0){
result = RocketMQLocalTransactionState.UNKNOWN;
}
} catch (Exception e) {
// 异常就回滚
log.error(">>>> exception message={} <<<<",e.getMessage());
result = RocketMQLocalTransactionState.ROLLBACK;
}
return result;
}
清空订单表,重启项目之后,再次调用一下接口,查看日志输出如下:
2021-04-27 14:24:14.855 INFO 20668 --- [nio-8088-exec-1] com.example.utils.RocketMqHelper : 发送事务消息,topic:TX_ORDER_ADD
2021-04-27 14:24:15.038 INFO 20668 --- [nio-8088-exec-1] com.example.Listener.OrderTXMsgListener : >>>> TX message listener execute local transaction, message=GenericMessage [payload=byte[71], headers={rocketmq_TOPIC=TX_ORDER_ADD, rocketmq_FLAG=0, id=0c1dc077-ca38-08a1-b0f1-65444f3a8a42, contentType=application/json;charset=UTF-8, rocketmq_TRANSACTION_ID=7F00000150BC18B4AAC288FC96700000, timestamp=1619504655038}],args=com.example.entity.OrderEntity@47bc7591 <<<<
2021-04-27 14:24:15.047 INFO 20668 --- [nio-8088-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-04-27 14:24:15.141 INFO 20668 --- [nio-8088-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
sendStatus---SEND_OK
localTXState---UNKNOW
2021-04-27 14:24:59.156 INFO 20668 --- [pool-1-thread-1] com.example.Listener.OrderTXMsgListener : >>>> TX message listener check local transaction, message=[123, 34, 105, 100, 34, 58, 110, 117, 108, 108, 44, 34, 117, 115, 101, 114, 73, 100, 34, 58, 49, 44, 34, 111, 114, 100, 101, 114, 84, 121, 112, 101, 34, 58, 110, 117, 108, 108, 44, 34, 111, 114, 100, 101, 114, 78, 111, 34, 58, 34, 49, 49, 49, 49, 49, 34, 44, 34, 112, 114, 105, 99, 101, 34, 58, 50, 48, 48, 46, 48, 125] <<<<
接收到消息,开始消费..OrderType:null,OrderNO:11111
通过日志分析可以看出,在执行完本地事务方法之后,返回的本地事务状态是UNKNOW,接着执行检查本地事务方法,如果查询到有这条订单的order_no的数据 返回COMMIT 消费者继续消费信息,和我们的预期是一样的。
简单改造下执行本地事务的方法,让直接返回RocketMQLocalTransactionState.ROLLBACK
,表示该消息将被删除,不允许被消费。
/**
* 步骤二:
* 描述:mq收到事务消息后,开始执行本地事务
* @Transactional:开启本地事务
* @param msg
* @param arg
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info(">>>> TX message listener execute local transaction, message={},args={} <<<<",msg,arg);
// 执行本地事务
RocketMQLocalTransactionState result = RocketMQLocalTransactionState.COMMIT;
try {
String jsonString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
OrderEntity order = GSON.fromJson(jsonString, OrderEntity.class);
orderService.addOrder(order);
} catch (Exception e) {
log.error(">>>> exception message={} <<<<",e.getMessage());
result = RocketMQLocalTransactionState.UNKNOWN;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
再次调用一下接口,查看日志输出如下:
通过日志分析可以看出,在执行完本地事务方法之后,返回的本地事务状态是ROLLBACK,消费者并没有消费这条消息
这里你可能质疑,前面的发送和本地事务都没啥问题,要么commit要么rollback,但如果这里消费失败怎么办呢?其实这里会产生问题的几率几乎不存在,首先RocketMQ就是高可用的,要真的你系统很庞大很庞大,你可以集群;再者,这里消费成功与否,源码内部已做处理,只要没异常,就会进行消费,而且它也有重试机制;最后,这里消费逻辑你可以扩展,当消费不成功时,你可以把该记录保存下来,定时提醒或人工去处理
我模拟了下消费端消费抛出个异常,并发消费模型中,消息消费失败默认会重试 16 次(16次可能是之前的版本,我测试的4.8.0 可以重试34次),每一次的间隔时间不一样;而顺序消费,如果一条消息消费失败,则会一直消费,直到消费成功。故在顺序消费使用过程中,应用程序需要区分系统异常、业务异常,如果是不符合业务规则导致的异常,则重试多少次都无法消费成功,这个时候一定要告警机制,及时进行人为干预,否则消费会积压会发现消费端会一直重试进行消费,由此可见确实 这里消费逻辑你可以扩展,当消费不成功时,你可以把该记录保存下来,定时提醒或人工去处理
好了RocketMQ的事务消息先演示到这里 有任何不对的地方,评论区见!