RocketMQ笔记(十)SpringBoot整合RocketMQ多事务消息

一、简介

  在之前的文章中,我讲过了发送事务消息,好像是通过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_groupcustom_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}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值