0%

分布式事务解决方案(多数据源造成的分布式事务)

1
2
3
4
5
6
作者: 夜泊1990
企鹅: 1611756908
鹅群: 948233848
邮箱: hd1611756908@163.com
博客: https://hd1611756908.github.io/
B 站: https://space.bilibili.com/514155929/

一、分布式事务产生的原因(多数据源)


二、分布式解决方案

2.1 什么是JTA

  • JTA介绍

    1
    JTA(Java Transaction API)被称为Java事务API,是由Java语言提供的一套解决分布式事务的API标准
  • 分布式事务(JTA)

    1
    一个分布式事务包括一个事务管理器和一个或多个资源(数据库)

2.2 XA协议

1
2
3
1. XA协议是JTA的基础
2. XA协议最早由Tuxedo首先提出,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准
3. XA协议采用两阶段提交方式来管理分布式事务,MySQL从5.x版本开始支XA协议

2.3 二阶段提交(2PC)

  • 什么是二阶段提交
    1
    二阶段提交顾名思义就是为保证分布式事务的正确运行,在进行事务管理的时候,采用分阶段提交的设计方案,而这里是分两个阶段提交,所以叫二阶段提交
  • 二阶段提交的流程

举一个下单的例子(订单服务A中插入一条订单数据,库存服务B中更新库存数据)

第一阶段

1
2
3
4
5
6
7
8
9
10
11
第一阶段步骤:
1. 事务询问
--> 事务管理器向所有事务参与者发送事务预处理请求,然后开始等待参与者的响应
2. 执行本地事务
--> 各个参与者执行数据库本地事务,但在执行完成后并不会真正提交数据库本地事务,而是先向事务管理器报告状态(YES/NO)
3. 反馈给事务管理器信息
--> 各个参与者执行本地事务(告诉事务管理者 YES/NO),此时还没有真正提交事务.

第一阶段完成之后会有两种结果
1. 所有的资源都返回给事务管理器 YES
2. 有一个或多个资源向事务管理器 NO

第二阶段(第二阶段分两种情况一种是成功事务完成,一种是失败,事务回滚)

  • 提交执行事务(成功)
1
2
3
第二阶段成功分为两步
1. 事务管理器向所有的资源发布提交的请求(在一阶段返回给事务管理器的状态是YES)
2. 资源收到提交的请求之后,正式执行本地事务的Commit操作,并且在完成之后释放这个事务占用的资源
  • 提交执行事务(失败/异常)
1
2
3
二阶段失败分为两步
1. 事务管理器向所有资源发出回滚请求
2. 本地事务回滚

  • 二阶段提交的优缺点
    1
    2
    3
    4
    优点: 简单方便
    缺点:
    1. 在进行事务操作时,会将所有资源锁定,直到事务结束,所以性能较差
    2. 如果资源或者事务管理器宕机或者失去联系会出现阻塞或者数据不一致问题

三、Atomikos事务管理器

3.1 Atomikos简介

1
2
Atomikos是一款Java/JTA事务处理工具,在SpringBoot的官网文档的分布式事务处理中Atomikos作为一种分布式事务的解决方案.
文档地址: https://docs.spring.io/spring-boot/docs/2.1.18.RELEASE/reference/html/boot-features-jta.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
从SpringBoot的官网可以看出,官网介绍非常简单,说了要想使用需要添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

然后就没介绍一些更多细节,不过Atomikos相对来说比较简单,只需要一个核心API即可

核心API介绍
1. AtomikosDataSourceBean
1.1 这个类是Atomikos连接池的首选类
1.2 如果要开启Atomikos JTA首选就是实例化此类
1.3 接下来要做的就是实例化此类的对象,并设置对象的属性
1.4 创建好此类对象后它会自动的加入到事务管理中
1.5 所有通过此类获取的连接执行的SQL都会加入到JTA事务管理

