安装搭建 Rocketmq 服务器
搭建单机 Rocketmq 服务器笔记:
《RocketMQ (一) 安装》
搭建双主双从同步复制 Rocketmq 服务器笔记:
《RocketMQ (二) 双主双从同步复制集群方案》
基于 Rocketmq 可靠消息的分布式事务方案原理
Rocketmq事务消息笔记:
《RocketMQ 发送事务消息原理分析和代码实现》
准备订单项目案例
新建 rocketmq-dtx 工程
新建 Empty Project:
工程命名为
rocketmq-dtx
,存放到任意文件夹下:
导入订单项目,无事务版本
下载项目代码
- 访问 git 仓库 https://gitee.com/benwang6/seata-samples
- 访问项目标签
- 下载无事务版
解压到 rocketmq-dtx 目录
压缩文件中的 7 个项目目录解压缩到 rocketmq-dtx
目录:
导入项目
在 idea 中按两下 shift
键,搜索 add maven projects
,打开 maven 工具:
然后选择 rocketmq-dex
工程目录下的 7 个项目的 pom.xml
导入:
order 添加事务状态表
Rocketmq收到事务消息后,会等待生产者提交或回滚该消息。如果无法得到生产者的提交或回滚指令,则会主动向生产者询问消息状态,称为回查。
在 order 项目中,为了让Rocketmq可以回查到事务的状态,需要记录事务的状态,所以我们添加一个事务的状态表来记录事务状态。
修改 db-init
项目中的 order.sql
文件,创建 tx_table
表:
drop database if exists `seata_order`;
CREATE DATABASE `seata_order` charset utf8;
use `seata_order`;
CREATE TABLE `order` (
`id` bigint(11) NOT NULL,
`user_id` bigint(11) DEFAULT NULL COMMENT '用户id',
`product_id` bigint(11) DEFAULT NULL COMMENT '产品id',
`count` int(11) DEFAULT NULL COMMENT '数量',
`money` decimal(11,0) DEFAULT NULL COMMENT '金额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' AFTER `money` ;
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
CREATE TABLE IF NOT EXISTS segment
(
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
VERSION BIGINT DEFAULT 0 NOT NULL COMMENT '版本号',
business_type VARCHAR(63) DEFAULT '' NOT NULL COMMENT '业务类型,唯一',
max_id BIGINT DEFAULT 0 NOT NULL COMMENT '当前最大id',
step INT DEFAULT 0 NULL COMMENT '步长',
increment INT DEFAULT 1 NOT NULL COMMENT '每次id增量',
remainder INT DEFAULT 0 NOT NULL COMMENT '余数',
created_at BIGINT UNSIGNED NOT NULL COMMENT '创建时间',
updated_at BIGINT UNSIGNED NOT NULL COMMENT '更新时间',
CONSTRAINT uniq_business_type UNIQUE (business_type)
) CHARSET = utf8mb4
ENGINE INNODB COMMENT '号段表';
INSERT INTO segment
(VERSION, business_type, max_id, step, increment, remainder, created_at, updated_at)
VALUES (1, 'order_business', 1000, 1000, 1, 0, NOW(), NOW());
CREATE TABLE tx_table(
`xid` char(32) PRIMARY KEY COMMENT '事务id',
`status` int COMMENT '0-提交,1-回滚,2-未知',
`created_at` BIGINT UNSIGNED NOT NULL COMMENT '创建时间'
);
运行 db-init 项目,会创建这个表:
order 发送事务消息,并执行本地事务
Rocketmq 中添加 Topic
使用 order-topic
来收发消息,在 Rocketmq 服务器上创建这个 Topic:
order-parent 中添加 rocketmq 起步依赖
修改 pom.xml
添加以下内容:
在 properties
中设置 rocketmq 起步依赖的版本
<rocketmq-spring-boot-starter.version>2.1.0</rocketmq-spring-boot-starter.version>
添加 rocketmq 起步依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring-boot-starter.version}</version>
</dependency>
order 项目中添加 rocketmq 连接信息配置:
修改 order 项目的 application.yml,添加 NameServer 地址,指定生产者组名:
rocketmq:
name-server: 192.168.64.151:9876;192.168.64.152:9876
producer:
group: order-group
添加 TxMapper
访问事务状态表
事务状态保存到 tx_table
表,在 TxMapper
接口和 TxMapper.xml
中添加事务状态数据的读写方法。
本地事务执行后要保存事务信息(事务id、事务状态)到数据库,以便之后进行事务回查,首先创建封装事务信息的类 TxInfo
:
package cn.tedu.order.tx;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TxInfo {
private String xid;
private Long created;
private Integer status;
}
TxMapper
接口:
package cn.tedu.order.mapper;
import cn.tedu.order.tx.TxInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface TxMapper extends BaseMapper<TxInfo> {
Boolean exists(String xid);
}
TxMapper.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="cn.tedu.order.mapper.TxMapper" >
<resultMap id="BaseResultMap" type="cn.tedu.order.tx.TxInfo" >
<id column="xid" property="xid" jdbcType="CHAR" />
<result column="created_at" property="created" jdbcType="BIGINT" />
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="insert">
INSERT INTO `tx_table`(`xid`,`created_at`,`status`) VALUES(#{xid},#{created},#{status});
</insert>
<select id="exists" resultType="boolean">
SELECT COUNT(1) FROM tx_table WHERE xid=#{xid};
</select>
<select id="selectById" resultMap="BaseResultMap">
SELECT `xid`,`created_at`,`status` FROM tx_table WHERE xid=#{xid};
</select>
</mapper>
Json处理工具
发送事务消息时,我们把事务对象序列化成 Json 字符串再发送。这里先添加一个工具 JsonUtil
用来处理 Json:
package cn.tedu.order.util;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class JsonUtil {
private static ObjectMapper ma