分布式事务实践之hmily

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服务代码及相关配置

  • 项目结构

image.png
  • 配置

spring-dubbo.xml
使用zookeeper作为注册中心,引用bank2暴露的转账接口,我这里的zookeeper地址需要改成你的zookeeper地址。





    


    

    

    


hmily配置
注意:

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

  • 项目结构

image.png
  • 配置

spring-dubbo.xml
和bank1不同点在于,bank2暴露服务的写法





    


    

    

    



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解决分布式事务的几个步骤

  1. 引入hmily依赖
  2. 创建hmily需要的数据库和表(如果使用mysql)
  3. 设计好TCC分布式事务中的try、confim和cancel三个逻辑。
    本文设计了一个冻结金额字段,为confirm和cancel操作作确认和回滚“铺垫”

github地址
https://github.com/xushengjun/JAVA-01/tree/main/Week_08/day2/homework2/hmily-dubbo-demo

你可能感兴趣的:(分布式事务实践之hmily)