大纲 电商系统 TCC 事务实战(编码) 电商系统的案例(SpringCloud) 特别注意
本节中的案例代码仅用于演示如何通过 SpringCloud + Hmily 实现 TCC 分布式事务,并未全面涵盖 TCC 在实际运行过程中可能出现的各种事务异常处理 场景(如空回滚、防悬挂、第二阶段重复提交等问题),而这些事务异常问题在生产环境中是必须重点考虑并妥善处理的 。
代码下载 本节完整的案例代码可以直接从 GitHub 下载对应章节 distributed-transaction-02。 版本说明 组件 版本 说明 JDK 11MySQL 5.7.44Hmily 2.1.1SpringBoot 2.3.7.RELEASEHmily 官方并不支持 SpringBoot 3.x SpringCloud Hoxton.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 ;
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 > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-dependencies</artifactId > <version > ${spring-cloud.version}</version > <type > pom</type > <scope > import</scope > </dependency > <dependency > <groupId > org.dromara</groupId > <artifactId > hmily-spring-boot-starter-springcloud</artifactId > <version > ${hmily.version}</version > </dependency > <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 > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > <version > ${mybatis.version}</version > </dependency > </dependencies > </dependencyManagement > <dependencies > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > ${mysql.version}</version > </dependency > <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 > <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: instance: hostname: ${hostname:localhost} prefer-ip-address: true lease-renewal-interval-in-seconds: 30 lease-expiration-duration-in-seconds: 90 server: peer-eureka-nodes-update-interval-ms: 60000 enable-self-preservation: false eviction-interval-timer-in-ms: 60000 client: register-with-eureka: false fetch-registry: false service-url: default-zone: http://${eureka.instance.hostname}:${server.port}/eureka/ eureka-service-url-poll-interval-seconds: 60 healthcheck: enabled: true endpoints: health: sensitive: false
模块代码 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 > <dependency > <groupId > org.dromara</groupId > <artifactId > hmily-annotation</artifactId > <version > ${hmily.version}</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <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 > <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 >
核心代码 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; 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(1 , "未支付" ), PAYING(2 , "支付中" ), PAY_FAIL(3 , "支付失败" ), PAY_SUCCESS(4 , "支付成功" ); private final int code; private final String desc; }
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 { @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) ; @Update("update `order` set status = #{status} where number = #{number}") int update (Order order) ; @Select("select * from order") List<Order> listAll () ; }
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; }
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 ; private String userId; private BigDecimal amount; }
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("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) ; @Update("update account set " + " freeze_amount= freeze_amount - #{amount}" + " where user_id =#{userId} and freeze_amount >= #{amount} ") int confirm (AccountDTO accountDTO) ; @Update("update account set balance = balance + #{amount}," + " freeze_amount= freeze_amount - #{amount} " + " where user_id =#{userId} and freeze_amount >= #{amount}") int cancel (AccountDTO accountDTO) ; @Select("select id,user_id,balance, freeze_amount from account where user_id =#{userId} limit 1") AccountDO findByUserId (String userId) ; }
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; private String productId; private Integer totalInventory; private Integer lockInventory; }
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 ; private String productId; private Integer count; }
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 { @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) ; @Update("update inventory set " + " lock_inventory = lock_inventory - #{count} " + " where product_id =#{productId} and lock_inventory > 0 ") int confirm (InventoryDTO inventoryDTO) ; @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) ; @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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <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 > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > <dependency > <groupId > org.mybatis.spring.boot</groupId > <artifactId > mybatis-spring-boot-starter</artifactId > </dependency > <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 > <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 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule MaxAutoRetriesNextServer: 0 MaxAutoRetries: 0 ReadTimeout: 10000 feign: hystrix: enabled: false 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: server: configMode: local appName: order-sc config: appName: order-sc serializer: kryo contextTransmittalMode: threadLocal scheduledThreadMax: 16 scheduledRecoveryDelay: 60 scheduledCleanDelay: 60 scheduledPhyDeletedDelay: 600 scheduledInitDelay: 30 recoverDelayTime: 60 cleanDelayTime: 180 limit: 200 retryMax: 10 bufferSize: 8192 consumerThreads: 16 asyncRepository: true autoSql: true phyDeleted: true storeDays: 3 repository: mysql repository: database: driverClassName: com.mysql.cj.jdbc.Driver 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 { @RequestMapping("/account-service/account/payment") @Hmily Boolean payment (@RequestBody AccountDTO accountDO) ; @RequestMapping("/account-service/account/findByUserId") BigDecimal findByUserId (@RequestParam("userId") String userId) ; @Hmily @RequestMapping("/account-service/account/mockWithTryException") Boolean mockWithTryException (@RequestBody AccountDTO accountDO) ; @Hmily @RequestMapping("/account-service/account/mockWithTryTimeout") Boolean mockWithTryTimeout (@RequestBody AccountDTO accountDO) ; @Hmily @RequestMapping("/account-service/account/paymentWithNested") Boolean paymentWithNested (@RequestBody AccountNestedDTO nestedDTO) ; @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 { @RequestMapping("/inventory-service/inventory/decrease") @Hmily Boolean decrease (@RequestBody InventoryDTO inventoryDTO) ; @RequestMapping("/inventory-service/inventory/testDecrease") Boolean testDecrease (@RequestBody InventoryDTO inventoryDTO) ; @RequestMapping("/inventory-service/inventory/findByProductId") Integer findByProductId (@RequestParam("productId") String productId) ; @Hmily @RequestMapping("/inventory-service/inventory/mockWithTryException") Boolean mockWithTryException (@RequestBody InventoryDTO inventoryDTO) ; @Hmily @RequestMapping("/inventory-service/inventory/mockWithTryTimeout") Boolean mockWithTryTimeout (@RequestBody InventoryDTO inventoryDTO) ; }
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 { String orderPay (Integer count, BigDecimal amount) ; String mockInventoryWithTryException (Integer count, BigDecimal amount) ; String mockAccountWithTryException (Integer count, BigDecimal amount) ; String mockInventoryWithTryTimeout (Integer count, BigDecimal amount) ; String mockAccountWithTryTimeout (Integer count, BigDecimal amount) ; String orderPayWithNested (Integer count, BigDecimal amount) ; String orderPayWithNestedException (Integer count, BigDecimal amount) ; void updateOrderStatus (Order order) ; }
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); } @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())); order.setProductId("1" ); order.setStatus(OrderStatusEnum.NOT_PAY.getCode()); order.setTotalAmount(amount); order.setCount(count); order.setUserId("10000" ); return order; } }
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 { void makePayment (Order order) ; String mockPaymentInventoryWithTryException (Order order) ; String mockPaymentAccountWithTryException (Order order) ; String mockPaymentInventoryWithTryTimeout (Order order) ; String mockPaymentAccountWithTryTimeout (Order order) ; String makePaymentWithNested (Order order) ; String makePaymentWithNestedException (Order order) ; }
订单支付的 Service 实现类(这里需要使用 @HmilyTCC 注解,同时指定 confirmMethod 和 cancelMethod 参数 ) 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); 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)) .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())); } }
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); } }
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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > <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 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule MaxAutoRetriesNextServer: 0 MaxAutoRetries: 0 ReadTimeout: 3000 feign: hystrix: enabled: false 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: server: configMode: local appName: account-sc config: appName: account-sc serializer: kryo contextTransmittalMode: threadLocal scheduledThreadMax: 16 scheduledRecoveryDelay: 60 scheduledCleanDelay: 60 scheduledPhyDeletedDelay: 600 scheduledInitDelay: 30 recoverDelayTime: 60 cleanDelayTime: 180 limit: 200 retryMax: 10 bufferSize: 8192 consumerThreads: 16 asyncRepository: true autoSql: true phyDeleted: true storeDays: 3 repository: mysql repository: database: driverClassName: com.mysql.cj.jdbc.Driver 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 { @RequestMapping("/inventory-service/inventory/decrease") @Hmily Boolean decrease (@RequestBody InventoryDTO inventoryDTO) ; @Hmily @RequestMapping("/inventory-service/inventory/mockWithTryException") Boolean mockWithTryException (@RequestBody InventoryDTO inventoryDTO) ; }
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 { boolean payment (AccountDTO accountDTO) ; boolean mockWithTryException (AccountDTO accountDTO) ; boolean mockWithTryTimeout (AccountDTO accountDTO) ; boolean paymentWithNested (AccountNestedDTO nestedDTO) ; boolean paymentWithNestedException (AccountNestedDTO nestedDTO) ; AccountDO findByUserId (String userId) ; }
账户的 Service 实现类(这里需要使用 @HmilyTCC 注解,同时指定 confirmMethod 和 cancelMethod 参数 ) 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 { private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class); private final AccountMapper accountMapper; private final InventoryClient inventoryClient; @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 { 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); } public boolean confirm (final AccountDTO accountDTO) { LOGGER.info("============执行confirm 付款接口===============" ); return accountMapper.confirm(accountDTO) > 0 ; } 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 ; } @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; } }
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(); } }
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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-netflix-eureka-client</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > <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 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: server: configMode: local appName: inventory-sc config: appName: inventory-sc serializer: kryo contextTransmittalMode: threadLocal scheduledThreadMax: 16 scheduledRecoveryDelay: 60 scheduledCleanDelay: 60 scheduledPhyDeletedDelay: 600 scheduledInitDelay: 30 recoverDelayTime: 60 cleanDelayTime: 180 limit: 200 retryMax: 10 bufferSize: 8192 consumerThreads: 16 asyncRepository: true autoSql: true phyDeleted: true storeDays: 3 repository: mysql repository: database: driverClassName: com.mysql.cj.jdbc.Driver 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
模块代码 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 { InventoryDO findByProductId (String productId) ; Boolean decrease (InventoryDTO inventoryDTO) ; Boolean mockWithTryException (InventoryDTO inventoryDTO) ; Boolean mockWithTryTimeout (InventoryDTO inventoryDTO) ; }
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; } @Override public InventoryDO findByProductId (String productId) { return inventoryMapper.findByProductId(productId); } @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 { 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 { 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 ; } public Boolean cancelMethod (InventoryDTO inventoryDTO) { LOGGER.info("==========cancelMethod库存取消方法===========" ); return inventoryMapper.cancel(inventoryDTO) > 0 ; } }
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); } }
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); } }
代码测试
(3) 通过 Swagger 的接口测试界面,分别测试不同的接口,并观察各个微服务应用输出的日志信息,验证 Hmily 的 TCC 分布式事务是否生效 常见错误 SPI 异常 问题描述:使用 hmily-spring-boot-starter-springcloud 的 2.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:回滚 Try ❌ 扣了钱只能走退款流程(异步、不可控) 事务控制权 ❌ 在第三方手里 接口可幂等重试 ⚠️ 部分支持,但语义不同
内部的账户系统变成外部第三方支付系统时,推荐的架构拆分方案
TCC 只覆盖「订单 + 库存」
这是最常见、也是最成熟的方案(99% 电商系统的选择)1 2 3 4 5 6 7 8 9 10 11 用户下单请求 ↓ 订单服务(Try) ↓ 库存服务(Try) ↓ TCC Confirm ↓ 生成 "待支付订单" ↓ 跳转第三方支付
方案特点 Hmily TCC 只负责: 支付系统: 订单状态机控制后续流程 那钱和库存不一致怎么办?
以订单状态驱动(核心)
典型的订单状态流转:1 2 3 4 5 → INIT(初始化) → STOCK_LOCKED(库存已锁) → PAYING(支付中) → PAID(支付成功) → CANCELED / CLOSED
TCC 只保证 INIT → STOCK_LOCKED 支付结果通过异步回调驱动 支付结果异步对账(最终一致性)
支付成功回调: 支付超时 / 失败: 这是 “可靠消息 + 最终一致性” 模型 什么时候还能 “部分用 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 阶段完成。
参考资料