大纲
参考资料
账户系统 TCC 事务实战(编码)
账户系统的前置知识
Hmily 分布式事务框架
Hmily 是一个基于 Java 的金融级柔性分布式事务开源解决方案(已停止维护),主要用于微服务架构下的 TCC(Try-Confirm-Cancel)和 TAC(自动补偿)事务模式;支持通过零侵入式设计快速集成 Dubbo、SpringCloud 等框架,支持多种 RPC 框架和日志存储方式(如 MySQL、Redis、MongoDB、ZooKeeper 等),具备高可靠性、性能和可观测性,可在分布式环境中确保多个服务的一致性执行。它适合金融级场景处理复杂分布式事务。
Hmily 的官方文档
Hmily 的官方文档可以查阅 GitHub WiKi,该文档已过时(最后更改时间是 2018 年),仅供参考,强烈建议直接阅读 Hmily 官方的案例代码。
Hmily 的官方案例
Hmily 的官方案例代码可以从 GitHub 仓库 获取,包括 TCC、TAC 事务模式使用的 Demo。
Hmily 的版本说明
特别注意
- Maven 中央仓库中
hmily-spring-boot-starter-springcloud 的最新版本是 2.1.1,并没有 2.1.2 版本。 - 如果需要
2.1.2 版本,可以从 这里(不稳定,生产环境慎用) 直接下载,并通过 mvn install 命令安装到本地。
Hmily 的核心功能
- 高可靠性:支持分布式场景下的事务异常回滚、超时异常恢复、防止事务悬挂
- 易用性:提供零侵入性式的
Spring-Boot、Spring-Namespace 快速与业务系统集成 - 高性能:去中心化设计,与业务系统完全融合,天然支持集群部署
- 可观测性:支持 Metrics 多项指标性能监控,以及 Admin 管理后台 UI 展示
- 多种 RPC:支持
Dubbo、SpringCloud、Motan、Sofa-rpc、brpc、tars 等知名 RPC 框架 - 日志存储:支持
mysql、oracle、mongodb、redis、zookeeper 等日常存储方式 - 复杂场景:支持 RPC 嵌套调用事务
使用 Hmily 的必要前提
- 必须使用
JDK8+ 。 - TCC 模式必须要使用一款
RPC 框架,比如 : Dubbo, SpringCloud,Montan。
Hmily 的 TCC 模式
当使用 Hmily 的 TCC 模式时,开发者需要根据自身业务需求提供 try、confirm、cancel 等三个方法,并且 confirm、cancel 方法由开发者自己完成实现,Hmily 只是负责调用方法来达到事务的一致性。
![]()
Hmily 的 TAC 模式
当使用 Hmily 的 TAC 模式时,开发者必须使用关系型数据库来进行业务操作,Hmily 会自动生成 回滚 SQL;当业务出现异常的时候,Hmily 会自动执行 回滚 SQL 来达到事务的一致性。
![]()
Hmily 的工作原理
Hmily 的工作原理
- Hmily 是一种基于 TCC、TAC 模型的分布式事务解决方案,其核心思想是将分布式事务拆分为各个服务的本地事务来处理。
- 在事务执行过程中,每个参与者服务需要实现
Try / Confirm / Cancel 三个本地事务接口,用于完成资源预处理、提交和回滚。 - 当全局事务开始时,事务协调器会通过 RPC 调用各参与者的
Try 接口;如果所有参与者执行成功,则继续调用 Confirm 完成事务提交;一旦出现异常,则通过 Cancel 接口进行事务回滚。 - Hmily 通过 RPC 在各服务之间传递事务上下文并协调执行流程,从而实现对分布式事务的统一控制。由于通信基于 RPC 协议,只要各语言能够实现对应的 TCC 接口规范,理论上即可支持跨语言的分布式事务场景。
- Hmily 采用去中心化的事务协调方式,由事务发起方充当事务协调器,通过 RPC 调用各参与者的
Try、Confirm、Cancel 接口,并依赖事务日志和恢复机制,在没有独立中心节点的情况下完成分布式事务协调。
![]()
Hmily 事务协调器的工作流程
总结
- Hmily 基于 TCC 模型,通过 RPC 协调各服务执行本地的 Try、Confirm、Cancel 事务,将分布式事务问题转化为多个可控的本地事务,从而实现分布式事务一致性。
- 在 Hmily 中,没有独立部署的中心事务协调器服务。事务发起方本身就是事务协调器,协调逻辑以内嵌组件的形式存在于各个业务服务中,事务状态通过共享的事务日志存储来维护。
Hmily 的注意事项
依赖冲突问题
- Hmily-TCC 依赖了一些通用基础库,在实际项目中可能与已有依赖产生版本冲突。
- 通常需要通过排查 Maven 的依赖树,统一版本号,或使用
exclude 排除冲突依赖来解决。
数据一致性问题
- 如果分布式事务在各个阶段发生异常且缺乏完善的处理机制,可能会导致业务数据不一致。
- 为避免该问题,需要在 TCC 接口的
try、confirm、cancel 阶段设计合理的异常处理和幂等控制,并根据业务场景选择回滚、重试或补偿等策略。
分布式锁问题
- 分布式事务通常涉及多个节点的协同执行,部分场景下需要借助分布式锁来保证数据操作的正确性。
- 在 Hmily-TCC 中,可结合 ZooKeeper、Redis 等组件实现分布式锁。
- 使用分布式锁时需重点关注锁的粒度、超时设置和释放机制,以避免死锁或性能下降。
分布式事务性能问题
- Hmily-TCC 基于 AOP 对目标方法进行代理,在一定程度上会引入性能开销。
- 可通过合理设计 TCC 接口、减少事务粒度、结合缓存或异步处理等方式,对整体性能进行优化。
分布式事务可见性问题
- Hmily-TCC 中事务数据的可见性主要依赖底层数据库事务机制,因此需要确保所使用的数据库具备良好的 ACID 支持,并尽量保持各参与方使用一致的数据库引擎。
- 同时,应合理选择数据库隔离级别,以避免脏读、不可重复读或幻读等事务并发问题。
账户系统的案例(SpringCloud)
特别注意
本节中的案例代码仅用于演示如何通过 SpringCloud + Hmily 实现 TCC 分布式事务,并未涵盖 TCC 在实际运行过程中可能出现的各种事务异常处理场景(如空回滚、防悬挂、第二阶段重复提交等问题),而这些事务异常问题在生产环境中是必须重点考虑并妥善处理的。
代码下载
- 本节完整的案例代码可以直接从 GitHub 下载对应章节
distributed-transaction-01。
版本说明
| 组件 | 版本 | 说明 |
|---|
| JDK | 11 | |
| MySQL | 5.7.44 | |
| Hmily | 2.1.1 | |
| SpringBoot | 2.3.7.RELEASE | Hmily 官方并不支持 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-server | 9091 | Eureka 注册中心 |
fencai-hmily-money-from | 9092 | 扣钱服务(出账) |
fencai-hmily-money-to | 9093 | 加钱服务(入账) |
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
| ├── distributed-transaction-01.iml ├── eureka-server │ ├── eureka-server.iml │ ├── pom.xml │ └── src │ └── main │ ├── java │ │ └── com │ │ └── distributed │ │ └── transaction │ │ └── EurekaServerApplication.java │ └── resources │ └── application.yml ├── fencai-hmily-money-from │ ├── fencai-hmily-money-from.iml │ ├── pom.xml │ └── src │ └── main │ ├── java │ │ └── com │ │ └── distributed │ │ └── transaction │ │ ├── client │ │ │ └── AccountClient.java │ │ ├── controller │ │ │ └── AccountController.java │ │ ├── entity │ │ │ └── Account.java │ │ ├── mapper │ │ │ └── AccountMapper.java │ │ ├── MoneyFromApplication.java │ │ └── service │ │ ├── AccountService.java │ │ └── impl │ │ └── AccountServiceImpl.java │ └── resources │ ├── application.yml │ ├── hmily.yml │ └── mapper │ └── AccountMapper.xml ├── fencai-hmily-money-to │ ├── fencai-hmily-money-to.iml │ ├── pom.xml │ └── src │ └── main │ ├── java │ │ └── com │ │ └── distributed │ │ └── transaction │ │ ├── controller │ │ │ └── AccountController.java │ │ ├── entity │ │ │ └── Account.java │ │ ├── mapper │ │ │ └── AccountMapper.java │ │ ├── MoneyToApplication.java │ │ └── service │ │ ├── AccountService.java │ │ └── impl │ │ └── AccountServiceImpl.java │ └── resources │ ├── application.yml │ ├── hmily.yml │ └── mapper │ └── AccountMapper.xml └── pom.xml
|
TCC 模型说明
在本案例中,账户系统使用的 TCC 模型如下图所示:
![]()
提示
更多关于账户系统其他 TCC 模型的介绍可参考 这里。
整体部署架构
![]()
数据库初始化
1 2 3 4 5
| CREATE DATABASE account_from DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
CREATE DATABASE account_to DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
|
- 创建数据库表(
account_from.t_account)
1 2 3 4 5 6 7 8 9 10 11 12 13
| USE account_from;
CREATE TABLE `t_account` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '物理主键', `name` varchar(256) NOT NULL DEFAULT '' COMMENT '用户名称', `balance` decimal(20,2) NOT NULL DEFAULT '0.00' COMMENT '账户余额', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO account_from.t_account(id, name, balance) VALUES(1, '张三', 100);
|
- 创建数据库表(
account_to.t_account)
1 2 3 4 5 6 7 8 9 10 11 12 13
| USE account_to;
CREATE TABLE `t_account` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '物理主键', `name` varchar(256) NOT NULL DEFAULT '' COMMENT '用户名称', `balance` decimal(20,2) NOT NULL DEFAULT '0.00' COMMENT '账户余额', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
INSERT INTO account_to.t_account(id, name, balance) VALUES(2, '李四', 100);
|
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
| <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.1.1.RELEASE</version> </parent>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR9</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>org.dromara</groupId> <artifactId>hmily-spring-boot-starter-springcloud</artifactId> <version>2.1.1</version> <exclusions> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </dependencyManagement>
<modules> <module>eureka-server</module> <module>fencai-hmily-money-to</module> <module>fencai-hmily-money-from</module> </modules>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
|
注册中心模块
模块配置
- Maven 的 XML 配置内容(
pom.xml)
1 2 3 4 5 6 7
| <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
|
- SpringBoot 的 YML 配置内容(
application.yml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| server: port: 9090
spring: application: name: eureka-server
eureka: instance: hostname: localhost server: enable-self-preservation: false client: register-with-eureka: false fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
|
模块代码
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
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</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> </dependency> </dependencies>
|
- MyBatis 的 XML 映射文件内容(
AccountMapper.xml)
1 2 3 4 5 6 7
| <mapper namespace="com.distributed.transaction.mapper.AccountMapper">
<update id="updateBalance"> update `t_account` set balance = balance + #{delta} where id = #{id} and balance + #{delta} >= 0 </update>
</mapper>
|
- 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
| server: port: 9091
spring: application: name: fencai-hmily-money-from datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.112:3307/account_from?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: "123456"
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.distributed.transaction.entity configuration: map-underscore-to-camel-case: true use-generated-keys: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
eureka: client: service-url: defaultZone: http://localhost:9090/eureka instance: instance-id: ${spring.application.name}-${server.port} prefer-ip-address: true
logging: level: root: info org.dromara.hmily: DEBUG
|
- 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: fencai-hmily-money-from-sc
config: appName: fencai-hmily-money-from-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.56.112:3307/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 36 37 38 39 40 41 42 43 44 45 46 47 48
| import java.io.Serializable; import java.math.BigDecimal;
public class Account implements Serializable {
private Long id;
private String name;
private BigDecimal balance;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal balance) { this.balance = balance; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import org.apache.ibatis.annotations.Param; import java.math.BigDecimal;
public interface AccountMapper {
int updateBalance(@Param("id") Long id, @Param("delta") BigDecimal delta);
}
|
- 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
| import org.dromara.hmily.annotation.Hmily; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import java.math.BigDecimal;
@FeignClient(value = "fencai-hmily-money-to") public interface AccountClient {
@Hmily @GetMapping("/account/{id}/{transMoney}") boolean transfer(@PathVariable("id") Long id, @PathVariable("transMoney") BigDecimal transMoney);
}
|
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
| import java.math.BigDecimal;
public interface AccountService {
boolean tryUpdateBalance(Long fromId, BigDecimal delta, Long toId);
boolean confirmUpdateBalance(Long fromId, BigDecimal delta, Long toId);
boolean cancelUpdateBalance(Long fromId, BigDecimal delta, Long toId);
}
|
- 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
| import com.distributed.transaction.client.AccountClient; import com.distributed.transaction.mapper.AccountMapper; import com.distributed.transaction.service.AccountService; import org.dromara.hmily.annotation.HmilyTCC; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal;
@Service public class AccountServiceImpl implements AccountService {
@Resource private AccountMapper accountMapper;
@Resource private AccountClient accountClient;
@Override @HmilyTCC(confirmMethod = "confirmUpdateBalance", cancelMethod = "cancelUpdateBalance") public boolean tryUpdateBalance(Long fromId, BigDecimal delta, Long toId) { System.out.println("出账 开始"); accountMapper.updateBalance(fromId, delta);
accountClient.transfer(toId, delta.negate());
return Boolean.TRUE; }
@Override public boolean confirmUpdateBalance(Long fromId, BigDecimal delta, Long toId) { System.out.println("出账 确认"); return Boolean.TRUE; }
@Override public boolean cancelUpdateBalance(Long fromId, BigDecimal delta, Long toId) { System.out.println("出账 取消"); accountMapper.updateBalance(fromId, delta.negate()); return Boolean.TRUE; }
}
|
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
| import com.distributed.transaction.service.AccountService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal;
@RestController public class AccountController {
@Resource private AccountService accountService;
@GetMapping("/account/{fromId}/{transMoney}/{toId}") public boolean transfer(@PathVariable("fromId") Long fromId, @PathVariable("transMoney") BigDecimal transMoney, @PathVariable("toId") Long toId) { return accountService.tryUpdateBalance(fromId, transMoney.negate(), toId); }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableEurekaClient @EnableFeignClients @MapperScan("com.distributed.transaction.mapper") @SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) public class MoneyFromApplication {
public static void main(String[] args) { SpringApplication.run(MoneyFromApplication.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
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</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> </dependency> </dependencies>
|
- MyBatis 的 XML 映射文件内容(
AccountMapper.xml)
1 2 3 4 5 6 7
| <mapper namespace="com.distributed.transaction.mapper.AccountMapper">
<update id="updateBalance"> update `t_account` set balance = balance + #{delta} where id = #{id} and balance + #{delta} >= 0 </update>
</mapper>
|
- 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
| server: port: 9092
spring: application: name: fencai-hmily-money-to datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.56.112:3307/account_to?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false username: root password: "123456"
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.distributed.transaction.entity configuration: map-underscore-to-camel-case: true use-generated-keys: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
eureka: client: service-url: defaultZone: http://localhost:9090/eureka instance: instance-id: ${spring.application.name}-${server.port} prefer-ip-address: true
logging: level: root: info org.dromara.hmily: DEBUG
|
- 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: fencai-hmily-money-to-sc
config: appName: fencai-hmily-money-to-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.56.112:3307/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 36 37 38 39 40 41 42 43 44 45 46 47 48
| import java.io.Serializable; import java.math.BigDecimal;
public class Account implements Serializable {
private Long id;
private String name;
private BigDecimal balance;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public BigDecimal getBalance() { return balance; }
public void setBalance(BigDecimal balance) { this.balance = balance; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
public interface AccountMapper {
int updateBalance(@Param("id") Long id, @Param("delta") BigDecimal delta);
}
|
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
| import java.math.BigDecimal;
public interface AccountService {
boolean tryUpdateBalance(Long id, BigDecimal delta);
boolean confirmUpdateBalance(Long id, BigDecimal delta);
boolean cancelUpdateBalance(Long id, BigDecimal delta);
}
|
- 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
| import com.distributed.transaction.mapper.AccountMapper; import com.distributed.transaction.service.AccountService; import org.dromara.hmily.annotation.HmilyTCC; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal;
@Service public class AccountServiceImpl implements AccountService {
@Resource private AccountMapper accountMapper;
@Override @HmilyTCC(confirmMethod = "confirmUpdateBalance", cancelMethod = "cancelUpdateBalance") public boolean tryUpdateBalance(Long id, BigDecimal delta) { System.out.println("入账 开始"); return Boolean.TRUE; }
@Override public boolean confirmUpdateBalance(Long id, BigDecimal delta) { System.out.println("入账 确认"); accountMapper.updateBalance(id, delta); return Boolean.TRUE; }
@Override public boolean cancelUpdateBalance(Long id, BigDecimal delta) { System.out.println("入账 取消"); return Boolean.TRUE; }
}
|
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
| import com.distributed.transaction.service.AccountService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.math.BigDecimal;
@RestController public class AccountController {
@Resource private AccountService accountService;
@GetMapping("/account/{id}/{transMoney}") public boolean transfer(@PathVariable("id") Long id, @PathVariable("transMoney") BigDecimal transMoney) { return accountService.tryUpdateBalance(id, transMoney); }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient @MapperScan("com.distributed.transaction.mapper") @SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) public class MoneyToApplication {
public static void main(String[] args) { SpringApplication.run(MoneyToApplication.class); }
}
|
代码测试
正常调用测试
- (1) 依次启动
eureka-server、fencai-hmily-money-from、fencai-hmily-money-to 应用 - (2) 通过浏览器调用扣钱接口:
http://127.0.0.1:9091/account/1/5/2 - (3) 可以发现各个应用会打印以下日志信息:
fencai-hmily-money-from 应用fencai-hmily-money-to 应用
- (4) 最后观察数据库的数据是否有发生变化,如果数据有发生变化,则说明 Hmily 正常运行
- 数据库表
account_from.t_account:账户余额减少了 5 元 - 数据库表
account_to.t_account:账户余额增加了 5 元
异常调用测试
- (1) 更改
fencai-hmily-money-from 模块中的 Try 方法,故意抛出运行时异常,验证 Cancel 方法是否会被执行(回滚操作)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
@Override @HmilyTCC(confirmMethod = "confirmUpdateBalance", cancelMethod = "cancelUpdateBalance") public boolean tryUpdateBalance(Long fromId, BigDecimal delta, Long toId) { System.out.println("出账 开始"); accountMapper.updateBalance(fromId, delta);
accountClient.transfer(toId, delta.negate());
int num = 10 / 0; return Boolean.TRUE; }
|
- (2) 依次启动
eureka-server、fencai-hmily-money-from、fencai-hmily-money-to 应用 - (3) 通过浏览器调用扣钱接口:
http://127.0.0.1:9091/account/1/5/2 - (4) 可以发现各个应用会打印以下日志信息:
fencai-hmily-money-from 应用fencai-hmily-money-to 应用
- (5) 最后观察数据库的数据是否没有发生变化,如果数据没有发生变化,则说明 Hmily 正常运行
- 数据库表
account_from.t_account:账户余额保持不变 - 数据库表
account_to.t_account:账户余额保持不变
常见错误
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。
参考资料