分布式事務(wù)實(shí)踐之hmily

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ǔ)公共類


image.png
  • 數(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ù)和表


image.png
  • pom依賴

本項(xiàng)目將所有需要的依賴都放在了bank-common工程中,聚合工程的父pom中僅作依賴的版本控制。
需要添加hmily、dubbo、mysql、mybatis、zookeeper、springboot、spring等相關(guān)依賴

二、bank1服務(wù)代碼及相關(guān)配置

  • 項(xiàng)目結(jié)構(gòu)

image.png
  • 配置

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配置
注意:

  1. appName的名稱,server和config中保持一致
  2. 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)

image.png
  • 配置

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元


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...


轉(zhuǎn)賬后,1塊錢(qián)轉(zhuǎn)出


image.png
  • bank2

轉(zhuǎn)賬前,ls賬戶有10000元


image.png

轉(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)


image.png

五、踩坑

try、confirm和cancel方法的入?yún)⒁恢拢駝t即使在 @HmilyTCC注解中配置了confirm和cancel方法,hmily仍會(huì)報(bào)confirm\cancel方法找不到。

六、總結(jié)

使用hmily解決分布式事務(wù)的幾個(gè)步驟

  1. 引入hmily依賴
  2. 創(chuàng)建hmily需要的數(shù)據(jù)庫(kù)和表(如果使用mysql)
  3. 設(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容