hmily簡(jiǎn)介
Hmily 一款金融級(jí)的分布式事務(wù)解決方案,支持 Dubbo、Spring Cloud、Motan ,GRPC,BRCP等 RPC 框架進(jìn)行分布式事務(wù)。
本文演示使用hmily框架,TCC方案解決分布式事務(wù)問(wèn)題。
TCC方案,try(業(yè)務(wù)預(yù)處理)-confirm(業(yè)務(wù)確認(rèn))-cancel(業(yè)務(wù)取消,回滾try的處理)。
try執(zhí)行失敗,TM(事務(wù)管理器)會(huì)進(jìn)行cancel回滾操作;
confirm、cancel失敗,TM會(huì)進(jìn)行重試操作
引入hmily框架后,作相關(guān)的配置后,代碼中使用@HmilyTCC注解,標(biāo)記業(yè)務(wù)預(yù)處理所在方法,并在@HmilyTCC注解中配置confirm業(yè)務(wù)確認(rèn)和cancel業(yè)務(wù)取消操作的方法。
@HmilyTCC(confirmMethod = "confirmMethod", cancelMethod = "cancelMethod")
try方法是暴露給業(yè)務(wù)模塊的方法,confirm和cancel方法是提供給hmily框架的方法,用作業(yè)務(wù)確認(rèn)和回滾操作。
說(shuō)明:本文僅粘貼出部分重要配置和代碼,源碼在文末的github倉(cāng)庫(kù)中
一、項(xiàng)目介紹
-
業(yè)務(wù)邏輯
bank1服務(wù)從zs賬戶中扣款,調(diào)用bank2服務(wù),給ls賬戶轉(zhuǎn)賬。
-
技術(shù)棧
zookeeper
docker(可選,因?yàn)楸卷?xiàng)目使用docker創(chuàng)建、啟動(dòng)zookeeper容器)
dubbo
hmily
springboot
mysql
mybatis
-
項(xiàng)目結(jié)構(gòu)及介紹
創(chuàng)建一個(gè)聚合工程hmily-dubbo-demo
bank1和bank2兩個(gè)子服務(wù),bank-common作為子工程,存放基礎(chǔ)公共類
-
數(shù)據(jù)庫(kù)及表
兩個(gè)子服務(wù)各對(duì)應(yīng)一個(gè)數(shù)據(jù)庫(kù)和表
數(shù)據(jù)庫(kù)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 '凍結(jié)金額',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC;
數(shù)據(jù)庫(kù)hmily,hmily框架專用,配置好mysql地址,hmily框架會(huì)自動(dòng)創(chuàng)建庫(kù)和表
-
pom依賴
本項(xiàng)目將所有需要的依賴都放在了bank-common工程中,聚合工程的父pom中僅作依賴的版本控制。
需要添加hmily、dubbo、mysql、mybatis、zookeeper、springboot、spring等相關(guān)依賴
二、bank1服務(wù)代碼及相關(guān)配置
-
項(xiàng)目結(jié)構(gòu)
-
配置
spring-dubbo.xml
使用zookeeper作為注冊(cè)中心,引用bank2暴露的轉(zhuǎn)賬接口,我這里的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作為數(shù)據(jù)庫(kù),本文采用mysql,所以僅做了mysql數(shù)據(jù)源的配置
hmily:
server:
configMode: local
appName: bank1-server
# 如果server.configMode eq local 的時(shí)候才會(huì)讀取到這里的配置信息.
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(業(yè)務(wù)確認(rèn))。
@HmilyTCC注解中,標(biāo)記confim和cancelMethod方法實(shí)現(xiàn)
關(guān)鍵設(shè)計(jì)點(diǎn):賬戶表中的frozen_balance字段
當(dāng)賬戶資金轉(zhuǎn)出時(shí),try方法中判斷資金(account_balance)是否足夠,并將轉(zhuǎn)賬金額先轉(zhuǎn)入凍結(jié)金額(frozen_balance)中。
若bank1和bank2的try方法都成功,則執(zhí)行confirm方法,將bank1中的凍結(jié)金額扣除。
若bank1和bank2的try方法有一方失敗,則執(zhí)行cancel方法,將bank1中的凍結(jié)金額劃回給賬戶(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,扣減失敗");
}
//遠(yuǎn)程調(diào)用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服務(wù)
-
項(xiàng)目結(jié)構(gòu)
-
配置
spring-dubbo.xml
和bank1不同點(diǎn)在于,bank2暴露服務(wù)的寫(xiě)法
<?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作為一個(gè)TM事務(wù)管理器,相對(duì)于bank1和bank2業(yè)務(wù)服務(wù),是一個(gè)公共的第三方模塊。
所以bank2的hmily配置和bank1的不同僅僅是appName的不同。
hmily:
server:
configMode: local
appName: bank2-server
# 如果server.configMode eq local 的時(shí)候才會(huì)讀取到這里的配置信息.
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邏輯實(shí)現(xiàn)
confirmMethod和cancelMethod暴露給hmily,作為確認(rèn)和回滾的方法。
當(dāng)錢(qián)轉(zhuǎn)入bank2時(shí),在try方法中,先將錢(qián)劃入凍結(jié)金額(frozen_balance字段)中,在confirm方法中將錢(qián)從凍結(jié)金額中,劃到賬戶(account_balance字段)中,若失敗,則將凍結(jié)金額中的錢(qián)扣除。
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... ");
}
}
四、驗(yàn)證
-
發(fā)起轉(zhuǎn)賬
瀏覽器訪問(wèn)bank1轉(zhuǎn)賬接口,發(fā)起轉(zhuǎn)賬
http://localhost:8762/bank1/transfer
-
bank1
轉(zhuǎn)賬前
zs賬戶有10000元
日志
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...
轉(zhuǎn)賬后,1塊錢(qián)轉(zhuǎn)出
-
bank2
轉(zhuǎn)賬前,ls賬戶有10000元
轉(zhuǎn)賬日志
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...
轉(zhuǎn)賬后,ls賬戶多了1塊錢(qián)
五、踩坑
try、confirm和cancel方法的入?yún)⒁恢拢駝t即使在 @HmilyTCC注解中配置了confirm和cancel方法,hmily仍會(huì)報(bào)confirm\cancel方法找不到。
六、總結(jié)
使用hmily解決分布式事務(wù)的幾個(gè)步驟
- 引入hmily依賴
- 創(chuàng)建hmily需要的數(shù)據(jù)庫(kù)和表(如果使用mysql)
- 設(shè)計(jì)好TCC分布式事務(wù)中的try、confim和cancel三個(gè)邏輯。
本文設(shè)計(jì)了一個(gè)凍結(jié)金額字段,為confirm和cancel操作作確認(rèn)和回滾“鋪墊”
github地址
https://github.com/xushengjun/JAVA-01/tree/main/Week_08/day2/homework2/hmily-dubbo-demo