动态多数据源使用seata实现分布式事务

22 篇文章 0 订阅
12 篇文章 0 订阅

一、背景

目前公司很多应用因为历史原因,一个应用访问多个数据库进行插入和更新操作,这就可能产生数据一致性问题,同时应用如果跨服务的调用也可能会产生事务问题。

目前应用是采用dynamic-datasource-spring-boot-starter做多数据源控制的。而seata是一款开源的分布式事务框架。我们了解到dynamic-datasource-spring-boot-starter的新版本已经支持基于seata的分布式事务了,而官网的例子基本都是标准的单数据源的整合,下面我们分别对dynamic-datasource-spring-boot-starter、seata以及它们的整合的进行功能使用实践。

二、dynamic-datasource

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。相关的特性如下,更多的信息可以查看github官网。我们这里主要用到了它对seata数据源的支持和动态切换数据源的特性。

官网地址:https://github.com/baomidou/dynamic-datasource-spring-boot-starter

  1. 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从混合模式。

  2. 支持数据库敏感配置信息加密 ENC()。

  3. 支持每个数据库独立初始化表结构schema和数据库database。

  4. 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。

  5. 支持自定义注解 ,需继承DS(3.2.0+)。

  6. 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。

  7. 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。

  8. 提供自定义数据源来源 方案(如全从数据库加载)。

  9. 提供项目启动后动态增加移除数据源方案。

  10. 提供Mybatis环境下的 纯读写分离方案。

  11. 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。

  12. 支持多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。

  13. 提供基于seata的分布式事务方案。

  14. 提供本地多数据源事务方案。

三、seata介绍

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata主打AT模式。AT模式的机制如下:

  1. 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  2. 二阶段:提交异步化,非常快速地完成。回滚通过一阶段的回滚日志进行反向补偿。

四、动手实践

在实际使用上面的框架过程中,会遇到各式各样的场景,遇到各种问题。实践出真知,下面我们模拟简单的业务实际动手来检测下他们的功能吧!

4.1 业务流程图

如上图所示,我们模拟一个下单的业务,主要的业务流程:

  1. postman使用http请求下单服务

  2. 下单服务访问订单库产生下单记录、访问信用分库校验和扣减信用分

  3. 访问库存库,扣减库存

  4. 如果下单成功,最终会返回true

4.2 准备环境

  1. 数据库:seata_storage(库存数据库)、seata_order(订单数据库)、seata_credit(信用分数据库)、seata(seata数据库)

  2. 应用:order-service(下单服务)、storage-service(库存服务)、seata tc(分布式事务协调器)

  3. 配置中心: Nacos

  4. seata服务端:本次实践的seata版本为v1.4.2

4.2.1 seata的安装

具体的安装步骤见官网:http://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html

  1. 下载tc包,https://github.com/seata/seata/releases/tag/v1.4.2

  2. tc包解压后,修改register.conf,进行nacos地址,名称空间等配置。

  3. nacos配置项,可以将通过脚本:https://github.com/seata/seata/tree/1.4.2/script/config-center/nacos自动上传修改;也可以自行在nacos上手动添加配置项;该配置项主要为seata集群数据库地址和一些开关等;

  4. 创建seata数据库,sql脚本见:https://github.com/seata/seata/tree/1.4.2/script/server/db

  5. 启动seata服务端,使用解压包中的seataserver.sh启动,可在nacos查看启动的seata集群

踩坑:nacos如果使用带有权限的版本,密码不要带有特殊字符,否则在启动时会一直报403错误,因为从seata的服务端request到nacos时,密码特殊字符被转义传递从而导致错误。

4.2.2 业务应用的配置订单应用(接入了两个数据库):

##########服务注册
spring.cloud.nacos.discovery.server-addr=localhost:8848
spring.cloud.nacos.discovery.username=nacos_test
spring.cloud.nacos.discovery.password=nacos_test 
##########seata相关
seata.enabled=true 
seata.tx-service-group=my_test_tx_group
seata.enable-auto-data-source-proxy=false
seata.config.type=nacos 
seata.config.nacos.data-id=seataServer.properties 
seata.config.nacos.server-addr=localhost:8848
seata.config.nacos.application=seata-server 
seata.config.nacos.namespace=7322f40d-74de-4679-ad4c-c3dff499cd98
seata.config.nacos.group=SEATA_GROUP 
seata.config.nacos.username=seata 
seata.config.nacos.password=seata 
seata.registry.type=nacos 
seata.registry.nacos.server-addr=localhost:8848
seata.registry.nacos.namespace=7322f40d-74de-4679-ad4c-c3dff499cd98 
seata.registry.nacos.group=SEATA_GROUP 
seata.registry.nacos.username=seata 
seata.registry.nacos.password=seata 
logging.level.io.seata = debug
########数据源1(主数据源,订单库) 
spring.datasource.dynamic.primary=master
spring.datasource.dynamic.seata=true
spring.datasource.dynamic.datasource.master.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.dynamic.datasource.master.url=jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.dynamic.datasource.master.username=seata
spring.datasource.dynamic.datasource.master.password=seata
#########数据源2(信用分库)
spring.datasource.dynamic.datasource.credit.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.dynamic.datasource.credit.url=jdbc:mysql://localhost:3306/seata_credit?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.dynamic.datasource.credit.username=seata 
spring.datasource.dynamic.datasource.credit.password=seata

