TCC(Try/Confirm/Cancel)编程模式的核心思想是:针对每个分支事务操作,都要向全局事务发起方注册Try、Confirm和Cancel三个操作,具体这些操作由我们自己根据业务进行实现,然后分为两个阶段去执行:
- Try 阶段主要是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能真正构成一个完整的业务逻辑。
- Confirm 阶段主要是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。
- Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
TCC框架
目前市面上的TCC框架众多比如下面这几种:
框架名称 | Gitbub地址 |
---|---|
tcc-transaction | https://github.com/changmingxie/tcc-transaction |
Hmily | https://github.com/yu199195/hmily |
ByteTCC | https://github.com/liuyangming/ByteTCC |
EasyTransaction | https://github.com/QNJR-GROUP/EasyTransaction |
Hmily是一个高性能分布式事务tcc开源框架。基于java语言来开发(JDK1.8),支持dubbo,springcloud,motan等rpc框架进行分布式事务。它目前支持以下特性:
- 支持嵌套事务(Nested transaction support)。
- 采用disruptor框架进行事务日志的异步读写,与RPC框架的性能毫无差别。
- 支持SpringBoot-starter 项目启动,使用简单。
- RPC框架支持 : dubbo,motan,springcloud。
- 本地事务存储支持 : redis,mongodb,zookeeper,file,mysql。
- 事务日志序列化支持 :java,hessian,kryo,protostuff。
- 采用Aspect AOP 切面思想与Spring无缝集成,天然支持集群。
- RPC事务恢复,超时异常恢复等。
Hmily利用AOP对参与分布式事务的本地方法与远程方法进行拦截处理,通过多方拦截,事务参与者能透明的调用到另一方的Try、Conform、Cancel方法;传递事务上下文;并记录事务日志,酌情进行补偿,重试等。
Hmily不需要事务协调服务,但需要提供一个数据库(mysql/mongodb/zookeeper/redis/file)来进行日志存储。Hmily实现的TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try业务。Confirm/Cancel业务逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被Hmily事务框架发现即可,不需要被调用它的其他业务服务所感知。
官网介绍:https://dromara.org/website/zh-cn/docs/hmily/index.html
Hmily快速入门
业务说明
本案例通过hmily框架实现TCC分布式事务,模拟两个账户的转账交易过程。两个账户分别在不同的银行(张三在bank1、李四在bank2),bank1、bank2是两个微服务。对于交易过程中的每个操作,要么都成功,要么都失败。
数据库
创建bank1库,并导入以下表结构和数据:
CREATE DATABASE `bank1` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
USE bank1;
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
INSERT INTO `account_info` VALUES (1, '张三', '1', '', 10000);
创建bank2库,并导入以下表结构和数据:
CREATE DATABASE `bank2` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
USE bank2;
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '户主姓名',
`account_no` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '银行卡号',
`account_password` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '帐户密码',
`account_balance` double NULL DEFAULT NULL COMMENT '帐户余额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
INSERT INTO `account_info` VALUES (2, '李四', '2', NULL, 0);
Hmily用来存储日志的数据表由它自动创建
演示案例代码
发起者
application.yml配置(只显示hmily部分)
org:
dromara:
hmily :
serializer : kryo #序列化工具
retryMax : 30 #最大重试次数
repositorySupport : db #持久化方式
started: true #事务发起方
hmilyDbConfig :
driverClassName : com.mysql.jdbc.Driver
url : jdbc:mysql://localhost:3306/bank1?useUnicode=true
username : root
password : 123
Hmily配置类
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class DatabaseConfiguration {
@Autowired
private Environment env;
@Bean
public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){
HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retryMax")));
hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySupport"));
hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.started")));
HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassName"));
hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url"));
hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username"));
hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password"));
hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
return hmilyTransactionBootstrap;
}
}
feign代理
@FeignClient(value = "hmily-demo-bank2")
public interface Bank2Client {
@GetMapping("/bank2/transfer")
@Hmily
Boolean transfer(@RequestParam("amount") Double amount);
}
业务
注意:Try、Confirm、cancel的方法参数必须保持一致。
@Service
public class AccountInfoTccImpl implements AccountInfoTcc {
@Autowired
private AccountInfoDao accountInfoDao;
@Autowired
private Bank2Client bank2Client;
@Override
@Hmily(confirmMethod = "commit", cancelMethod = "rollback")
public void prepare( String accountNo, double amount) {
System.out.println("...Bank1 Service prepare..." );
if(!bank2Client.transfer(amount)){
throw new RuntimeException("bank2 exception");
}
}
@Override
public void commit( String accountNo, double amount) {
System.out.println("...Bank1 Service commit..." );
}
@Override
public void rollback( String accountNo, double amount) {
accountInfoDao.updateAccountBalance(accountNo ,amount );
System.out.println("...Bank1 Service rollback..." );
}
}
启动类
@SpringBootApplication(exclude = MongoAutoConfiguration.class,scanBasePackages = {"org.dromara.hmily","cn.itcast.tcc.account"})
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"cn.itcast.tcc.hmilydemo.bank1.feignClient"})
public class Bank1HmilyServer {
public static void main(String[] args) {
SpringApplication.run(Bank1HmilyServer.class, args);
}
}
参与者
org:
dromara:
hmily :
serializer : kryo #序列化工具
retryMax : 30 #最大重试次数
repositorySupport : db #持久化方式
started: false #事务参与方
hmilyDbConfig :
driverClassName : com.mysql.jdbc.Driver
url : jdbc:mysql://localhost:3306/bank2?useUnicode=true
username : root
password : 123
业务
@Service
public class AccountInfoServiceImpl implements AccountInfoService {
@Autowired
private AccountInfoDao accountInfoDao;
@Override
@Hmily(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
public Boolean updateAccountBalance(String accountNo, Double amount) {
System.out.println("...Bank2 Service Begin ...");
try{
accountInfoDao.updateAccountBalance(accountNo ,amount);
}catch(Exception e){
e.printStackTrace();
throw new RuntimeException( e.getMessage() );
}
return true;
}
public Boolean confirmMethod(String accountNo, Double amount) {
System.out.println("...Bank2 Service commit..." );
return true;
}
public Boolean cancelMethod(String accountNo, Double amount) {
accountInfoDao.updateAccountBalance(accountNo ,amount * -1);
System.out.println("...Bank2 Service rollback..." );
return true;
}
}
Hmily环境搭建
新建数据库tcc_undo_log,此库用来存储hmily事务日志,空库即可,由hmily自动建表。
CREATE DATABASE `tcc_undo_log` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
Maven环境
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-springcloud</artifactId>
<version>2.0.5-RELEASE</version>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</exclusion>
<exclusion>
<artifactId>logback-core</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
yaml文件
org.dromara.hmily.serializer = kryo
org.dromara.hmily.retryMax = 30
org.dromara.hmily.repositorySupport = db
org.dromara.hmily.started = true #事务发起方 false为参与方(被调用)
org.dromara.hmily.hmilyDbConfig.driverClassName = com.mysql.cj.jdbc.Driver
org.dromara.hmily.hmilyDbConfig.url = jdbc:mysql://localhost:3306/tcc_undo_log?useUnicode=true
org.dromara.hmily.hmilyDbConfig.username = root
org.dromara.hmily.hmilyDbConfig.password = 123
新建配置文件
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class HmilyConfig {
@Autowired
private Environment env;
@Bean
public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){
HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retryMax")));
hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySupport"));
hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.started")));
HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassName"));
hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url"));
hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username"));
hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password"));
hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
return hmilyTransactionBootstrap;
}
}
启动类上增加org.dromara.hmily的扫描项
@SpringBootApplication(exclude = MongoAutoConfiguration.class,scanBasePackages = {"org.dromara.hmily","cn.itcast.wanxinp2p.account"})
Feign代理(远程接口)中,controller的方法中要增加@Hmily注解
案例
@FeignClient(value="account-service")//服务名
public interface AccountApiAgent {
@PostMapping(value = "/account/tcc/accounts")
@Hmily
RestResponse<AccountDTO> register(@RequestBody AccountRegisterDTO dto);
}
@Override
@Hmily(confirmMethod = "confirmRegister", cancelMethod = "cancelRegister")
public void register(ConsumerRegisterDTO consumerRegisterDTO) {
... ...
}
public void confirmRegister(ConsumerRegisterDTO consumerRegisterDTO) {
log.info("execute confirmRegister");
}
public void cancelRegister(ConsumerRegisterDTO consumerRegisterDTO) {
log.info("execute cancelRegister");
remove(Wrappers.<Consumer>lambdaQuery().eq(Consumer::getMobile,
consumerRegisterDTO.getMobile()));
}