3.2 环境构建

  • 业务描述

    1
    2
    3
    4
    5
    # 简单测试业务,非真实业务逻辑
    例如用户购买商品的业务逻辑,整个业务逻辑涉及到2张表

    商品服务:对下单的商品扣除商品库存
    订单服务:根据购买需求创建订单
  • 测试数据库创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    -- 创建订单库
    CREATE DATABASE order_db DEFAULT CHARACTER SET UTF8;

    -- 订单表
    CREATE TABLE `order`(
    order_id INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
    product_id INT(11) DEFAULT NULL COMMENT '商品ID'
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT='订单表';


    -- 创建商品库
    CREATE DATABASE product_db DEFAULT CHARACTER SET UTF8;

    -- 商品表
    CREATE TABLE product(
    product_id INT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID',
    price DOUBLE DEFAULT NULL COMMENT '商品价格',
    stock INT(11) DEFAULT NULL COMMENT '库存'
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT='商品表';
  • 映射数据库表的实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /**
    * 订单类
    * @Author 夜泊1990
    * @Web https://ukoko.gitee.io/
    * @Date 2021年4月23日
    * @Email hd1611756908@163.com
    */
    public class Order {
    //订单ID
    private Integer orderId;
    //商品ID
    private Integer productId;
    }
    <-------------------------------------分割线----------------------------------->
    /**
    * 产品类
    * @Author 夜泊1990
    * @Web https://ukoko.gitee.io/
    * @Date 2021年4月23日
    * @Email hd1611756908@163.com
    */
    public class Product {
    //商品ID
    private Integer productId ;
    //商品价格
    private Double price;
    //商品库存
    private Integer stock;
    }
  • 业务代码Mapper层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    /**
    * 操作订单表的接口
    * @Author 夜泊1990
    * @Web https://ukoko.gitee.io/
    * @Date 2021年4月23日
    * @Email hd1611756908@163.com
    */
    public interface OrderMapper {
    /**
    * 创建订单
    * @param productId: 商品ID
    */
    void createOrder(Integer productId);
    }
    <-------------------------------------分割线----------------------------------->
    /**
    * 操作商品的表的接口
    * @Author 夜泊1990
    * @Web https://ukoko.gitee.io/
    * @Date 2021年4月23日
    * @Email hd1611756908@163.com
    */
    public interface ProductMapper {
    /**
    * 更新商品库存
    * @param productId: 商品ID
    * @param count : 添加或者减少的商品数量
    */
    void updateStock(@Param("productId") Integer productId,@Param("count")Integer count);
    }
  • 注册接口的xml配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="cn.ukoko.multiple.mapper.order.OrderMapper">
    <!--
    创建订单的SQL
    -->
    <insert id="createOrder" parameterType="int">
    INSERT INTO `order`(product_id) VALUES(#{productId})
    </insert>
    </mapper>

    <-------------------------------------分割线----------------------------------->

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="cn.ukoko.multiple.mapper.product.ProductMapper">
    <!--
    更新商品库存的SQL
    -->
    <update id="updateStock">
    UPDATE product SET stock=stock-#{count} WHERE product_id=#{productId}
    </update>
    </mapper>
  • 业务代码Service层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    /**
    * @Author 夜泊1990
    * @Web https://ukoko.gitee.io/
    * @Date 2021年4月23日
    * @Email hd1611756908@163.com
    */
    public interface BusinessService {
    /**
    * 下单
    * @param productId: 商品ID
    * @param count : 下单商品数量
    */
    void PlaceOrder(Integer productId,Integer count);
    }
    <-------------------------------------分割线----------------------------------->

    /**
    * @Author 夜泊1990
    * @Web https://ukoko.gitee.io/
    * @Date 2021年4月23日
    * @Email hd1611756908@163.com
    */
    @Service
    public class BusinessServiceImpl implements BusinessService{

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private ProductMapper productMapper;

    @Transactional
    @Override
    public void PlaceOrder(Integer productId, Integer count) {
    orderMapper.createOrder(productId);
    productMapper.updateStock(productId,count);
    }
    }
  • 多数据库配置(application.properties)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 数据源配置01
    spring.datasource.first.url=jdbc:mysql://192.168.1.134:3307/order_db?characterEncoding=utf-8&serverTimezone=GMT%2B8
    spring.datasource.first.username=root
    spring.datasource.first.password=root
    spring.datasource.first.driverClassName=com.mysql.cj.jdbc.Driver
    # 数据源配置02
    spring.datasource.second.url=jdbc:mysql://192.168.1.134:3308/product_db?characterEncoding=utf-8&serverTimezone=GMT%2B8
    spring.datasource.second.username=root
    spring.datasource.second.password=root
    spring.datasource.second.driverClassName=com.mysql.cj.jdbc.Driver
  • 多数据源配置(配置文件编写)

    • DataSourceConfig1.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      /**
      * 配置数据源product_db
      * @Author 夜泊1990
      * @Web https://ukoko.gitee.io/
      * @Date 2021年4月23日
      * @Email hd1611756908@163.com
      */
      @Configuration
      @MapperScan(basePackages = {"cn.ukoko.multiple.mapper.product"},sqlSessionTemplateRef = "productSqlSessionTemplate")
      public class DataSource1 {

      @Value("${spring.datasource.first.url}")
      private String url;
      @Value("${spring.datasource.first.username}")
      private String username;
      @Value("${spring.datasource.first.password}")
      private String password;
      @Value("${spring.datasource.first.driverClassName}")
      private String driverClassName;

      /**
      * 创建product_db数据源
      */
      @Bean(name = "productDataSource")
      @Primary
      public DataSource createProductDataSource(){
      AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
      DruidXADataSource dds = new DruidXADataSource();
      dds.setUsername(username);
      dds.setPassword(password);
      dds.setUrl(url);
      dds.setDriverClassName(driverClassName);
      ds.setXaDataSource(dds);
      ds.setUniqueResourceName("productDataSource");
      return ds;
      }

      @Primary
      @Bean(name = "productSqlSessionFactory")
      public SqlSessionFactory orderSqlSessionFactory(@Qualifier(value = "productDataSource") DataSource dataSource) throws Exception {
      SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
      //设置数据源
      sqlSessionFactoryBean.setDataSource(dataSource);
      //设置别名
      sqlSessionFactoryBean.setTypeAliasesPackage("cn.ukoko.multiple.entity");
      //设置驼峰
      org.apache.ibatis.session.Configuration c = new org.apache.ibatis.session.Configuration();
      c.setMapUnderscoreToCamelCase(true);
      sqlSessionFactoryBean.setConfiguration(c);
      //设置映射接口的xml配置文件
      sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
      return sqlSessionFactoryBean.getObject();
      }

      /**
      * 创建SqlSessionTemplate
      */
      @Primary
      @Bean(name = "productSqlSessionTemplate")
      public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("productSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
      return new SqlSessionTemplate(sqlSessionFactory);
      }
      }
    • DataSourceConfig2.java
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      /**
      * 配置数据源product_db
      * @Author 夜泊1990
      * @Web https://ukoko.gitee.io/
      * @Date 2021年4月23日
      * @Email hd1611756908@163.com
      */
      @Configuration
      @MapperScan(basePackages = {"cn.ukoko.multiple.mapper.order"},sqlSessionTemplateRef = "orderSqlSessionTemplate")
      public class DataSource2 {

      @Value("${spring.datasource.second.url}")
      private String url;
      @Value("${spring.datasource.second.username}")
      private String username;
      @Value("${spring.datasource.second.password}")
      private String password;
      @Value("${spring.datasource.second.driverClassName}")
      private String driverClassName;

      /**
      * 创建product_db数据源
      */
      @Bean(name = "orderDataSource")
      public DataSource createProductDataSource(){
      AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
      DruidXADataSource dds = new DruidXADataSource();
      dds.setUsername(username);
      dds.setPassword(password);
      dds.setUrl(url);
      dds.setDriverClassName(driverClassName);
      ds.setXaDataSource(dds);
      ds.setUniqueResourceName("orderDataSource");
      return ds;
      }

      @Bean(name = "orderSqlSessionFactory")
      public SqlSessionFactory productSqlSessionFactory(@Qualifier(value = "orderDataSource") DataSource dataSource) throws Exception {
      SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
      //设置数据源
      sqlSessionFactoryBean.setDataSource(dataSource);
      //设置别名
      sqlSessionFactoryBean.setTypeAliasesPackage("cn.ukoko.multiple.entity");
      //设置驼峰
      org.apache.ibatis.session.Configuration c = new org.apache.ibatis.session.Configuration();
      c.setMapUnderscoreToCamelCase(true);
      sqlSessionFactoryBean.setConfiguration(c);
      //设置映射接口的xml配置文件
      sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
      return sqlSessionFactoryBean.getObject();
      }

      /**
      * 创建SqlSessionTemplate
      */
      @Bean(name = "orderSqlSessionTemplate")
      public SqlSessionTemplate productSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
      return new SqlSessionTemplate(sqlSessionFactory);
      }
      }

      测试完成

  • 测试用例地址

    1
    https://gitee.com/ukoko/ukoko-multiple.git
----------------本文结束感谢您的阅读---------------