暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL 分布式事务实践之hmily

践行者的脚印 2021-07-06
1663

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作为子工程,存放基础公共类                                

  • 数据库及表

    两个子服务各对应一个数据库和表数据库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框架会自动创建库和表


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

  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服务

  • 项目结构

  • 配置

    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元

日志

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块钱转出

  • bank2

    转账前,ls账户有10000元

转账日志

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块钱 

 

五、踩坑

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



最后修改时间:2021-07-07 17:11:25
文章转载自践行者的脚印,如果涉嫌侵权,请发送邮件至:[email protected]进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论