hmily简介
Hmily 一款金融级的分布式事务解决方案,支持 Dubbo、Spring Cloud、Motan ,GRPC,BRCP等 RPC 框架进行分布式事务。
本文演示使用hmily框架,TCC方案解决分布式事务问题。
TCC方案,try(业务预处理)-confirm(业务确认)-cancel(业务取消,回滚try的处理)。
try执行失败,TM(事务管理器)会进行cancel回滚操作;
confirm、cancel失败,TM会进行重试操作
引入hmily框架后,作相关的配置后,代码中使用@HmilyTCC注解,标记业务预处理所在方法,并在@HmilyTCC注解中配置confirm业务确认和cancel业务取消操作的方法。
@HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
try方法是暴露给业务模块的方法,confirm和cancel方法是提供给hmily框架的方法,用作业务确认和回滚操作。
说明:本文仅粘贴出部分重要配置和代码,源码在文末的github仓库中
一、项目介绍
-
业务逻辑
bank1服务从zs账户中扣款,调用bank2服务,给ls账户转账。
-
技术栈
zookeeper
docker(可选,因为本项目使用docker创建、启动zookeeper容器)
dubbo
hmily
springboot
mysql
mybatis
-
项目结构及介绍
创建一个聚合工程hmily-dubbo-demo
bank1和bank2两个子服务,bank-common作为子工程,存放基础公共类
image.png
-
数据库及表
两个子服务各对应一个数据库和表
数据库bank1和bank2,表account_info
CREATE TABLE `account_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`account_name` varchar(100) COLLATE utf8_bin DEFAULT NULL COMMENT '户主姓名',
`account_balance` double DEFAULT NULL COMMENT '帐户余额',
`frozen_balance` double DEFAULT NULL COMMENT '冻结金额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
数据库hmily,hmily框架专用,配置好mysql地址,hmily框架会自动创建库和表
image.png
-
pom依赖
本项目将所有需要的依赖都放在了bank-common工程中,聚合工程的父pom中仅作依赖的版本控制。
需要添加hmily、dubbo、mysql、mybatis、zookeeper、springboot、spring等相关依赖
二、bank1服务代码及相关配置
-
项目结构
-
配置
spring-dubbo.xml
使用zookeeper作为注册中心,引用bank2暴露的转账接口,我这里的zookeeper地址需要改成你的zookeeper地址。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="bank1-server"/>
<dubbo:registry protocol="zookeeper" address="192.168.99.105:2181"/>
<dubbo:protocol name="dubbo" port="20886"
server="netty" client="netty"
charset="UTF-8" threadpool="fixed" threads="500"
queues="0" buffer="8192" accepts="0" payload="8388608"/>
<dubbo:reference timeout="500000000"
interface="org.example.service.Bank2AccountService"
id="bank2AccountService"
retries="0" check="false" actives="20" loadbalance="hmilyRandom"/>
</beans>
hmily配置
注意:
- appName的名称,server和config中保持一致
- hmily支持使用mysql、mongodb、zookeeper、redis作为数据库,本文采用mysql,所以仅做了mysql数据源的配置
hmily:
server:
configMode: local
appName: bank1-server
# 如果server.configMode eq local 的时候才会读取到这里的配置信息.
config:
appName: bank1-server
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
remote:
zookeeper:
serverList: 127.0.0.1:2181
fileExtension: yml
path: /hmily/xiaoyu
repository:
database:
driverClassName: com.mysql.jdbc.Driver
url : jdbc:mysql://127.0.0.1:3306/hmily?useUnicode=true&characterEncoding=utf8
username: root
password: root
maxActive: 20
minIdle: 10
connectionTimeout: 30000
idleTimeout: 600000
maxLifetime: 1800000
file:
path:
prefix: /hmily
mongo:
databaseName:
url:
userName:
password:
zookeeper:
host: localhost:2181
sessionTimeOut: 1000
rootPath: /hmily
redis:
cluster: false
sentinel: false
clusterUrl:
sentinelUrl:
masterName:
hostName:
port:
password:
maxTotal: 8
maxIdle: 8
minIdle: 2
maxWaitMillis: -1
minEvictableIdleTimeMillis: 1800000
softMinEvictableIdleTimeMillis: 1800000
numTestsPerEvictionRun: 3
testOnCreate: false
testOnBorrow: false
testOnReturn: false
testWhileIdle: false
timeBetweenEvictionRunsMillis: -1
blockWhenExhausted: true
timeOut: 1000
metrics:
metricsName: prometheus
host:
port: 9071
async: true
threadCount : 16
jmxConfig:
-
代码
decreaseBalance方法作为try(业务确认)。
@HmilyTCC注解中,标记confim和cancelMethod方法实现
关键设计点:账户表中的frozen_balance字段
当账户资金转出时,try方法中判断资金(account_balance)是否足够,并将转账金额先转入冻结金额(frozen_balance)中。
若bank1和bank2的try方法都成功,则执行confirm方法,将bank1中的冻结金额扣除。
若bank1和bank2的try方法有一方失败,则执行cancel方法,将bank1中的冻结金额划回给账户(account_balance)中。
Bank1AccountServiceImpl代码
package org.example.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hmily.annotation.HmilyTCC;
import org.dromara.hmily.common.exception.HmilyRuntimeException;
import org.example.AccountInfo;
import org.example.mapper.AccountInfoMapper;
import org.example.service.Bank1AccountService;
import org.example.service.Bank2AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service("bank1AccountService")
@Slf4j
public class Bank1AccountServiceImpl implements Bank1AccountService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Autowired
private Bank2AccountService bank2AccountService;
@Override
@Transactional
@HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
public Boolean decreaseBalance(String name, Double amount) {
//从账户扣减
if (accountInfoMapper.decreaseBalance(name, amount) <= 0) {
//扣减失败
throw new HmilyRuntimeException("bank1 exception,扣减失败");
}
//远程调用bank2
if (!bank2AccountService.increaseAccountBalance("ls", amount)) {
throw new HmilyRuntimeException("bank2Client exception");
}
if (amount == 10) {//异常一定要抛在Hmily里面
throw new RuntimeException("bank1 make exception 10");
}
log.info("******** Bank1 Service end try... ");
return Boolean.TRUE;
}
@Override
public AccountInfo selectByName(String accountName) {
return accountInfoMapper.selectByName(accountName);
}
public boolean confirmMethod(String name, Double amount) {
int result = accountInfoMapper.confirm();
log.info("******** Bank1 Service begin commit...");
return result > 0;
}
public boolean cancelMethod(String name, Double amount) {
int result = accountInfoMapper.cancel();
log.info("******** Bank1 Service end rollback... ");
return result > 0;
}
}
accountInfoMapper.decreaseBalance方法
注意,我的update方法的条件,使用了 account_balance > #{amount} 判断金额是否足够。
@Update("update account_info set account_balance = account_balance - #{amount} , frozen_balance = frozen_balance + #{amount} " +
"where account_balance > #{amount} and account_name = #{name}")
int decreaseBalance(@Param("name") String name, @Param("amount") Double amount);
三、bank2服务
-
项目结构
-
配置
spring-dubbo.xml
和bank1不同点在于,bank2暴露服务的写法
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<dubbo:application name="bank2_service"/>
<dubbo:registry protocol="zookeeper" address="192.168.99.105:2181"/>
<dubbo:protocol name="dubbo" port="20886"
server="netty" client="netty"
charset="UTF-8" threadpool="fixed" threads="500"
queues="0" buffer="8192" accepts="0" payload="8388608"/>
<dubbo:service interface="org.example.service.Bank2AccountService"
ref="bank2AccountService" executes="20"/>
</beans>
application.yml
server:
port: 8763
spring:
application:
name: bank2-server
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/bank2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
logging:
level:
root: info
org.springframework.web: info
org.apache.ibatis: info
org.dromara.hmily.bonuspoint: debug
org.dromara.hmily.lottery: debug
org.dromara.hmily: debug
io.netty: info
org.example: debug
hmily.yml
hmily作为一个TM事务管理器,相对于bank1和bank2业务服务,是一个公共的第三方模块。
所以bank2的hmily配置和bank1的不同仅仅是appName的不同。
hmily:
server:
configMode: local
appName: bank2-server
# 如果server.configMode eq local 的时候才会读取到这里的配置信息.
config:
appName: bank2-server
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
remote:
zookeeper:
serverList: 127.0.0.1:2181
fileExtension: yml
path: /hmily/xiaoyu
repository:
database:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/hmily?useUnicode=true&characterEncoding=utf8
username: root
password: root
maxActive: 20
minIdle: 10
connectionTimeout: 30000
idleTimeout: 600000
maxLifetime: 1800000
file:
path:
prefix: /hmily
mongo:
databaseName:
url:
userName:
password:
zookeeper:
host: localhost:2181
sessionTimeOut: 1000
rootPath: /hmily
redis:
cluster: false
sentinel: false
clusterUrl:
sentinelUrl:
masterName:
hostName:
port:
password:
maxTotal: 8
maxIdle: 8
minIdle: 2
maxWaitMillis: -1
minEvictableIdleTimeMillis: 1800000
softMinEvictableIdleTimeMillis: 1800000
numTestsPerEvictionRun: 3
testOnCreate: false
testOnBorrow: false
testOnReturn: false
testWhileIdle: false
timeBetweenEvictionRunsMillis: -1
blockWhenExhausted: true
timeOut: 1000
metrics:
metricsName: prometheus
host:
port: 9072
async: true
threadCount: 16
jmxConfig:
-
代码
increaseAccountBalance作为try逻辑实现
confirmMethod和cancelMethod暴露给hmily,作为确认和回滚的方法。
当钱转入bank2时,在try方法中,先将钱划入冻结金额(frozen_balance字段)中,在confirm方法中将钱从冻结金额中,划到账户(account_balance字段)中,若失败,则将冻结金额中的钱扣除。
package org.example.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.dromara.hmily.annotation.HmilyTCC;
import org.example.mapper.AccountInfoMapper;
import org.example.service.Bank2AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service("bank2AccountService")
@Slf4j
public class Bank2AccountServiceImpl implements Bank2AccountService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Override
@Transactional
@HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
public boolean increaseAccountBalance(String accountName, Double amount) {
accountInfoMapper.increaseAccountBalance(accountName, amount);
log.info("******** Bank2 Service Begin try ...");
return Boolean.TRUE;
}
@Override
public String hi(String serverName) {
return "hello," + serverName;
}
public void confirmMethod(String accountName, Double amount) {
accountInfoMapper.confirmAccountBalance();
log.info("******** Bank2 Service commit... ");
}
public void cancelMethod(String accountName, Double amount) {
accountInfoMapper.cancelAccountBalance(accountName);
log.info("******** Bank2 Service begin cancel... ");
}
}
四、验证
-
发起转账
浏览器访问bank1转账接口,发起转账
http://localhost:8762/bank1/transfer
-
bank1
转账前
zs账户有10000元
image.png
日志
2021-03-28 10:25:19.442 DEBUG 8008 --- [nio-8762-exec-7] o.d.h.t.e.HmilyTccTransactionExecutor : ......hmily tcc transaction starter....
2021-03-28 10:25:19.453 DEBUG 8008 --- [nio-8762-exec-7] o.e.m.AccountInfoMapper.decreaseBalance : ==> Preparing: update account_info set account_balance = account_balance - ? , frozen_balance = frozen_balance + ? where account_balance > ? and account_name = ?
2021-03-28 10:25:19.454 DEBUG 8008 --- [nio-8762-exec-7] o.e.m.AccountInfoMapper.decreaseBalance : ==> Parameters: 1.0(Double), 1.0(Double), 1.0(Double), zs(String)
2021-03-28 10:25:19.457 DEBUG 8008 --- [nio-8762-exec-7] o.e.m.AccountInfoMapper.decreaseBalance : <== Updates: 1
2021-03-28 10:25:19.488 INFO 8008 --- [nio-8762-exec-7] o.e.s.impl.Bank1AccountServiceImpl : ******** Bank1 Service end try...
2021-03-28 10:25:19.493 DEBUG 8008 --- [ecutorHandler-7] o.d.h.t.e.HmilyTccTransactionExecutor : hmily transaction confirm .......!start
2021-03-28 10:25:19.495 DEBUG 8008 --- [ecutorHandler-7] o.e.mapper.AccountInfoMapper.confirm : ==> Preparing: update account_info set frozen_balance = 0 where frozen_balance > 0
2021-03-28 10:25:19.495 DEBUG 8008 --- [ecutorHandler-7] o.e.mapper.AccountInfoMapper.confirm : ==> Parameters:
2021-03-28 10:25:19.504 DEBUG 8008 --- [ecutorHandler-7] o.e.mapper.AccountInfoMapper.confirm : <== Updates: 1
2021-03-28 10:25:19.504 INFO 8008 --- [ecutorHandler-7] o.e.s.impl.Bank1AccountServiceImpl : ******** Bank1 Service begin commit...
转账后,1块钱转出
image.png
-
bank2
转账前,ls账户有10000元
image.png
转账日志
2021-03-28 10:25:19.467 DEBUG 7979 --- [:20886-thread-6] o.d.h.t.e.HmilyTccTransactionExecutor : ......hmily tcc transaction starter....
2021-03-28 10:25:19.474 DEBUG 7979 --- [:20886-thread-6] o.e.m.A.increaseAccountBalance : ==> Preparing: update account_info set frozen_balance = ? where account_name = ?
2021-03-28 10:25:19.475 DEBUG 7979 --- [:20886-thread-6] o.e.m.A.increaseAccountBalance : ==> Parameters: 1.0(Double), ls(String)
2021-03-28 10:25:19.477 DEBUG 7979 --- [:20886-thread-6] o.e.m.A.increaseAccountBalance : <== Updates: 1
2021-03-28 10:25:19.477 INFO 7979 --- [:20886-thread-6] o.e.s.impl.Bank2AccountServiceImpl : ******** Bank2 Service Begin try ...
2021-03-28 10:25:19.480 DEBUG 7979 --- [ecutorHandler-7] o.d.h.t.e.HmilyTccTransactionExecutor : hmily transaction confirm .......!start
2021-03-28 10:25:19.482 DEBUG 7979 --- [ecutorHandler-7] o.e.m.A.confirmAccountBalance : ==> Preparing: update account_info set account_balance = account_balance + frozen_balance , frozen_balance = 0 where frozen_balance > 0
2021-03-28 10:25:19.483 DEBUG 7979 --- [ecutorHandler-7] o.e.m.A.confirmAccountBalance : ==> Parameters:
2021-03-28 10:25:19.490 DEBUG 7979 --- [ecutorHandler-7] o.e.m.A.confirmAccountBalance : <== Updates: 1
2021-03-28 10:25:19.490 INFO 7979 --- [ecutorHandler-7] o.e.s.impl.Bank2AccountServiceImpl : ******** Bank2 Service commit...
转账后,ls账户多了1块钱
image.png
五、踩坑
try、confirm和cancel方法的入参要一致,否则即使在 @HmilyTCC注解中配置了confirm和cancel方法,hmily仍会报confirm\cancel方法找不到。
六、总结
使用hmily解决分布式事务的几个步骤
- 引入hmily依赖
- 创建hmily需要的数据库和表(如果使用mysql)
- 设计好TCC分布式事务中的try、confim和cancel三个逻辑。
本文设计了一个冻结金额字段,为confirm和cancel操作作确认和回滚“铺垫”
github地址
https://github.com/xushengjun/JAVA-01/tree/main/Week_08/day2/homework2/hmily-dubbo-demo
网友评论