基于 Hmily 实战 TCC 分布式事务之三

大纲

电商系统 TCC 事务实战(编码)

电商系统的案例(SpringCloud)

特别注意

本节中的案例代码仅用于演示如何通过 SpringCloud + Hmily 实现 TCC 分布式事务,并未全面涵盖 TCC 在实际运行过程中可能出现的各种事务异常处理场景(如空回滚、防悬挂、第二阶段重复提交等问题),而这些事务异常问题在生产环境中是必须重点考虑并妥善处理的

代码下载

  • 本节完整的案例代码可以直接从 GitHub 下载对应章节 distributed-transaction-02

版本说明

组件版本说明
JDK11
MySQL5.7.44
Hmily2.1.1
SpringBoot2.3.7.RELEASEHmily 官方并不支持 SpringBoot 3.x
SpringCloudHoxton.SR9

Hmily 版本说明

  • 在 Maven 中央仓库中,Hmily 最新的版本是 2.1.2(2022 年 12 月发布)。特别注意,Maven 中央仓库中 hmily-spring-boot-starter-springcloud 的最新版本是 2.1.1,并没有 2.1.2 版本;如果需要 2.1.2 版本,可以从 这里(不稳定,生产环境慎用) 直接下载,并通过 mvn install 命令安装到本地。

目录结构说明

  • 在本案例中,电商系统的核心模块如下所示:
模块名称端口说明
eureka-server8761Eureka 注册中心
hmily-demo-common基础模块(Entity、DTO、Mapper)
hmily-demo-tcc-springcloud-order8090订单模块
hmily-demo-tcc-springcloud-account8885账户模块
hmily-demo-tcc-springcloud-inventory8883库存模块
  • 在本案例中,电商系统的整体目录结构如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
distributed-transaction-02
├── hmily-demo-common
│   ├── pom.xml
│   └── src
│   └── main
│   ├── java
│   │   └── org
│   │   └── dromara
│   │   └── hmily
│   │   └── demo
│   │   └── common
│   │   ├── account
│   │   │   ├── dto
│   │   │   │   ├── AccountDTO.java
│   │   │   │   └── AccountNestedDTO.java
│   │   │   ├── entity
│   │   │   │   └── AccountDO.java
│   │   │   └── mapper
│   │   │   └── AccountMapper.java
│   │   ├── inventory
│   │   │   ├── dto
│   │   │   │   └── InventoryDTO.java
│   │   │   ├── entity
│   │   │   │   └── InventoryDO.java
│   │   │   └── mapper
│   │   │   └── InventoryMapper.java
│   │   └── order
│   │   ├── entity
│   │   │   └── Order.java
│   │   ├── enums
│   │   │   └── OrderStatusEnum.java
│   │   └── mapper
│   │   └── OrderMapper.java
│   └── resources
│   └── mybatis
│   └── mybatis-config.xml
├── hmily-demo-tcc-springcloud-account
│   ├── pom.xml
│   └── src
│   └── main
│   ├── java
│   │   └── org
│   │   └── dromara
│   │   └── hmily
│   │   └── demo
│   │   └── springcloud
│   │   └── account
│   │   ├── client
│   │   │   └── InventoryClient.java
│   │   ├── controller
│   │   │   └── AccountController.java
│   │   ├── HmilyAccountApplication.java
│   │   └── service
│   │   ├── AccountService.java
│   │   └── impl
│   │   └── AccountServiceImpl.java
│   └── resources
│   ├── application.yml
│   └── hmily.yml
├── hmily-demo-tcc-springcloud-eureka
│   ├── pom.xml
│   └── src
│   └── main
│   ├── java
│   │   └── org
│   │   └── dromara
│   │   └── hmily
│   │   └── demo
│   │   └── springcloud
│   │   └── eureka
│   │   └── EurekaServerApplication.java
│   └── resources
│   └── application.yml
├── hmily-demo-tcc-springcloud-inventory
│   ├── pom.xml
│   └── src
│   └── main
│   ├── java
│   │   └── org
│   │   └── dromara
│   │   └── hmily
│   │   └── demo
│   │   └── springcloud
│   │   └── inventory
│   │   ├── controller
│   │   │   └── InventoryController.java
│   │   ├── HmilyInventoryApplication.java
│   │   └── service
│   │   ├── impl
│   │   │   └── InventoryServiceImpl.java
│   │   └── InventoryService.java
│   └── resources
│   ├── application.yml
│   └── hmily.yml
├── hmily-demo-tcc-springcloud-order
│   ├── pom.xml
│   └── src
│   └── main
│   ├── java
│   │   └── org
│   │   └── dromara
│   │   └── hmily
│   │   └── demo
│   │   └── springcloud
│   │   └── order
│   │   ├── client
│   │   │   ├── AccountClient.java
│   │   │   └── InventoryClient.java
│   │   ├── configuration
│   │   │   └── SwaggerConfig.java
│   │   ├── controller
│   │   │   └── OrderController.java
│   │   ├── HmilyOrderApplication.java
│   │   └── service
│   │   ├── impl
│   │   │   ├── OrderServiceImpl.java
│   │   │   └── PaymentServiceImpl.java
│   │   ├── OrderService.java
│   │   └── PaymentService.java
│   └── resources
│   ├── application.yml
│   └── hmily.yml
└── pom.xml

TCC 模型说明

在本案例中,电商系统使用的 TCC 模型如下图所示:

整体部署架构

数据库初始化

  • 创建数据库
1
2
3
4
5
6
7
8
-- 创建订单库
CREATE DATABASE IF NOT EXISTS `hmily_order` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ;

-- 创建账户库
CREATE DATABASE IF NOT EXISTS `hmily_account` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ;