库存应用(一个数据库):

##########服务注册 
spring.cloud.nacos.discovery.server-addr=localhost:8848 
spring.cloud.nacos.discovery.username=nacos_test 
spring.cloud.nacos.discovery.password=nacos_test
####seata相关
seata.enabled=true seata.tx-service-group=my_test_tx_group 
seata.enable-auto-data-source-proxy=false 
seata.config.type=nacos
seata.config.nacos.data-id=seataServer.properties 
seata.config.nacos.server-addr=localhost:8848 
seata.config.nacos.application=seata-server 
seata.config.nacos.namespace=7322f40d-74de-4679-ad4c-c3dff499cd98
seata.config.nacos.group=SEATA_GROUP 
seata.config.nacos.username=seata 
seata.config.nacos.password=seata 
seata.registry.type=nacos 
seata.registry.nacos.server-addr=localhost:8848 
seata.registry.nacos.namespace=7322f40d-74de-4679-ad4c-c3dff499cd98 
seata.registry.nacos.group=SEATA_GROUP 
seata.registry.nacos.username=seata
seata.registry.nacos.password=seata 
logging.level.io.seata = debug
######数据源
spring.datasource.dynamic.primary=master 
spring.datasource.dynamic.seata=true 
spring.datasource.dynamic.datasource.master.driver-class-name=com.mysql.jdbc.Driver 
spring.datasource.dynamic.datasource.master.url=jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.dynamic.datasource.master.username=seata
spring.datasource.dynamic.datasource.master.password=seata

pom依赖:

<dependency>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <groupId>com.baomidou</groupId>
  <version>3.2.1</version>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  <exclusions>
  <exclusion>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  </exclusion>
  </exclusions>
</dependency>
<dependency>
  <groupId>io.seata</groupId>
  <artifactId>seata-spring-boot-starter</artifactId>
  <version>1.4.2</version>
</dependency>

4.3 开始验证

下面我们分多种情况来验证这种多数据源的事务是否有效,再加上SEATA来控制事务会产生什么效果。 4.3.1 同应用多库多表调用逻辑为:订单服务向订单库插入订单记录,接着向信用分库扣除信用分,暂时不访问库存服务1)订单controller的代码:

@RequestMapping("/orderByCredit")
public Boolean orderByCredit(String userId, @Nullable String requestId, int count) {
    //①下单,并产生下单日志
    orderService.onlyOrder(userId, "product-1", requestId, count);
    //② 访问信用库,校验并扣减信用分
    creditService.checkHasCredit(userId, count);
    return true;
}

2)使用多数据源@DS注解访问credit库修改数据

@DS("credit")
public void checkHasCredit(String userId, Integer val){
    Credit credit = creditDAO.selectByPrimaryKey(userId);
    if (credit == null) {
      throw new RuntimeException("无信用,无法购买");
    }
    credit.setCredit(credit.getCredit() - val);
    creditDAO.updateByPrimaryKey(credit);
    if (credit.getCredit() <= 0) {
      throw new RuntimeException("信用分不足,无法购买");
    }
}

3)使用postman请求使得,新增订单记录成功,并操作第二个数据库信用分不足报错,该情况按照预期,事务不会生效,订单库插入数据成功,接口也报错了。

注:订单库订单表插入成功,订单日志失败也不会回滚4)加上spring的@Transactional注解,下单请求

@RequestMapping("/orderByCredit")
@Transactional
public Boolean orderByCredit(String userId, @Nullable String requestId, int count) {
    //①下单,并产生下单日志
    orderService.onlyOrder(userId, "product-1", requestId, count);
    //② 访问信用库,校验并扣减信用分
    creditService.checkHasCredit(userId, count);
    return true;
}

这种情况产生找不到表的报错,操作②的@DS("credit")注解失效,没有按照预期访问credit库,访问到操作①的库了,但是操作①插入的数据回滚了。

注:订单库订单表插入成功,订单日志插入失败也会回滚。

