目录
一、简介
在之前的文章中,我讲过了发送事务消息,好像是通过RocketMQTemplate就完成了普通消息和事务消息的发送,有小伙伴就会问,如果一个系统可能不止一个事务,都会进入到同一个监听啊,有什么办法处理么?也就是多事务发送消息。主要是两种方式:
- 自己在发生消息的时候在头部添加区分业务的属性,然后在监听器取出来,然后做不同的业务
- 每个事务都配置单独的事务监听器(本文的方式)
二、Maven依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>rocketmq</artifactId>
<groupId>com.alian</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>06-send-transactional-message</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alian</groupId>
<artifactId>common-rocketmq-dto</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
父工程已经在我上一篇文章里,通用公共包也在我上一篇文章里有说明,包括消费者。具体参考:RocketMQ笔记(一)SpringBoot整合RocketMQ发送同步消息
三、生产者
在本文中假设有两个事务,员工消息和订单消息需要事务处理。
3.1、application配置
application.properties
server.port=8006
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type= com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=test
spring.datasource.password=Alian!@34
spring.datasource.url=jdbc:mysql://192.168.0.139:3306/test?characterEncoding=utf8&useUnicode=true&useSSL=false&zeroDateTimeBehavior=convertToNull&autoReconnect=true&allowMultiQueries=true&failOverReadOnly=false&connectTimeout=6000&maxReconnects=5
spring.datasource.initialSize=5
spring.datasource.minIdle= 5
spring.datasource.maxActive=20
# rocketmq地址
rocketmq.name-server=192.168.0.234:9876
# 默认的生产者组
rocketmq.producer.group=transactional_group
# 发送同步消息超时时间
rocketmq.producer.send-message-timeout=3000
# 用于设置在消息发送失败后,生产者是否尝试切换到下一个服务器。设置为 true 表示启用,在发送失败时尝试切换到下一个服务器
rocketmq.producer.retry-next-server=true
# 用于指定消息发送失败时的重试次数
rocketmq.producer.retry-times-when-send-failed=3
# 设置消息压缩的阈值,为0表示禁用消息体的压缩
rocketmq.producer.compress-message-body-threshold=0
3.2、数据库
我们这里准备两个表,员工表 和 订单表用于测试。
员工表
CREATE TABLE `employee` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`code` varchar(30) NOT NULL DEFAULT '' COMMENT '编号',
`emp_name` varchar(20) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(2) NOT NULL DEFAULT '0' COMMENT '年龄',
`salary` double(8,2) NOT NULL DEFAULT '0.00' COMMENT '工资',
`department` varchar(20) NOT NULL DEFAULT '' COMMENT '部门',
`hire_date` date NOT NULL DEFAULT '1970-07-01' COMMENT '入职时间',
PRIMARY KEY (`id`),
UNIQUE KEY `code_UNIQUE` (`code`),
KEY `idx_code` (`code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4
订单表
CREATE TABLE `tb_order` (
`id` bigint(10) unsigned NOT NULL COMMENT '主键',
`user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
`goods_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '商品id',
`price` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '价格(单位:分)',
`num` int(10) unsigned NOT NULL DEFAULT '1' COMMENT '数量',
`title` varchar(100) NOT NULL DEFAULT '' COMMENT '订单标题',
`order_status` varchar(2) NOT NULL DEFAULT '01' COMMENT '订单状态(00:成功,01:处理中,02:失败)',
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_goods_id` (`goods_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单明细表'
3.3、实体
Employee.java
@Data
@Entity
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 员工编号
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
/**
* 员工编号
*/
private String code;
/**
* 员工姓名
*/
private String empName;
/**
* 员工年龄
*/
private int age;
/**
* 工资
*/
private double salary = 0.00;
/**
* 部门
*/
private String department;
/**
* 入职时间
*/
private LocalDate hireDate;
}
Order.java
@Data
@Entity
@Table(name="tb_order")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 员工编号
*/
@Id
private Long id;
/**
* 用户编号
*/
private String userId;
/**
* 商品
*/
private String goodsId;
/**
* 价格
*/
private Integer price;
/**
* 数量
*/
private Integer num;
/**
* 订单标题
*/
private String title;
/**
* 订单状态
*/
private String orderStatus;
}
3.4、持久层
EmployeeRepository.java
public interface EmployeeRepository extends PagingAndSortingRepository<Employee, Integer> {
Employee findByCode(String code);
}
OrderRepository.java
public interface OrderRepository extends PagingAndSortingRepository<Order, Long> {
}
4.2、配置类
首先我们需要配置自定义的 RocketMQTemplate,最重要的是设置 TransactionMQProducer 的生产者组名称,这里是custom_transactional_group。
CustomEmployeeRocketMQTemplate.java
package com.alian.multiTransactional.config;
import org.apache.rocketmq.spring.annotation.ExtRocketMQTemplateConfiguration;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
@ExtRocketMQTemplateConfiguration(group = "custom_employee_transactional_group")
public class CustomEmployeeRocketMQTemplate extends RocketMQTemplate {
}
CustomOrderRocketMQTemplate.java
package com.alian.multiTransactional.config;
import org.apache.rocketmq.spring.annotation.ExtRocketMQTemplateConfiguration;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
@ExtRocketMQTemplateConfiguration(group = "custom_order_transactional_group")
public class CustomOrderRocketMQTemplate extends RocketMQTemplate {
}
-
@ExtRocketMQTemplateConfiguration:用于标识一个类是用于配置扩展的 RocketMQTemplate。通过在这个类上使用 @ExtRocketMQTemplateConfiguration 注解,可以实现自定义的 RocketMQTemplate 配置。
-
group: 这是 @ExtRocketMQTemplateConfiguration 注解的一个参数,用于指定 RocketMQ 生产者组的名称。上面分别指定的是名称为 custom_employee_transactional_group 和 custom_order_transactional_group。
4.4、监听器
4.4.1、员工事务监听器
EmployeeTransactionListener.java
package com.alian.multiTransactional.listener;
import com.alian.multiTransactional.domain.Employee;
import com.alian.multiTransactional.repository.EmployeeRepository;
import com.alibaba.fastjson.JSON;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
@Slf4j
@RocketMQTransactionListener(rocketMQTemplateBeanName = "customEmployeeRocketMQTemplate")
public class EmployeeTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private EmployeeRepository employeeRepository;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
try {
log.info("事务消息Headers为:{}", message.getHeaders());
String payload = new String((byte[]) message.getPayload());
log.info("事务消息为:{}", payload);
Employee employee = JSON.parseObject(payload, Employee.class);
employee.setId(null);
Employee save = employeeRepository.save(employee);
if (save.getId() != null) {
log.info("保存员工成功:{}", save.getId());
return RocketMQLocalTransactionState.COMMIT;
}
log.info("保存员工失败:{}", save.getId());
} catch (Exception e) {
log.error("发送事务消息异常:{}", e.getMessage());
return RocketMQLocalTransactionState.UNKNOWN;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
String id = message.getHeaders().get("rocketmq_KEYS",String.class);
log.info("事务消息key为:{}", id);
Employee employee = employeeRepository.findByCode(id);
if (employee == null) {
return RocketMQLocalTransactionState.ROLLBACK;
}
return RocketMQLocalTransactionState.COMMIT;
}
}
如果我们通过下面的模板发送事务消息就出触发上面的监听器
@Autowired
private CustomEmployeeRocketMQTemplate customEmployeeRocketMQTemplate;
4.4.2、订单事务监听器
OrderTransactionListener.java
package com.alian.multiTransactional.listener;
import com.alian.multiTransactional.domain.Order;
import com.alian.multiTransactional.repository.OrderRepository;
import com.alibaba.fastjson.JSON;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import java.util.Optional;
@Slf4j
@RocketMQTransactionListener(rocketMQTemplateBeanName = "customOrderRocketMQTemplate")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderRepository orderRepository;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
try {
log.info("事务消息Headers为:{}", message.getHeaders());
String payload = new String((byte[]) message.getPayload());
log.info("事务消息为:{}", payload);
Order order = JSON.parseObject(payload, Order.class);
Order save = orderRepository.save(order);
if (save.getId() != null) {// 随意判断的
log.info("保存订单成功:{}", save.getId());
return RocketMQLocalTransactionState.COMMIT;
}
log.info("保存订单失败:{}", save.getId());
return RocketMQLocalTransactionState.ROLLBACK;
} catch (Exception e) {
log.error("发送事务消息异常:{}", e.getMessage());
return RocketMQLocalTransactionState.UNKNOWN;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
Long id = message.getHeaders().get("rocketmq_KEYS", Long.class);
log.info("事务消息key为:{}", id);
Optional<Order> order = orderRepository.findById(id);
if (order.isPresent()) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
如果我们通过下面的模板发送事务消息就出触发上面的监听器
@Autowired
private CustomOrderRocketMQTemplate customOrderRocketMQTemplate;
五、测试
5.1、员工事务消息
@Slf4j
@SpringBootTest
public class SendEmployeeTransactionalMessageTest {
@Autowired
private CustomEmployeeRocketMQTemplate customEmployeeRocketMQTemplate;
@Test
public void sendMessageInTransaction() {
// 计算过期时间戳
String uuid = UUID.randomUUID().toString().replace("-", "");
String topic = "transaction_message_topic";
log.info("uuid={}", uuid);
String code = "BAT10015";
JSONObject json = new JSONObject();
json.put("code", code);
json.put("empName", "张若尘");
json.put("age", "25");
json.put("salary", "15000");
json.put("department", "测试部");
json.put("hireDate", "2020-05-21");
Message<JSONObject> rocketMessage = MessageBuilder.withPayload(json)
.setHeader(RocketMQHeaders.TRANSACTION_ID, uuid)
.setHeader(RocketMQHeaders.KEYS, code)
.build();
TransactionSendResult transactionSendResult = customEmployeeRocketMQTemplate.sendMessageInTransaction(topic, rocketMessage, null);
log.info("【发送状态】:{}", transactionSendResult.getLocalTransactionState());
}
@AfterEach
public void waiting() {
try {
Thread.sleep(40000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
事务消息Headers为:{rocketmq_TOPIC=transaction_message_topic, rocketmq_FLAG=0, __transactionId__=7F00000139E818B4AAC25173BC240000, rocketmq_TRANSACTION_ID=7F00000139E818B4AAC25173BC240000, rocketmq_KEYS=BAT10015, id=ec467820-0d1f-7677-ee6b-872ee07afa87, TRANSACTION_ID=7fe2f541b79c4d348c5241cf1f1b783c, contentType=application/json, timestamp=1710588939327}
事务消息为:{"hireDate":"2020-05-21","code":"BAT10015","empName":"张若尘","salary":"15000","department":"测试部","age":"25"}
保存员工成功:28
【发送状态】:COMMIT_MESSAGE
接收到事务消息:{"hireDate":"2020-05-21","code":"BAT10015","empName":"张若尘","salary":"15000","department":"测试部","age":"25"}
5.2、订单事务消息
@Slf4j
@SpringBootTest
public class SendOrderTransactionalMessageTest {
@Autowired
private CustomOrderRocketMQTemplate customOrderRocketMQTemplate;
@Test
public void sendMessageInTransactionWithCustom() {
String topic = "transaction_message_topic";
Long id = System.currentTimeMillis();
JSONObject json = new JSONObject();
json.put("id", id);
json.put("userId", 10086);
json.put("goodsId", 20000);
json.put("price", 10000);
json.put("num", 1);
json.put("title", "话费");
json.put("orderStatus", "01");
Message<JSONObject> rocketMessage = MessageBuilder.withPayload(json)
.setHeader(RocketMQHeaders.KEYS, id)
.build();
TransactionSendResult transactionSendResult = customOrderRocketMQTemplate.sendMessageInTransaction(topic, rocketMessage, null);
log.info("【发送状态】:{}", transactionSendResult.getLocalTransactionState());
}
@AfterEach
public void waiting() {
try {
Thread.sleep(40000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
事务消息Headers为:{rocketmq_TOPIC=transaction_message_topic, rocketmq_FLAG=0, __transactionId__=7F0000014B8018B4AAC251581EC40000, rocketmq_TRANSACTION_ID=7F0000014B8018B4AAC251581EC40000, rocketmq_KEYS=1710587129417, id=e4435d7a-dd9e-abb4-b89b-9b268cc1d67d, contentType=application/json, timestamp=1710587129570}
事务消息为:{"goodsId":20000,"price":10000,"num":1,"orderStatus":"01","id":1710587129417,"title":"话费","userId":10086}
保存订单成功:1710587129417
【发送状态】:COMMIT_MESSAGE
接收到事务消息:{"goodsId":20000,"price":10000,"num":1,"orderStatus":"01","id":1710587129417,"title":"话费","userId":10086}