-- 创建库存库
CREATE DATABASE IF NOT EXISTS `hmily_stock` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin ;
  • 创建订单表(hmily_order.order
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 切换数据库
USE `hmily_order`;

-- 创建订单表
CREATE TABLE `order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`create_time` datetime NOT NULL,
`number` varchar(20) COLLATE utf8mb4_bin NOT NULL,
`status` tinyint(4) NOT NULL,
`product_id` varchar(128) NOT NULL,
`total_amount` decimal(10,0) NOT NULL,
`count` int(4) NOT NULL,
`user_id` varchar(128) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
  • 创建账户表(hmily_account.account
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 切换数据库
USE `hmily_account`;

-- 创建账户表
CREATE TABLE `account` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` varchar(128) NOT NULL,
`balance` decimal(10,0) NOT NULL COMMENT '用户余额',
`freeze_amount` decimal(10,0) NOT NULL COMMENT '冻结金额,扣款暂存余额',
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- 插入数据
insert into `account`(`id`,`user_id`,`balance`,`freeze_amount`,`create_time`,`update_time`) values(1,'10000', 10000000,0,'2017-09-18 14:54:22',NULL);
  • 创建库存表(hmily_stock.inventory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 切换数据库
USE `hmily_stock`;

-- 创建库存表
CREATE TABLE `inventory` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_id` VARCHAR(128) NOT NULL,
`total_inventory` int(10) NOT NULL COMMENT '总库存',
`lock_inventory` int(10) NOT NULL COMMENT '锁定库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

-- 插入数据
insert into `inventory`(`id`,`product_id`,`total_inventory`,`lock_inventory`) values(1,'1',10000000,0);

Hmily 数据库的初始化

当 Hmily 使用 MySQL 来存储事务日志时,只要项目引入 hmily-spring-boot-starter-springcloud 依赖,并且在 hmily.yml 中添加 MySQL 相关的配置,那么 Hmily 在执行分布式事务时,默认会自动创建相关的数据库(hmily)和数据库表,比如 MySQL 初始化的 SQL 脚本源文件可以查看 GitHub 仓库。值得一提的是,不同版本的 Hmily,其数据库初始化的 SQL 脚本可能有差异。

核心代码

父 Pom 模块

提示

父 Pom 模块主要用于统一管理和定义项目中各类核心依赖的版本号,避免在各个子模块中重复声明版本,从而提升依赖管理的可维护性和一致性。

  • Maven 的 XML 配置内容(pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
<relativePath/>
</parent>

<properties>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<springfox.version>2.6.1</springfox.version>
<mysql.version>8.0.22</mysql.version>
<mybatis.version>1.1.1</mybatis.version>
<hmily.version>2.1.1</hmily.version>
</properties>

<dependencyManagement>
<dependencies>
<!-- SpringCloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Hmily -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-spring-boot-starter-springcloud</artifactId>
<version>${hmily.version}</version>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-bean-validators</artifactId>
<version>${springfox.version}</version>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
注册中心模块
模块配置
  • Maven 的 XML 配置内容(pom.xml
1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<!-- Eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
  • SpringBoot 的 YML 配置内容(application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
server:
port: 8761

spring:
application:
name: springcloud-eureka

# Eureka 服务端配置
eureka:
instance:
# 实例主机名,使用环境变量 hostname,默认为 localhost
hostname: ${hostname:localhost}
# 优先使用 IP 地址而非主机名进行注册和通信
prefer-ip-address: true
# 客户端向服务端发送心跳续约的间隔时间(秒),用于保持注册状态
lease-renewal-interval-in-seconds: 30
# 服务端在收到最后一次心跳后等待的时间(秒),超时后将实例标记为下线
lease-expiration-duration-in-seconds: 90
server:
# 更新 Eureka 集群节点列表的时间间隔(毫秒)
peer-eureka-nodes-update-interval-ms: 60000
# 是否开启自我保护模式,关闭后当大量实例异常时会直接剔除
enable-self-preservation: false
# 服务端清理失效实例的任务执行间隔(毫秒)
eviction-interval-timer-in-ms: 60000
client:
# 是否将本实例注册到 Eureka 服务器,单机模式通常设为 false,集群模式必须设置为 true
register-with-eureka: false
# 是否从 Eureka 服务器获取注册表信息,单机模式通常设为 false,集群模式必须设置为 true
fetch-registry: false
# Eureka 服务器的服务地址
service-url:
default-zone: http://${eureka.instance.hostname}:${server.port}/eureka/
# 轮询更新 Eureka 服务地址列表的时间间隔(秒)
eureka-service-url-poll-interval-seconds: 60
# 是否启用健康检查机制
healthcheck:
enabled: true

# Spring Boot Actuator 端点配置
endpoints:
health:
# 是否对健康端点进行敏感信息保护,设为 false 允许无鉴权访问
sensitive: false
模块代码
  • SpringBoot 的主启动类
1
2
3
4
5
6
7
8
9
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class);
}

}
基础模块
核心配置
  • Maven 的 XML 配置内容(pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<dependencies>
<!-- Hmily -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-annotation</artifactId>
<version>${hmily.version}</version>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
</dependencies>
  • MyBatis 的 XML 配置内容(mybatis-config.xml
1
2
3
4
5
6
7
8
9
<configuration>
<properties>
<property name="dialect" value="mysql"/>
<property name="pageSqlId" value=".*Page$"/>
</properties>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
核心代码
  • 订单的 Entity 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Data
public class Order implements Serializable {

private static final long serialVersionUID = -8551347266419380333L;

private Integer id;

/**
* 创建时间
*/
private Date createTime;

/**
* 订单编号
*/
private String number;

/**
* 订单状态
*/
private Integer status;

/**
* 商品id
*/
private String productId;

/**
* 付款金额
*/
private BigDecimal totalAmount;

/**
* 购买数量
*/
private Integer count;

/**
* 购买人
*/
private String userId;
}
  • 订单状态的枚举类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {

/**
* Not pay order status enum.
*/
NOT_PAY(1, "未支付"),

/**
* Paying order status enum.
*/
PAYING(2, "支付中"),

/**
* Pay fail order status enum.
*/
PAY_FAIL(3, "支付失败"),

/**
* Pay success order status enum.
*/
PAY_SUCCESS(4, "支付成功");

private final int code;

private final String desc;
}
  • 订单的 Mapper 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public interface OrderMapper {

/**
* 保存订单.
*
* @param order 订单对象
* @return rows int
*/
@Insert(" insert into `order` (create_time,number,status,product_id,total_amount,count,user_id) " +
" values ( #{createTime},#{number},#{status},#{productId},#{totalAmount},#{count},#{userId})")
int save(Order order);

/**
* 更新订单.
*
* @param order 订单对象
* @return rows int
*/
@Update("update `order` set status = #{status} where number = #{number}")
int update(Order order);

/**
* 获取所有的订单
*
* @return List<Order> list
*/
@Select("select * from order")
List<Order> listAll();
}
  • 账户的 Entity 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class AccountDO implements Serializable {

private static final long serialVersionUID = -81849676368907419L;

private Integer id;

private String userId;

private BigDecimal balance;

private BigDecimal freezeAmount;

private Date createTime;

private Date updateTime;
}
  • 账户的 DTO 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class AccountDTO implements Serializable {

private static final long serialVersionUID = 7223470850578998427L;

/**
* 用户id
*/
private String userId;

/**
* 扣款金额
*/
private BigDecimal amount;

}
  • 账户的 Mapper 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public interface AccountMapper {

/**
* Update int.
*
* @param accountDTO the account dto
* @return the int
*/
@Update("update account set balance = balance - #{amount}," +
" freeze_amount= freeze_amount + #{amount} ,update_time = now()" +
" where user_id =#{userId} and balance >= #{amount} ")
int update(AccountDTO accountDTO);

/**
* Confirm int.
*
* @param accountDTO the account dto
* @return the int
*/
@Update("update account set " +
" freeze_amount= freeze_amount - #{amount}" +
" where user_id =#{userId} and freeze_amount >= #{amount} ")
int confirm(AccountDTO accountDTO);

/**
* Cancel int.
*
* @param accountDTO the account dto
* @return the int
*/
@Update("update account set balance = balance + #{amount}," +
" freeze_amount= freeze_amount - #{amount} " +
" where user_id =#{userId} and freeze_amount >= #{amount}")
int cancel(AccountDTO accountDTO);

/**
* 根据userId获取用户账户信息
*
* @param userId 用户id
* @return AccountDO account do
*/
@Select("select id,user_id,balance, freeze_amount from account where user_id =#{userId} limit 1")
AccountDO findByUserId(String userId);
}
  • 库存的 Entity 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class InventoryDO implements Serializable {

private static final long serialVersionUID = 6957734749389133832L;

private Integer id;

/**
* 商品id.
*/
private String productId;

/**
* 总库存.
*/
private Integer totalInventory;

/**
* 锁定库存.
*/
private Integer lockInventory;
}
  • 库存的 DTO 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class InventoryDTO implements Serializable {

private static final long serialVersionUID = 8229355519336565493L;

/**
* 商品id.
*/
private String productId;

/**
* 数量.
*/
private Integer count;
}
  • 库存的 Mapper 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public interface InventoryMapper {

/**
* Decrease int.
*
* @param inventoryDTO the inventory dto
* @return the int
*/
@Update("update inventory set total_inventory = total_inventory - #{count}," +
" lock_inventory= lock_inventory + #{count} " +
" where product_id =#{productId} and total_inventory > 0 ")
int decrease(InventoryDTO inventoryDTO);

/**
* Confirm int.
*
* @param inventoryDTO the inventory dto
* @return the int
*/
@Update("update inventory set " +
" lock_inventory = lock_inventory - #{count} " +
" where product_id =#{productId} and lock_inventory > 0 ")
int confirm(InventoryDTO inventoryDTO);

/**
* Cancel int.
*
* @param inventoryDTO the inventory dto
* @return the int
*/
@Update("update inventory set total_inventory = total_inventory + #{count}," +
" lock_inventory= lock_inventory - #{count} " +
" where product_id =#{productId} and lock_inventory > 0 ")
int cancel(InventoryDTO inventoryDTO);

/**
* Find by product id inventory do.
*
* @param productId the product id
* @return the inventory do
*/
@Select("select id,product_id,total_inventory ,lock_inventory from inventory where product_id =#{productId}")
InventoryDO findByProductId(String productId);
}
订单模块
模块配置
  • Maven 的 XML 配置内容(pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<dependencies>
<dependency>
<groupId>com.distributed.transaction</groupId>
<artifactId>hmily-demo-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- Eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- Hmily -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-spring-boot-starter-springcloud</artifactId>
<version>${hmily.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-bean-validators</artifactId>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
  • SpringBoot 的 YML 配置内容(application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
server:
port: 8090
address: 0.0.0.0
servlet:
context-path: /

spring:
main:
allow-bean-definition-overriding: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.2.140:3306/hmily_order?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
application:
name: order-service

mybatis:
type-aliases-package: org.dromara.hmily.demo.common.order.entity
config-location: classpath:mybatis/mybatis-config.xml

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
instance-id: ${spring.application.name}-${server.port} # 自定义服务名称
prefer-ip-address: true # 将IP注册到Eureka Server上,若不配置默认使用机器的主机名

# Ribbon 的负载均衡策略
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
MaxAutoRetriesNextServer: 0
MaxAutoRetries: 0
ReadTimeout: 10000

# Feign 配置
feign:
hystrix:
enabled: false # 在 Feign 中是否开启 Hystrix 功能,默认情况下 Feign 不开启 hystrix 功能

logging:
level:
root: info
org.apache.ibatis: debug
org.dromara.hmily.demo.bonuspoint: debug
org.dromara.hmily.demo.lottery: debug
org.dromara.hmily.demo: debug
io.netty: info
  • Hmily 的 YML 配置内容(hmily.yml),可以根据实际业务需求,适当调整 Hmily 的配置参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Hmily 服务端相关配置
hmily:
# Hmily Server 配置
server:
# 配置模式:local 表示本地配置模式
configMode: local
# 当前应用名称(Hmily Server 使用)
appName: order-sc

# 只有 server.configMode 为 local 时才会读取以下配置
config:
# 当前应用名称(业务侧使用)
appName: order-sc
# 序列化方式,推荐使用 kryo;支持 hessian、protostuff、jdk;测试表现:kryo > hessian > protostuff > jdk
serializer: kryo
# 上下文传递模式(threadLocal / transmittableThreadLocal)
contextTransmittalMode: threadLocal
# 定时任务线程池最大线程数,高并发场景下可适当调大
scheduledThreadMax: 16
# 恢复任务首次延迟时间(秒)
scheduledRecoveryDelay: 60
# 清理任务执行间隔(秒)
scheduledCleanDelay: 60
# 物理删除任务执行间隔(秒)
scheduledPhyDeletedDelay: 600
# 定时任务初始化延迟时间(秒)
scheduledInitDelay: 30
# 事务恢复延迟时间(秒),强烈建议大于 RPC 调用的超时时间,否则可能误触发补偿;比如设置为:≥ RPC 超时 + 网络抖动余量
recoverDelayTime: 60
# 清理延迟时间(秒)
cleanDelayTime: 180
# 单次处理事务的最大数量限制
limit: 200
# 最大重试次数,默认 10 次,业务服务宕机时定时任务在 retryMax 次内执行 cancel 或 confirm
retryMax: 10
# Disruptor 缓冲区大小,高并发时可调大,建议为 2 的幂次方
bufferSize: 8192
# 消费者线程数
consumerThreads: 16
# 是否启用异步事务日志存储
asyncRepository: true
# 是否自动建表(仅关系型数据库有效)
autoSql: true
# 是否启用物理删除
phyDeleted: true
# 事务数据保留天数
storeDays: 3
# 事务日志存储类型(mysql / postgresql / mongodb / redis / zookeeper / file 等)
repository: mysql

# Hmily 仓库相关配置
repository:
# 数据库相关配置
database:
# JDBC 驱动类名
driverClassName: com.mysql.cj.jdbc.Driver
# 数据库连接 URL
url: jdbc:mysql://192.168.2.140:3306/hmily?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
# 数据库用户名
username: root
# 数据库密码
password: 123456
# 连接池最大连接数
maxActive: 20
# 连接池最小空闲连接数
minIdle: 10
# 获取连接超时时间(毫秒)
connectionTimeout: 30000
# 连接空闲超时时间(毫秒)
idleTimeout: 600000
# 连接最大生命周期(毫秒)
maxLifetime: 1800000
模块代码
  • 账户的 Feign 客户端(这里需要添加 @Hmily 注解,用于标记参与 TCC 分布式事务的业务方法,声明该方法由 Hmily 框架接管并按 Try / Confirm / Cancel 事务模型执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@FeignClient(value = "account-service")
public interface AccountClient {

/**
* 用户账户付款.
*
* @param accountDO 实体类
* @return true 成功
*/
@RequestMapping("/account-service/account/payment")
@Hmily
Boolean payment(@RequestBody AccountDTO accountDO);

/**
* 获取用户账户信息.
*
* @param userId 用户id
* @return AccountDO big decimal
*/
@RequestMapping("/account-service/account/findByUserId")
BigDecimal findByUserId(@RequestParam("userId") String userId);

/**
* Mock with try exception boolean.
*
* @param accountDO the account do
* @return the boolean
*/
@Hmily
@RequestMapping("/account-service/account/mockWithTryException")
Boolean mockWithTryException(@RequestBody AccountDTO accountDO);

/**
* Mock with try timeout boolean.
*
* @param accountDO the account do
* @return the boolean
*/
@Hmily
@RequestMapping("/account-service/account/mockWithTryTimeout")
Boolean mockWithTryTimeout(@RequestBody AccountDTO accountDO);

/**
* Payment with nested boolean.
*
* @param nestedDTO the nested dto
* @return the boolean
*/
@Hmily
@RequestMapping("/account-service/account/paymentWithNested")
Boolean paymentWithNested(@RequestBody AccountNestedDTO nestedDTO);

/**
* Payment with nested exception boolean.
*
* @param nestedDTO the nested dto
* @return the boolean
*/
@Hmily
@RequestMapping("/account-service/account/paymentWithNestedException")
Boolean paymentWithNestedException(@RequestBody AccountNestedDTO nestedDTO);
}
  • 库存的 Feign 客户端(这里需要添加 @Hmily 注解,用于标记参与 TCC 分布式事务的业务方法,声明该方法由 Hmily 框架接管并按 Try / Confirm / Cancel 事务模型执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@FeignClient(value = "inventory-service")
public interface InventoryClient {

/**
* 库存扣减.
*
* @param inventoryDTO 实体对象
* @return true 成功
*/
@RequestMapping("/inventory-service/inventory/decrease")
@Hmily
Boolean decrease(@RequestBody InventoryDTO inventoryDTO);

/**
* Test decrease boolean.
*
* @param inventoryDTO the inventory dto
* @return the boolean
*/
@RequestMapping("/inventory-service/inventory/testDecrease")
Boolean testDecrease(@RequestBody InventoryDTO inventoryDTO);

/**
* 获取商品库存.
*
* @param productId 商品id
* @return InventoryDO integer
*/
@RequestMapping("/inventory-service/inventory/findByProductId")
Integer findByProductId(@RequestParam("productId") String productId);

/**
* 模拟库存扣减异常.
*
* @param inventoryDTO 实体对象
* @return true 成功
*/
@Hmily
@RequestMapping("/inventory-service/inventory/mockWithTryException")
Boolean mockWithTryException(@RequestBody InventoryDTO inventoryDTO);

/**
* 模拟库存扣减超时.
*
* @param inventoryDTO 实体对象
* @return true 成功
*/
@Hmily
@RequestMapping("/inventory-service/inventory/mockWithTryTimeout")
Boolean mockWithTryTimeout(@RequestBody InventoryDTO inventoryDTO);
}
  • 订单的 Service 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public interface OrderService {

/**
* 创建订单并且进行扣除账户余额支付,并进行库存扣减操作.
*
* @param count 购买数量
* @param amount 支付金额
* @return string string
*/
String orderPay(Integer count, BigDecimal amount);

/**
* 模拟在订单支付操作中,库存在try阶段中的库存异常.
*
* @param count 购买数量
* @param amount 支付金额
* @return string string
*/
String mockInventoryWithTryException(Integer count, BigDecimal amount);

/**
* Mock account with try exception string.
*
* @param count the count
* @param amount the amount
* @return the string
*/
String mockAccountWithTryException(Integer count, BigDecimal amount);

/**
* 模拟在订单支付操作中,库存在try阶段中的timeout.
*
* @param count 购买数量
* @param amount 支付金额
* @return string string
*/
String mockInventoryWithTryTimeout(Integer count, BigDecimal amount);

/**
* Mock account with try timeout string.
*
* @param count the count
* @param amount the amount
* @return the string
*/
String mockAccountWithTryTimeout(Integer count, BigDecimal amount);

/**
* Order pay with nested string.
*
* @param count the count
* @param amount the amount
* @return the string
*/
String orderPayWithNested(Integer count, BigDecimal amount);

/**
* Order pay with nested exception string.
*
* @param count the count
* @param amount the amount
* @return the string
*/
String orderPayWithNestedException(Integer count, BigDecimal amount);

/**
* 更新订单状态.
*
* @param order 订单实体类
*/
void updateOrderStatus(Order order);
}
  • 订单的 Service 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@Service("orderService")
public class OrderServiceImpl implements OrderService {

private static final Logger LOGGER = LoggerFactory.getLogger(OrderServiceImpl.class);

private final OrderMapper orderMapper;

private final PaymentService paymentService;

@Autowired(required = false)
public OrderServiceImpl(OrderMapper orderMapper, PaymentService paymentService) {
this.orderMapper = orderMapper;
this.paymentService = paymentService;
}

@Override
public String orderPay(Integer count, BigDecimal amount) {
Order order = saveOrder(count, amount);
long start = System.currentTimeMillis();
paymentService.makePayment(order);
System.out.println("hmily-cloud分布式事务耗时:" + (System.currentTimeMillis() - start));
return "success";
}

@Override
public String mockInventoryWithTryException(Integer count, BigDecimal amount) {
Order order = saveOrder(count, amount);
return paymentService.mockPaymentInventoryWithTryException(order);
}

@Override
public String mockAccountWithTryException(Integer count, BigDecimal amount) {
Order order = saveOrder(count, amount);
return paymentService.mockPaymentAccountWithTryException(order);
}

/**
* 模拟在订单支付操作中,库存在try阶段中的timeout
*
* @param count 购买数量
* @param amount 支付金额
* @return string
*/
@Override
public String mockInventoryWithTryTimeout(Integer count, BigDecimal amount) {
Order order = saveOrder(count, amount);
return paymentService.mockPaymentInventoryWithTryTimeout(order);
}

@Override
public String mockAccountWithTryTimeout(Integer count, BigDecimal amount) {
Order order = saveOrder(count, amount);
return paymentService.mockPaymentAccountWithTryTimeout(order);
}

@Override
public String orderPayWithNested(Integer count, BigDecimal amount) {
Order order = saveOrder(count, amount);
return paymentService.makePaymentWithNested(order);
}

@Override
public String orderPayWithNestedException(Integer count, BigDecimal amount) {
Order order = saveOrder(count, amount);
return paymentService.makePaymentWithNestedException(order);
}

@Override
public void updateOrderStatus(Order order) {
orderMapper.update(order);
}

private Order saveOrder(Integer count, BigDecimal amount) {
final Order order = buildOrder(count, amount);
orderMapper.save(order);
return order;
}

private Order buildOrder(Integer count, BigDecimal amount) {
LOGGER.info("构建订单对象");
Order order = new Order();
order.setCreateTime(new Date());
order.setNumber(String.valueOf(IdWorkerUtils.getInstance().createUUID()));
//demo中的表里只有商品id为 1的数据
order.setProductId("1");
order.setStatus(OrderStatusEnum.NOT_PAY.getCode());
order.setTotalAmount(amount);
order.setCount(count);
//demo中 表里面存的用户id为10000
order.setUserId("10000");
return order;
}
}
  • 订单支付的 Service 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public interface PaymentService {

/**
* 订单支付.
*
* @param order 订单实体
*/
void makePayment(Order order);

/**
* mock订单支付的时候库存异常.
*
* @param order 订单实体
* @return String string
*/
String mockPaymentInventoryWithTryException(Order order);

/**
* Mock payment account with try exception string.
*
* @param order the order
* @return the string
*/
String mockPaymentAccountWithTryException(Order order);

/**
* mock订单支付的时候库存超时.
*
* @param order 订单实体
* @return String string
*/
String mockPaymentInventoryWithTryTimeout(Order order);

/**
* Mock payment account with try timeout string.
*
* @param order the order
* @return the string
*/
String mockPaymentAccountWithTryTimeout(Order order);

/**
* Make payment with nested.
*
* @param order the order
* @return the string
*/
String makePaymentWithNested(Order order);

/**
* Make payment with nested exception.
*
* @param order the order
* @return the string
*/
String makePaymentWithNestedException(Order order);
}
  • 订单支付的 Service 实现类(这里需要使用 @HmilyTCC 注解,同时指定 confirmMethodcancelMethod 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
@Service
@SuppressWarnings("all")
public class PaymentServiceImpl implements PaymentService {

private static final Logger LOGGER = LoggerFactory.getLogger(PaymentServiceImpl.class);

private final OrderMapper orderMapper;

private final AccountClient accountClient;

private final InventoryClient inventoryClient;

@Autowired(required = false)
public PaymentServiceImpl(OrderMapper orderMapper,
AccountClient accountClient,
InventoryClient inventoryClient) {
this.orderMapper = orderMapper;
this.accountClient = accountClient;
this.inventoryClient = inventoryClient;
}

@Override
@HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public void makePayment(Order order) {
LOGGER.info("===========执行springcloud makePayment 扣减资金接口==========");
updateOrderStatus(order, OrderStatusEnum.PAYING);
// 检查数据
/**
final BigDecimal accountInfo = accountClient.findByUserId(order.getUserId());
final Integer inventoryInfo = inventoryClient.findByProductId(order.getProductId());
if (accountInfo.compareTo(order.getTotalAmount()) < 0) {
throw new HmilyRuntimeException("余额不足!");
}
if (inventoryInfo < order.getCount()) {
throw new HmilyRuntimeException("库存不足!");
}
*/
accountClient.payment(buildAccountDTO(order));
inventoryClient.decrease(buildInventoryDTO(order));
}

@Override
@HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public String mockPaymentInventoryWithTryException(Order order) {
LOGGER.info("===========执行springcloud mockPaymentInventoryWithTryException 扣减资金接口==========");
updateOrderStatus(order, OrderStatusEnum.PAYING);
//扣除用户余额
accountClient.payment(buildAccountDTO(order));
inventoryClient.mockWithTryException(buildInventoryDTO(order));
return "success";
}

@Override
@HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public String mockPaymentAccountWithTryException(Order order) {
LOGGER.info("===========执行springcloud mockPaymentAccountWithTryException 扣减资金接口==========");
updateOrderStatus(order, OrderStatusEnum.PAYING);
accountClient.mockWithTryException(buildAccountDTO(order));
return "success";
}

@Override
@HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public String mockPaymentInventoryWithTryTimeout(Order order) {
LOGGER.info("===========执行springcloud mockPaymentInventoryWithTryTimeout 扣减资金接口==========");
updateOrderStatus(order, OrderStatusEnum.PAYING);
accountClient.payment(buildAccountDTO(order));
inventoryClient.mockWithTryTimeout(buildInventoryDTO(order));
return "success";
}

@Override
@HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public String mockPaymentAccountWithTryTimeout(Order order) {
updateOrderStatus(order, OrderStatusEnum.PAYING);
accountClient.mockWithTryTimeout(buildAccountDTO(order));
return "success";
}

@Override
@HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public String makePaymentWithNested(Order order) {
updateOrderStatus(order, OrderStatusEnum.PAYING);
final BigDecimal balance = accountClient.findByUserId(order.getUserId());
if (balance.compareTo(order.getTotalAmount()) <= 0) {
throw new HmilyRuntimeException("余额不足!");
}
accountClient.paymentWithNested(buildAccountNestedDTO(order));
return "success";
}

@Override
@HmilyTCC(confirmMethod = "confirmOrderStatus", cancelMethod = "cancelOrderStatus")
public String makePaymentWithNestedException(Order order) {
updateOrderStatus(order, OrderStatusEnum.PAYING);
final BigDecimal balance = accountClient.findByUserId(order.getUserId());
if (balance.compareTo(order.getTotalAmount()) <= 0) {
throw new HmilyRuntimeException("余额不足!");
}
accountClient.paymentWithNestedException(buildAccountNestedDTO(order));
return "success";
}

public void confirmOrderStatus(Order order) {
updateOrderStatus(order, OrderStatusEnum.PAY_SUCCESS);
LOGGER.info("=========进行订单confirm操作完成================");
}

public void cancelOrderStatus(Order order) {
updateOrderStatus(order, OrderStatusEnum.PAY_FAIL);
LOGGER.info("=========进行订单cancel操作完成================");
}

private void updateOrderStatus(Order order, OrderStatusEnum orderStatus) {
order.setStatus(orderStatus.getCode());
orderMapper.update(order);
}

private AccountDTO buildAccountDTO(Order order) {
AccountDTO accountDTO = new AccountDTO();
accountDTO.setAmount(order.getTotalAmount());
accountDTO.setUserId(order.getUserId());
return accountDTO;
}

private InventoryDTO buildInventoryDTO(Order order) {
InventoryDTO inventoryDTO = new InventoryDTO();
inventoryDTO.setCount(order.getCount());
inventoryDTO.setProductId(order.getProductId());
return inventoryDTO;
}

private AccountNestedDTO buildAccountNestedDTO(Order order) {
AccountNestedDTO nestedDTO = new AccountNestedDTO();
nestedDTO.setAmount(order.getTotalAmount());
nestedDTO.setUserId(order.getUserId());
nestedDTO.setProductId(order.getProductId());
nestedDTO.setCount(order.getCount());
return nestedDTO;
}
}
  • Swagger 的配置类(Swagger UI 的访问地址:/swagger-ui.html,Swagger JSON 接口的访问地址:/v2/api-docs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
@EnableSwagger2
@SuppressWarnings("all")
public class SwaggerConfig {

private static final String VERSION = "1.0.0";

ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Swagger API")
.description("基于 SpringCloud + Hmily 实战电商系统 TCC 分布式事务")
.license("Apache 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.termsOfServiceUrl("")
.version(VERSION)
.build();
}

@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
// .paths(paths())
.build().pathMapping("/").directModelSubstitute(LocalDate.class, String.class)
.genericModelSubstitutes(ResponseEntity.class).useDefaultResponseMessages(false)
.globalResponseMessage(RequestMethod.GET, newArrayList(new ResponseMessageBuilder().code(500).message("500 message")
.responseModel(new ModelRef("Error")).build()));
}

}
  • 订单的 Controller 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@RestController
@RequestMapping("/order")
public class OrderController {

private final OrderService orderService;

@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}

@PostMapping(value = "/orderPay")
@ApiOperation(value = "订单支付接口(注意这里模拟的是创建订单并进行支付扣减库存等操作)")
public String orderPay(@RequestParam(value = "count") Integer count,
@RequestParam(value = "amount") BigDecimal amount) {
return orderService.orderPay(count, amount);
}

@PostMapping(value = "/mockInventoryWithTryException")
@ApiOperation(value = "模拟下单付款操作在try阶段时候,库存异常,此时账户系统和订单状态会回滚,达到数据的一致性(注意:这里模拟的是系统异常,或者rpc异常)")
public String mockInventoryWithTryException(@RequestParam(value = "count") Integer count,
@RequestParam(value = "amount") BigDecimal amount) {
return orderService.mockInventoryWithTryException(count, amount);
}

@PostMapping(value = "/mockInventoryWithTryTimeout")
@ApiOperation(value = "模拟下单付款操作在try阶段时候,库存超时异常(但是自身最后又成功了),此时账户系统和订单状态会回滚,(库存依赖事务日志进行恢复),达到数据的一致性(异常指的是超时异常)")
public String mockInventoryWithTryTimeout(@RequestParam(value = "count") Integer count,
@RequestParam(value = "amount") BigDecimal amount) {
return orderService.mockInventoryWithTryTimeout(count, amount);
}

@PostMapping(value = "/mockAccountWithTryException")
@ApiOperation(value = "模拟下单付款操作在try阶段时候,账户rpc异常,此时订单状态会回滚,达到数据的一致性(注意:这里模拟的是系统异常,或者rpc异常)")
public String mockAccountWithTryException(@RequestParam(value = "count") Integer count,
@RequestParam(value = "amount") BigDecimal amount) {
return orderService.mockAccountWithTryException(count, amount);
}

@PostMapping(value = "/mockAccountWithTryTimeout")
@ApiOperation(value = "模拟下单付款操作在try阶段时候,账户rpc超时异常(但是最后自身又成功了),此时订单状态会回滚,账户系统依赖自身的事务日志进行调度恢复,达到数据的一致性(异常指的是超时异常)")
public String mockAccountWithTryTimeout(@RequestParam(value = "count") Integer count,
@RequestParam(value = "amount") BigDecimal amount) {
return orderService.mockAccountWithTryTimeout(count, amount);
}

@PostMapping(value = "/orderPayWithNested")
@ApiOperation(value = "订单支付接口(这里模拟的是rpc的嵌套调用 order--> account--> inventory)")
public String orderPayWithNested(@RequestParam(value = "count") Integer count,
@RequestParam(value = "amount") BigDecimal amount) {
return orderService.orderPayWithNested(count, amount);
}

@PostMapping(value = "/orderPayWithNestedException")
@ApiOperation(value = "订单支付接口(里模拟的是rpc的嵌套调用 order--> account--> inventory, inventory异常情况")
public String orderPayWithNestedException(@RequestParam(value = "count") Integer count,
@RequestParam(value = "amount") BigDecimal amount) {
return orderService.orderPayWithNestedException(count, amount);
}
}
  • SpringBoot 的主启动类
1
2
3
4
5
6
7
8
9
10
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients
@MapperScan("org.dromara.hmily.demo.common.order.mapper")
public class HmilyOrderApplication {

public static void main(final String[] args) {
SpringApplication.run(HmilyOrderApplication.class, args);
}
}
账户模块
模块配置
  • Maven 的 XML 配置内容(pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependencies>
<dependency>
<groupId>com.distributed.transaction</groupId>
<artifactId>hmily-demo-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Hmily -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-spring-boot-starter-springcloud</artifactId>
<version>${hmily.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
  • SpringBoot 的 YML 配置内容(application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
server:
port: 8885
address: 0.0.0.0
servlet:
context-path: /account-service

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.2.140:3306/hmily_account?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
application:
name: account-service
main:
allow-bean-definition-overriding: true

mybatis:
type-aliases-package: org.dromara.hmily.demo.common.account.entity
config-location: classpath:mybatis/mybatis-config.xml

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
instance-id: ${spring.application.name}-${server.port} # 自定义服务名称
prefer-ip-address: true # 将IP注册到Eureka Server上,若不配置默认使用机器的主机名

# Ribbon 的负载均衡策略
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
MaxAutoRetriesNextServer: 0
MaxAutoRetries: 0
ReadTimeout: 3000

# Feign 配置
feign:
hystrix:
enabled: false # 在 Feign 中是否开启 Hystrix 功能,默认情况下 Feign 不开启 hystrix 功能

logging:
level:
root: info
org.apache.ibatis: debug
org.dromara.hmily.demo.bonuspoint: debug
org.dromara.hmily.demo.lottery: debug
org.dromara.hmily.demo: debug
io.netty: info
  • Hmily 的 YML 配置内容(hmily.yml),可以根据实际业务需求,适当调整 Hmily 的配置参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Hmily 服务端相关配置
hmily:
# Hmily Server 配置
server:
# 配置模式:local 表示本地配置模式
configMode: local
# 当前应用名称(Hmily Server 使用)
appName: account-sc

# 只有 server.configMode 为 local 时才会读取以下配置
config:
# 当前应用名称(业务侧使用)
appName: account-sc
# 序列化方式,推荐使用 kryo;支持 hessian、protostuff、jdk;测试表现:kryo > hessian > protostuff > jdk
serializer: kryo
# 上下文传递模式(threadLocal / transmittableThreadLocal)
contextTransmittalMode: threadLocal
# 定时任务线程池最大线程数,高并发场景下可适当调大
scheduledThreadMax: 16
# 恢复任务首次延迟时间(秒)
scheduledRecoveryDelay: 60
# 清理任务执行间隔(秒)
scheduledCleanDelay: 60
# 物理删除任务执行间隔(秒)
scheduledPhyDeletedDelay: 600
# 定时任务初始化延迟时间(秒)
scheduledInitDelay: 30
# 事务恢复延迟时间(秒),强烈建议大于 RPC 调用的超时时间,否则可能误触发补偿;比如设置为:≥ RPC 超时 + 网络抖动余量
recoverDelayTime: 60
# 清理延迟时间(秒)
cleanDelayTime: 180
# 单次处理事务的最大数量限制
limit: 200
# 最大重试次数,默认 10 次,业务服务宕机时定时任务在 retryMax 次内执行 cancel 或 confirm
retryMax: 10
# Disruptor 缓冲区大小,高并发时可调大,建议为 2 的幂次方
bufferSize: 8192
# 消费者线程数
consumerThreads: 16
# 是否启用异步事务日志存储
asyncRepository: true
# 是否自动建表(仅关系型数据库有效)
autoSql: true
# 是否启用物理删除
phyDeleted: true
# 事务数据保留天数
storeDays: 3
# 事务日志存储类型(mysql / postgresql / mongodb / redis / zookeeper / file 等)
repository: mysql

# Hmily 仓库相关配置
repository:
# 数据库相关配置
database:
# JDBC 驱动类名
driverClassName: com.mysql.cj.jdbc.Driver
# 数据库连接 URL
url: jdbc:mysql://192.168.2.140:3306/hmily?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
# 数据库用户名
username: root
# 数据库密码
password: 123456
# 连接池最大连接数
maxActive: 20
# 连接池最小空闲连接数
minIdle: 10
# 获取连接超时时间(毫秒)
connectionTimeout: 30000
# 连接空闲超时时间(毫秒)
idleTimeout: 600000
# 连接最大生命周期(毫秒)
maxLifetime: 1800000
模块代码
  • 账户的 Feign 客户端(这里需要添加 @Hmily 注解,用于标记参与 TCC 分布式事务的业务方法,声明该方法由 Hmily 框架接管并按 Try / Confirm / Cancel 事务模型执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FeignClient(value = "inventory-service")
public interface InventoryClient {

/**
* 库存扣减.
*
* @param inventoryDTO 实体对象
* @return true 成功
*/
@RequestMapping("/inventory-service/inventory/decrease")
@Hmily
Boolean decrease(@RequestBody InventoryDTO inventoryDTO);

/**
* 模拟库存扣减异常.
*
* @param inventoryDTO 实体对象
* @return true 成功
*/
@Hmily
@RequestMapping("/inventory-service/inventory/mockWithTryException")
Boolean mockWithTryException(@RequestBody InventoryDTO inventoryDTO);
}
  • 账户的 Service 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public interface AccountService {

/**
* 扣款支付.
*
* @param accountDTO 参数dto
* @return true boolean
*/
boolean payment(AccountDTO accountDTO);

/**
* Mock with try exception boolean.
*
* @param accountDTO the account dto
* @return the boolean
*/
boolean mockWithTryException(AccountDTO accountDTO);

/**
* Mock with try timeout boolean.
*
* @param accountDTO the account dto
* @return the boolean
*/
boolean mockWithTryTimeout(AccountDTO accountDTO);

/**
* Payment with nested boolean.
*
* @param nestedDTO the nested dto
* @return the boolean
*/
boolean paymentWithNested(AccountNestedDTO nestedDTO);

/**
* Payment with nested exception boolean.
*
* @param nestedDTO the nested dto
* @return the boolean
*/
boolean paymentWithNestedException(AccountNestedDTO nestedDTO);

/**
* 获取用户账户信息.
*
* @param userId 用户id
* @return AccountDO account do
*/
AccountDO findByUserId(String userId);
}
  • 账户的 Service 实现类(这里需要使用 @HmilyTCC 注解,同时指定 confirmMethodcancelMethod 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
@Service("accountService")
public class AccountServiceImpl implements AccountService {

/**
* logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);

private final AccountMapper accountMapper;

private final InventoryClient inventoryClient;

/**
* Instantiates a new Account service.
*
* @param accountMapper the account mapper
*/
@Autowired(required = false)
public AccountServiceImpl(final AccountMapper accountMapper, final InventoryClient inventoryClient) {
this.accountMapper = accountMapper;
this.inventoryClient = inventoryClient;
}

@Override
@HmilyTCC(confirmMethod = "confirm", cancelMethod = "cancel")
public boolean payment(final AccountDTO accountDTO) {
LOGGER.info("============执行try付款接口===============");
accountMapper.update(accountDTO);
return Boolean.TRUE;
}

@Override
@HmilyTCC(confirmMethod = "confirm", cancelMethod = "cancel")
public boolean mockWithTryException(AccountDTO accountDTO) {
throw new HmilyRuntimeException("账户扣减异常!");
}

@Override
@HmilyTCC(confirmMethod = "confirm", cancelMethod = "cancel")
public boolean mockWithTryTimeout(AccountDTO accountDTO) {
try {
//模拟延迟 当前线程暂停10秒
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int decrease = accountMapper.update(accountDTO);
if (decrease != 1) {
throw new HmilyRuntimeException("账户余额不足");
}
return true;
}

@Override
@HmilyTCC(confirmMethod = "confirmNested", cancelMethod = "cancelNested")
public boolean paymentWithNested(AccountNestedDTO nestedDTO) {
LOGGER.info("============执行tryNested付款接口===============");
accountMapper.update(buildAccountDTO(nestedDTO));
inventoryClient.decrease(buildInventoryDTO(nestedDTO));
return Boolean.TRUE;
}

@Override
@HmilyTCC(confirmMethod = "confirmNested", cancelMethod = "cancelNested")
public boolean paymentWithNestedException(AccountNestedDTO nestedDTO) {
accountMapper.update(buildAccountDTO(nestedDTO));
inventoryClient.mockWithTryException(buildInventoryDTO(nestedDTO));
return Boolean.TRUE;
}

@Override
public AccountDO findByUserId(final String userId) {
return accountMapper.findByUserId(userId);
}

/**
* Confirm boolean.
*
* @param accountDTO the account dto
* @return the boolean
*/
public boolean confirm(final AccountDTO accountDTO) {
LOGGER.info("============执行confirm 付款接口===============");
return accountMapper.confirm(accountDTO) > 0;
}


/**
* Cancel boolean.
*
* @param accountDTO the account dto
* @return the boolean
*/
public boolean cancel(final AccountDTO accountDTO) {
LOGGER.info("============执行cancel 付款接口===============");
return accountMapper.cancel(accountDTO) > 0;
}

@Transactional(rollbackFor = Exception.class)
public boolean confirmNested(AccountNestedDTO accountNestedDTO) {
LOGGER.info("============confirmNested确认付款接口===============");
return accountMapper.confirm(buildAccountDTO(accountNestedDTO)) > 0;
}

/**
* Cancel nested boolean.
*
* @param accountNestedDTO the account nested dto
* @return the boolean
*/
@Transactional(rollbackFor = Exception.class)
public boolean cancelNested(AccountNestedDTO accountNestedDTO) {
LOGGER.info("============cancelNested 执行取消付款接口===============");
return accountMapper.cancel(buildAccountDTO(accountNestedDTO)) > 0;
}

private AccountDTO buildAccountDTO(AccountNestedDTO nestedDTO) {
AccountDTO dto = new AccountDTO();
dto.setAmount(nestedDTO.getAmount());
dto.setUserId(nestedDTO.getUserId());
return dto;
}

private InventoryDTO buildInventoryDTO(AccountNestedDTO nestedDTO) {
InventoryDTO inventoryDTO = new InventoryDTO();
inventoryDTO.setCount(nestedDTO.getCount());
inventoryDTO.setProductId(nestedDTO.getProductId());
return inventoryDTO;
}
}
  • 账户的 Controller 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@RestController
@RequestMapping("/account")
public class AccountController {

private final AccountService accountService;

@Autowired
public AccountController(AccountService accountService) {
this.accountService = accountService;
}

@RequestMapping("/payment")
public Boolean payment(@RequestBody AccountDTO accountDO) {
return accountService.payment(accountDO);
}

@RequestMapping("/mockWithTryException")
public Boolean mockWithTryException(@RequestBody AccountDTO accountDO) {
return accountService.mockWithTryException(accountDO);
}

@RequestMapping("/mockWithTryTimeout")
public Boolean mockWithTryTimeout(@RequestBody AccountDTO accountDO) {
return accountService.mockWithTryTimeout(accountDO);
}

@RequestMapping("/paymentWithNested")
public Boolean paymentWithNested(@RequestBody AccountNestedDTO nestedDTO) {
return accountService.paymentWithNested(nestedDTO);
}

@RequestMapping("/paymentWithNestedException")
public Boolean paymentWithNestedException(@RequestBody AccountNestedDTO nestedDTO) {
return accountService.paymentWithNestedException(nestedDTO);
}

@RequestMapping("/findByUserId")
public BigDecimal findByUserId(@RequestParam("userId") String userId) {
return accountService.findByUserId(userId).getBalance();
}
}
  • SpringBoot 的主启动类
1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@EnableDiscoveryClient
@EnableFeignClients
@EnableTransactionManagement
@MapperScan("org.dromara.hmily.demo.common.account.mapper")
public class HmilyAccountApplication {

public static void main(final String[] args) {
SpringApplication.run(HmilyAccountApplication.class, args);
}
}
库存模块
模块配置
  • Maven 的 XML 配置内容(pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependencies>
<dependency>
<groupId>com.distributed.transaction</groupId>
<artifactId>hmily-demo-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Web支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Hmily -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-spring-boot-starter-springcloud</artifactId>
<version>${hmily.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
  • SpringBoot 的 YML 配置内容(application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
server:
port: 8883
address: 0.0.0.0
servlet:
context-path: /inventory-service

spring:
main:
allow-bean-definition-overriding: true
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.2.140:3306/hmily_stock?useUnicode=true&characterEncoding=utf8
username: root
password: 123456
application:
name: inventory-service

mybatis:
type-aliases-package: org.dromara.hmily.demo.common.inventory.entity
config-location: classpath:mybatis/mybatis-config.xml

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
instance-id: ${spring.application.name}-${server.port} # 自定义服务名称
prefer-ip-address: true # 将IP注册到Eureka Server上,若不配置默认使用机器的主机名

# Ribbon 的负载均衡策略
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

logging:
level:
root: info
org.apache.ibatis: debug
org.dromara.hmily.demo.bonuspoint: debug
org.dromara.hmily.demo.lottery: debug
org.dromara.hmily.demo: debug
io.netty: info
  • Hmily 的 YML 配置内容(hmily.yml),可以根据实际业务需求,适当调整 Hmily 的配置参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Hmily 服务端相关配置
hmily:
# Hmily Server 配置
server:
# 配置模式:local 表示本地配置模式
configMode: local
# 当前应用名称(Hmily Server 使用)
appName: inventory-sc

# 只有 server.configMode 为 local 时才会读取以下配置
config:
# 当前应用名称(业务侧使用)
appName: inventory-sc
# 序列化方式,推荐使用 kryo;支持 hessian、protostuff、jdk;测试表现:kryo > hessian > protostuff > jdk
serializer: kryo
# 上下文传递模式(threadLocal / transmittableThreadLocal)
contextTransmittalMode: threadLocal
# 定时任务线程池最大线程数,高并发场景下可适当调大
scheduledThreadMax: 16
# 恢复任务首次延迟时间(秒)
scheduledRecoveryDelay: 60
# 清理任务执行间隔(秒)
scheduledCleanDelay: 60
# 物理删除任务执行间隔(秒)
scheduledPhyDeletedDelay: 600
# 定时任务初始化延迟时间(秒)
scheduledInitDelay: 30
# 事务恢复延迟时间(秒),强烈建议大于 RPC 调用的超时时间,否则可能误触发补偿;比如设置为:≥ RPC 超时 + 网络抖动余量
recoverDelayTime: 60
# 清理延迟时间(秒)
cleanDelayTime: 180
# 单次处理事务的最大数量限制
limit: 200
# 最大重试次数,默认 10 次,业务服务宕机时定时任务在 retryMax 次内执行 cancel 或 confirm
retryMax: 10
# Disruptor 缓冲区大小,高并发时可调大,建议为 2 的幂次方
bufferSize: 8192
# 消费者线程数
consumerThreads: 16
# 是否启用异步事务日志存储
asyncRepository: true
# 是否自动建表(仅关系型数据库有效)
autoSql: true
# 是否启用物理删除
phyDeleted: true
# 事务数据保留天数
storeDays: 3
# 事务日志存储类型(mysql / postgresql / mongodb / redis / zookeeper / file 等)
repository: mysql

# Hmily 仓库相关配置
repository:
# 数据库相关配置
database:
# JDBC 驱动类名
driverClassName: com.mysql.cj.jdbc.Driver
# 数据库连接 URL
url: jdbc:mysql://192.168.2.140:3306/hmily?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
# 数据库用户名
username: root
# 数据库密码
password: 123456
# 连接池最大连接数
maxActive: 20
# 连接池最小空闲连接数
minIdle: 10
# 获取连接超时时间(毫秒)
connectionTimeout: 30000
# 连接空闲超时时间(毫秒)
idleTimeout: 600000
# 连接最大生命周期(毫秒)
maxLifetime: 1800000
模块代码
  • 库存的 Service 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface InventoryService {

/**
* 获取商品库存信息.
*
* @param productId 商品id
* @return InventoryDO inventory do
*/
InventoryDO findByProductId(String productId);

/**
* 扣减库存操作.
* 这一个tcc接口
*
* @param inventoryDTO 库存DTO对象
* @return true boolean
*/
Boolean decrease(InventoryDTO inventoryDTO);

/**
* mock 库存扣减try阶段异常.
*
* @param inventoryDTO dto
* @return true boolean
*/
Boolean mockWithTryException(InventoryDTO inventoryDTO);

/**
* mock 库存扣减try阶段超时.
*
* @param inventoryDTO dto
* @return true boolean
*/
Boolean mockWithTryTimeout(InventoryDTO inventoryDTO);
}
  • 库存的 Service 实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@Service("inventoryService")
public class InventoryServiceImpl implements InventoryService {

private static final Logger LOGGER = LoggerFactory.getLogger(InventoryServiceImpl.class);

private final InventoryMapper inventoryMapper;

@Autowired(required = false)
public InventoryServiceImpl(InventoryMapper inventoryMapper) {
this.inventoryMapper = inventoryMapper;
}

/**
* 获取商品库存信息.
*
* @param productId 商品id
* @return InventoryDO
*/
@Override
public InventoryDO findByProductId(String productId) {
return inventoryMapper.findByProductId(productId);
}

/**
* 扣减库存操作.
* 这一个tcc接口
*
* @param inventoryDTO 库存DTO对象
* @return true
*/
@Override
@HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
public Boolean decrease(InventoryDTO inventoryDTO) {
LOGGER.info("==========try扣减库存decrease===========");
inventoryMapper.decrease(inventoryDTO);
return true;
}

@Override
@HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
@Transactional
public Boolean mockWithTryException(InventoryDTO inventoryDTO) {
//这里是模拟异常所以就直接抛出异常了
throw new HmilyRuntimeException("库存扣减异常!");
}

@Override
@HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
@Transactional(rollbackFor = Exception.class)
public Boolean mockWithTryTimeout(InventoryDTO inventoryDTO) {
try {
//模拟延迟 当前线程暂停10秒
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOGGER.info("==========springcloud调用扣减库存mockWithTryTimeout===========");
final int decrease = inventoryMapper.decrease(inventoryDTO);
if (decrease != 1) {
throw new HmilyRuntimeException("库存不足");
}
return true;
}

public Boolean confirmMethod(InventoryDTO inventoryDTO) {
LOGGER.info("==========confirmMethod库存确认方法===========");
return inventoryMapper.confirm(inventoryDTO) > 0;
}

@Transactional(rollbackFor = Exception.class)
public Boolean confirmMethodTimeout(InventoryDTO inventoryDTO) {
try {
//模拟延迟 当前线程暂停11秒
Thread.sleep(11000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOGGER.info("==========Springcloud调用扣减库存确认方法===========");
inventoryMapper.decrease(inventoryDTO);
return true;
}

@Transactional(rollbackFor = Exception.class)
public Boolean confirmMethodException(InventoryDTO inventoryDTO) {
LOGGER.info("==========Springcloud调用扣减库存确认方法===========");
final int decrease = inventoryMapper.decrease(inventoryDTO);
if (decrease != 1) {
throw new HmilyRuntimeException("库存不足");
}
return true;
// throw new TccRuntimeException("库存扣减确认异常!");
}

public Boolean cancelMethod(InventoryDTO inventoryDTO) {
LOGGER.info("==========cancelMethod库存取消方法===========");
return inventoryMapper.cancel(inventoryDTO) > 0;
}

}
  • 库存的 Controller 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RestController
@RequestMapping("/inventory")
public class InventoryController {

private final InventoryService inventoryService;

@Autowired
public InventoryController(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}

@RequestMapping("/decrease")
public Boolean decrease(@RequestBody InventoryDTO inventoryDTO) {
return inventoryService.decrease(inventoryDTO);
}

@RequestMapping("/findByProductId")
public Integer findByProductId(@RequestParam("productId") String productId) {
return inventoryService.findByProductId(productId).getTotalInventory();
}

@RequestMapping("/mockWithTryException")
public Boolean mockWithTryException(@RequestBody InventoryDTO inventoryDTO) {
return inventoryService.mockWithTryException(inventoryDTO);
}

@RequestMapping("/mockWithTryTimeout")
public Boolean mockWithTryTimeout(@RequestBody InventoryDTO inventoryDTO) {
return inventoryService.mockWithTryTimeout(inventoryDTO);
}

}
  • SpringBoot 的主启动类
1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class})
@EnableEurekaClient
@EnableFeignClients
@EnableTransactionManagement
@MapperScan("org.dromara.hmily.demo.common.inventory.mapper")
public class HmilyInventoryApplication {

public static void main(final String[] args) {
SpringApplication.run(HmilyInventoryApplication.class, args);
}

}

代码测试

  • (1) 启动所有微服务应用

  • (2) 浏览器访问 http://127.0.0.1:8090/swagger-ui.html,打开 Swagger 的接口测试界面,如下图所示:

  • (3) 通过 Swagger 的接口测试界面,分别测试不同的接口,并观察各个微服务应用输出的日志信息,验证 Hmily 的 TCC 分布式事务是否生效

常见错误

SPI 异常
  • 问题描述:使用 hmily-spring-boot-starter-springcloud2.1.1 版本时,无论是分布式事务发起者,还是分布式事务参与者,第一次执行 TCC 分布式事务之前都会抛出以下异常:
1
org.dromara.hmily.spi.ExtensionLoader : not found service provider for : org.dromara.hmily.core.service.HmilyTransactionHandlerFactory
  • 解决方案:这异常只会在 Hmily 第一次执行 TCC 分布式事务之前才会抛出,不会影响 TCC 事务的真正执行,暂时找不到好的解决办法,建议参考 GitHub Issues

补充说明

TCC 不适用于外部支付系统

当内部的账户系统变成外部第三方支付系统(如支付宝、微信支付)后,Hmily TCC 在 “资金扣款” 这个核心链路上基本不再适用。但注意,是 Hmily 不再适合做完整 TCC,而不是说 Hmily 完全没用了

特别注意

  • 当账户系统演进为第三方支付系统后,由于支付行为不可回滚、事务控制权不在本系统内,Hmily TCC 不再适合覆盖资金扣减链路。
  • 实践中通常将 TCC 的事务边界限定在订单与库存等内部可控系统,支付采用异步回调与最终一致性方案,通过订单状态机和补偿机制保证业务正确性。

内部的账户系统变成外部第三方支付系统时,为什么 Hmily TCC 不再适用?

  • 使用 TCC 的前提条件,其实非常苛刻,要求每个分布式事务参与方都必须:

    • 能暴露 Try / Confirm / Cancel 这三个核心接口
    • 能保证:
      • 幂等
      • 空回滚
      • 防悬挂
      • 业务资源可控、可回滚
    • 第三方支付系统,几乎不满足这些条件
  • 第三方支付系统的 “天然不兼容点”,以支付宝为例:

TCC 要求第三方支付系统的现状
Try:预扣资源❌ 没有 “预扣 + 不结算” 这种语义
Confirm:最终提交❌ 支付成功即最终态
Cancel:回滚 Try❌ 扣了钱只能走退款流程(异步、不可控)
事务控制权❌ 在第三方手里
接口可幂等重试⚠️ 部分支持,但语义不同

内部的账户系统变成外部第三方支付系统时,推荐的架构拆分方案

  • TCC 只覆盖「订单 + 库存」

    • 这是最常见、也是最成熟的方案(99% 电商系统的选择)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      用户下单请求

      订单服务(Try)

      库存服务(Try)

      TCC Confirm

      生成 "待支付订单"

      跳转第三方支付
    • 方案特点
      • Hmily TCC 只负责:
        • 订单创建
        • 库存扣减
      • 支付系统:
        • 完全在 TCC 事务之外
      • 订单状态机控制后续流程
  • 那钱和库存不一致怎么办?

    • 以订单状态驱动(核心)

      • 典型的订单状态流转:
        1
        2
        3
        4
        5
        → INIT(初始化)
        → STOCK_LOCKED(库存已锁)
        → PAYING(支付中)
        → PAID(支付成功)
        → CANCELED / CLOSED
      • TCC 只保证 INIT → STOCK_LOCKED
      • 支付结果通过异步回调驱动
    • 支付结果异步对账(最终一致性)

      • 支付成功回调:
        • 更新订单转投为 PAID
        • 触发发货
      • 支付超时 / 失败:
        • 定时任务关闭订单
        • 释放库存(或反向补偿)
      • 这是 “可靠消息 + 最终一致性” 模型

什么时候还能 “部分用 TCC + 第三方支付”?

  • 只有一种场景勉强可行:自己(内部)实现了 “账户体系”,第三方支付只是 “充值通道”
  • 例如:
    • 用户先用支付宝充值到内部账户余额
    • 用户下单时:
      • 使用内部账户余额扣款(TCC)
      • 不直接调用支付宝
  • Hmily 依然非常适合这种场景
    1
    2
    支付宝 → 内部账户(非事务)
    内部账户 + 订单 + 库存(TCC)

部分用 TCC + 第三方支付的时序图

sequenceDiagram
    participant U as 用户
    participant O as 订单服务
    participant S as 库存服务
    participant P as 第三方支付

    %% ========== TCC 事务第一阶段:Try ==========
    rect rgb(245,245,245)
        Note over O,S: TCC 事务第一阶段:Try(校验 + 资源预留)
        U->>O: 1. 提交下单请求
        O->>O: 2. 创建订单(状态 = INIT)
        O->>S: 3. Try 锁定库存
        S-->>O: 库存锁定成功
        O->>O: 4. 更新订单状态 = STOCK_LOCKED
    end

    %% ========== TCC 事务第二阶段:Confirm / Cancel ==========
    rect rgb(230,255,230)
        alt 全局事务成功
            Note over O,S: TCC 事务第二阶段:Confirm(提交 Try 结果)
            O->>O: 5. Confirm 订单(状态保持 = STOCK_LOCKED)
            O->>S: 6. Confirm 库存锁定生效
        else 全局事务失败
            Note over O,S: TCC 事务第二阶段:Cancel(回滚 Try 结果)
            O->>O: 5'. Cancel 订单(状态 = CANCELED)
            O->>S: 6'. Cancel 释放库存
        end
    end

    %% ========== TCC 事务结束 ==========
    Note over O,S: TCC 事务结束(仅涵盖订单 + 库存)

    %% ========== 支付在 TCC 事务之外 ==========
    O->>O: 7. 更新订单状态 = PAYING
    O->>U: 8. 返回支付信息
    U->>P: 9. 跳转第三方支付
    P-->>O: 10. 支付结果回调

    alt 支付成功
        O->>O: 11. 更新订单状态 = PAID
        O->>S: 12. 业务确认库存扣减 / 发货
    else 支付失败 / 超时
        O->>O: 11'. 关闭订单(状态 = CLOSED)
        O->>S: 12'. 业务补偿释放库存
    end

时序图说明

在工程实现(架构最佳实践)中,订单状态由 INIT 更新为 STOCK_LOCKED 的操作放在 Try 阶段完成,用于标识库存已成功预占;Confirm 阶段仅用于确认库存锁定结果,不再修改订单状态,从而降低 Confirm 阶段的并发写与幂等复杂度。值得一提的是,这没有绝对的对错,订单状态由 INIT 更新为 STOCK_LOCKED 的操作也可以放在 Confirm 阶段完成。

参考资料