小结: 所以在这里我们稍微总结下,在一个方法中,如果操作了多个数据源,不能在外面使用@Transactional来进行事务控制,会使得@DB注解切换数据源不生效,产生报错,但是不报错的部分事务有效,因为还没提交。如何来对这种操作多数据源的情况进行事务控制呢,那就需要使用分布式事务SEATA5)加上seata的@GlobalTransactional注解,下单请求

@RequestMapping("/orderByCredit")
@GlobalTransactional
public Boolean orderByCredit(String userId, @Nullable String requestId, int count) {
    //① 下单,并产生下单日志
    orderService.onlyOrder(userId, "product-1", requestId, count);
    //② 访问信用库,校验并扣减信用分
    creditService.checkHasCredit(userId, count);
    return true;
}

在seata可用的情况下,步骤①,步骤②任意的地方出现错误,均可以事务回滚。

6)同时加上seata的@GlobalTransactional注解和Spring的@Transactional,下单请求 @Transactional会导致切换数据库异常。

4.3.2 跨应用多库多表

调用逻辑为:

订单服务向订单库插入订单记录,接着向信用分库扣除信用分,远程调用库存服务扣减库存。

1)下订单,使得扣减库存报错

订单服务

@RequestMapping("/orderByCredit")
@GlobalTransactional
public Boolean orderByCredit(String userId, @Nullable String requestId,@Nullable String requestId2,  int count) {
    //① 下单,并产生下单日志
    orderService.onlyOrder(userId, "product-1", requestId, count);
    //② 访问直连信用库,校验并扣减信用分
    creditService.checkHasCredit(userId, count);
    //③ 调用库存服务,扣减库存
    storageFeignClient.deductFlow("product-1", count, requestId2);
    return true;
}

库存服务

public void deductFlow(String commodityCode, int count, String requestId) throws InterruptedException {
    Storage storage = this.deduct(commodityCode, count);
    StorageFlow storageFlow = new StorageFlow();
    storageFlow.setStorageId(storage.getId());
    storageFlow.setFlowId(StringUtils.isEmpty(requestId) ? String.valueOf(System.currentTimeMillis()): requestId);
    storageFlowDAO.insert(storageFlow);
}

库存服务中的任意地方报错,调用链的数据库操作均会回滚,@GlobalTransactional注解会将seata申请的xid通过request传播下去,被调用的服务如果接入seata将会组成一个完整的分布式事务。

4.3.3 跨服务调用吃掉异常

1)如下代码所示,下单服务访问了自身应用的数据库,feign远程调用库存服务,但是catch了异常。

@RequestMapping("/orderByCredit")
@GlobalTransactional
public Boolean orderByCredit(String userId, @Nullable String requestId,@Nullable String requestId2,  int count) {
    //① 下单,并产生下单日志
    orderService.onlyOrder(userId, "product-1", requestId, count);
    //② 访问直连信用库,校验并扣减信用分
    creditService.checkHasCredit(userId, count);
    try {
        //③ 调用库存服务,扣减库存
        storageFeignClient.deductFlow("product-1", count, requestId2);
    } catch (Exception e){
        e.printStackTrace();
    }
}

2)使得接口请求,①、②步骤正常请求,③步骤出现内部数据库主键异常错误。查看结果,全局事务最终commit了,说明seata没有感知异常。

五、总结

  1. 在需要分布式事务的方法入口使用@GlobalTransactional即可实现分布式事务

  2. 多数据源不能在方法外层加@Transactional,这样会导致切换库异常;可直接使用@GlobalTransactional达到事务效果,因为本来这就是分布式事务场景。

  3. 全局事务最终决议是由全局事务入口应用(TM)决定的,即使下游事务节点发生了异常,只要TM没有catch到异常,就不会全局回滚。即决议是由TM发出的,并不是有些文档所说由TC决议的。

  4. 分析:tm决议简单快速,性能相对tc来说,tc压力被各个tm所分散

  5. 跨服务调用需要将全局事务id传递下去,seata已经封装了相关代码,需要依赖相关的包,如spring-cloud-starter-alibaba-seata


 ​作者更多文章

Redis系列:

Redis(一):单线程为何还能这么快

Redis(二):内存模型及回收算法

Redis(三):持久化

Redis(四):主从同步

ElasticSearch系列:

ElasticSearch:概述

ElasticSearch:核心

ElasticSearch:实战

ElasticSearch写入流程详解

RocketMQ系列:

RocketMQ—NameServer总结及核心源码剖析

RocketMQ—Producer(一)启动流程解密

Tomcat系列:

Tomcat:项目结构及架构分析

Tomcat:启动关闭流程分析

Tomcat:应用加载原理分析

其它:

字节跳动广告场景使用的“ClickHouse”介绍


关注IT巅峰技术,私信作者,获取以下2021全球架构师峰会PDF资料。

 

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值