分布式事务(1)-理论基础

分布式事务(2)---强一致性分布式事务解决方案

分布式事务(4)---最终一致性方案之TCC

前面介绍强一致性分布式解决方案,这里用Atomikos框架写一个实战的demo。模拟下单扣减库存的操作。

使用Atomikos,mybatis-plus框架搭建项目,springboot版本 2.3.2.RELEASE。

1.项目搭建

依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>com.baomidou</groupId>
  7. <artifactId>mybatis-plus-boot-starter</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>mysql</groupId>
  11. <artifactId>mysql-connector-java</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>org.springframework.boot</groupId>
  15. <artifactId>spring-boot-starter-jta-atomikos</artifactId>
  16. </dependency>

库存:

  1. import com.baomidou.mybatisplus.annotation.IdType;
  2. import com.baomidou.mybatisplus.annotation.TableId;
  3. import lombok.Data;
  4.  
  5. @Data
  6. public class Storage {
  7. @TableId(type= IdType.AUTO)
  8. private Integer id;
  9.  
  10. private Integer commodityId;
  11.  
  12. private Integer quantity;
  13.  
  14. }

订单:

  1. import com.baomidou.mybatisplus.annotation.IdType;
  2. import com.baomidou.mybatisplus.annotation.TableId;
  3. import com.baomidou.mybatisplus.annotation.TableName;
  4. import lombok.Data;
  5.  
  6. import java.math.BigDecimal;
  7.  
  8. @Data
  9. @TableName("t_order")
  10. public class Order {
  11.  
  12. @TableId(type= IdType.AUTO)
  13. private Integer id;
  14.  
  15. private String userId;
  16.  
  17. private Integer commodityId;
  18.  
  19. private Integer quantity;
  20.  
  21. private BigDecimal price;
  22.  
  23. private Integer status;
  24. }

初始化sql:需要建两个数据库,我这里建了一个njytest1和njytest2,让订单表和存库表在不同数据库生成初始化表数据。

  1. CREATE TABLE `storage` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `commodity_id` int(11) NOT NULL,
  4. `quantity` int(11) NOT NULL,
  5. PRIMARY KEY (`id`),
  6. UNIQUE KEY `idx_commodity_id` (`commodity_id`)
  7. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
  8. INSERT INTO `storage` (`id`, `commodity_id`, `quantity`) VALUES (1, 1, 10);
  9.  
  10. CREATE TABLE `t_order` (
  11. `id` int(11) NOT NULL AUTO_INCREMENT,
  12. `user_id` varchar(255) DEFAULT NULL,
  13. `commodity_id` int(11) NOT NULL,
  14. `quantity` int(11) DEFAULT 0,
  15. `price` decimal (10,2) DEFAULT NULL ,
  16. `status` int(11) DEFAULT NULL,
  17. PRIMARY KEY (`id`)
  18. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2.配置

数据库1配置类,用于接收数据源1的配置:

  1. import lombok.Data;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import org.springframework.stereotype.Component;
  4.  
  5. /**
  6. * Description:
  7. * Created by nijunyang on 2021/12/2 23:57
  8. */
  9. @Data
  10. @ConfigurationProperties(prefix = "mysql.datasource1")
  11. @Component
  12. public class DBConfig1 {
  13. private String url;
  14. private String username;
  15. private String password;
  16. private int minPoolSize;
  17. private int maxPoolSize;
  18. private int maxLifetime;
  19. private int borrowConnectionTimeout;
  20. private int loginTimeout;
  21. private int maintenanceInterval;
  22. private int maxIdleTime;
  23. private String testQuery;
  24. }

数据库2的配置类,用于接收数据源2的配置:

  1. import lombok.Data;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import org.springframework.stereotype.Component;
  4.  
  5. /**
  6. * Description:
  7. * Created by nijunyang on 2021/12/3 0:00
  8. */
  9. @Data
  10. @ConfigurationProperties(prefix = "mysql.datasource2")
  11. @Component
  12. public class DBConfig2 {
  13. private String url;
  14. private String username;
  15. private String password;
  16. private int minPoolSize;
  17. private int maxPoolSize;
  18. private int maxLifetime;
  19. private int borrowConnectionTimeout;
  20. private int loginTimeout;
  21. private int maintenanceInterval;
  22. private int maxIdleTime;
  23. private String testQuery;
  24. }

application.yml配置文件中对应的两个数据源配置:

  1. mysql:
  2. datasource1:
  3. url: jdbc:mysql://localhost:3306/njytest1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  4. username: root
  5. password: root
  6. minPoolsize: 3
  7. maxPoolSize: 25
  8. maxLifetime: 30000
  9. borrowConnectionTimeout: 30
  10. loginTimeout: 30
  11. maintenanceInterval: 60
  12. maxIdleTime: 60
  13. testQuery: SELECT 1
  14.  
  15. datasource2:
  16. url: jdbc:mysql://localhost:3306/njytest2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  17. username: root
  18. password: root
  19. minPoolsize: 3
  20. maxPoolSize: 25
  21. maxLifetime: 30000
  22. borrowConnectionTimeout: 30
  23. loginTimeout: 30
  24. maintenanceInterval: 60
  25. maxIdleTime: 60
  26. testQuery: SELECT 1
  27. logging:
  28. level:
  29. com.nijunyang.tx.xa.mapper1: debug
  30. com.nijunyang.tx.xa.mapper2: debug

我们需要将我们的mapper放到两个不同的包下面,才能给两个mapper配置不同的数据源。

  1. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  2. import com.nijunyang.tx.common.entity.Order;
  3. import org.springframework.stereotype.Repository;
  4.  
  5. /**
  6. * Description:
  7. * Created by nijunyang on 2021/12/3 0:09
  8. */
  9. @Repository
  10. public interface OrderMapper extends BaseMapper<Order> {
  11.  
  12. }
  1. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  2. import com.nijunyang.tx.common.entity.Storage;
  3. import org.apache.ibatis.annotations.Update;
  4. import org.springframework.stereotype.Repository;
  5.  
  6. /**
  7. * Description:
  8. * Created by nijunyang on 2021/12/3 0:10
  9. */
  10. @Repository
  11. public interface StorageMapper extends BaseMapper<Storage> {
  12.  
  13. @Update("UPDATE storage SET quantity = quantity - #{quantity} WHERE commodity_id = #{commodityId} and quantity >= #{quantity}")
  14. void reduce(Integer commodityId, Integer quantity);
  15. }

分别配置两个数据源的mybatis配置:

  1. MyBatisConfig1 制定使用com.nijunyang.tx.xa.mapper1包, 并且指定其sqlSessionTemplate 为名为orderSqlSessionTemplatebean
  1. MyBatisConfig2 制定使用com.nijunyang.tx.xa.mapper2包, 并且指定其sqlSessionTemplate 为名为storageSqlSessionTemplatebean
  1. 也就是这两个配置:
    @MapperScan(basePackages = "com.nijunyang.tx.xa.mapper1", sqlSessionTemplateRef = "orderSqlSessionTemplate")
  1. @MapperScan(basePackages = "com.nijunyang.tx.xa.mapper2", sqlSessionTemplateRef = "storageSqlSessionTemplate")

  1. import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
  2. import com.mysql.cj.jdbc.MysqlXADataSource;
  3. import org.apache.ibatis.session.SqlSessionFactory;
  4. import org.mybatis.spring.SqlSessionTemplate;
  5. import org.mybatis.spring.annotation.MapperScan;
  6. import org.springframework.beans.factory.annotation.Qualifier;
  7. import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
  8. import org.springframework.context.annotation.Bean;
  9. import org.springframework.context.annotation.Configuration;
  10. import org.springframework.context.annotation.Primary;
  11.  
  12. import javax.sql.DataSource;
  13. import java.sql.SQLException;
  14.  
  15. /**
  16. * Description:
  17. * Created by nijunyang on 2021/12/3 0:11
  18. */
  19. @Configuration
  20. /**
  21. * 制定此mapper使用哪个sqlSessionTemplate
  22. */
  23. @MapperScan(basePackages = "com.nijunyang.tx.xa.mapper1", sqlSessionTemplateRef = "orderSqlSessionTemplate")
  24. public class MyBatisConfig1 {
  25.  
  26. //配置XA数据源
  27. @Primary
  28. @Bean(name = "orderDataSource")
  29. public DataSource orderDataSource(DBConfig1 dbConfig1) throws SQLException {
  30. MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
  31. mysqlXaDataSource.setUrl(dbConfig1.getUrl());
  32. mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
  33. mysqlXaDataSource.setPassword(dbConfig1.getPassword());
  34. mysqlXaDataSource.setUser(dbConfig1.getUsername());
  35. mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
  36.  
  37. AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
  38. xaDataSource.setXaDataSource(mysqlXaDataSource);
  39. xaDataSource.setUniqueResourceName("orderDataSource");
  40.  
  41. xaDataSource.setMinPoolSize(dbConfig1.getMinPoolSize());
  42. xaDataSource.setMaxPoolSize(dbConfig1.getMaxPoolSize());
  43. xaDataSource.setMaxLifetime(dbConfig1.getMaxLifetime());
  44. xaDataSource.setBorrowConnectionTimeout(dbConfig1.getBorrowConnectionTimeout());
  45. xaDataSource.setLoginTimeout(dbConfig1.getLoginTimeout());
  46. xaDataSource.setMaintenanceInterval(dbConfig1.getMaintenanceInterval());
  47. xaDataSource.setMaxIdleTime(dbConfig1.getMaxIdleTime());
  48. xaDataSource.setTestQuery(dbConfig1.getTestQuery());
  49. return xaDataSource;
  50. }
  51.  
  52. @Primary
  53. @Bean(name = "orderSqlSessionFactory")
  54. public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource) throws Exception {
  55. // SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
  56. // bean.setDataSource(dataSource);
  57. // 这里用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否则 MyBatisPlus 不会生效
  58. MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
  59. bean.setDataSource(dataSource);
  60. return bean.getObject();
  61. }
  62.  
  63. @Primary
  64. @Bean(name = "orderSqlSessionTemplate")
  65. public SqlSessionTemplate orderSqlSessionTemplate(
  66. @Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
  67. SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
  68. return sqlSessionTemplate;
  69. }
  1. import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
  2. import com.mysql.cj.jdbc.MysqlXADataSource;
  3. import org.apache.ibatis.session.SqlSessionFactory;
  4. import org.mybatis.spring.SqlSessionTemplate;
  5. import org.mybatis.spring.annotation.MapperScan;
  6. import org.springframework.beans.factory.annotation.Qualifier;
  7. import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
  8. import org.springframework.context.annotation.Bean;
  9. import org.springframework.context.annotation.Configuration;
  10.  
  11. import javax.sql.DataSource;
  12. import java.sql.SQLException;
  13.  
  14. /**
  15. * Description:
  16. * Created by nijunyang on 2021/12/3 0:16
  17. */
  18. @Configuration
  19. /**
  20. * 制定此mapper使用哪个sqlSessionTemplate
  21. */
  22. @MapperScan(basePackages = "com.nijunyang.tx.xa.mapper2", sqlSessionTemplateRef = "storageSqlSessionTemplate")
  23. public class MyBatisConfig2 {
  24.  
  25. //配置XA数据源
  26. @Bean(name = "storageDataSource")
  27. public DataSource storageDataSource(DBConfig2 dbConfig2) throws SQLException {
  28. MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
  29. mysqlXaDataSource.setUrl(dbConfig2.getUrl());
  30. mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
  31. mysqlXaDataSource.setPassword(dbConfig2.getPassword());
  32. mysqlXaDataSource.setUser(dbConfig2.getUsername());
  33. mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
  34.  
  35. AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
  36. xaDataSource.setXaDataSource(mysqlXaDataSource);
  37. xaDataSource.setUniqueResourceName("storageDataSource");
  38.  
  39. xaDataSource.setMinPoolSize(dbConfig2.getMinPoolSize());
  40. xaDataSource.setMaxPoolSize(dbConfig2.getMaxPoolSize());
  41. xaDataSource.setMaxLifetime(dbConfig2.getMaxLifetime());
  42. xaDataSource.setBorrowConnectionTimeout(dbConfig2.getBorrowConnectionTimeout());
  43. xaDataSource.setLoginTimeout(dbConfig2.getLoginTimeout());
  44. xaDataSource.setMaintenanceInterval(dbConfig2.getMaintenanceInterval());
  45. xaDataSource.setMaxIdleTime(dbConfig2.getMaxIdleTime());
  46. xaDataSource.setTestQuery(dbConfig2.getTestQuery());
  47. return xaDataSource;
  48. }
  49.  
  50. @Bean(name = "storageSqlSessionFactory")
  51. public SqlSessionFactory storageSqlSessionFactory(@Qualifier("storageDataSource") DataSource dataSource) throws Exception {
  52. // SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
  53. // bean.setDataSource(dataSource);
  54. // 这里用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否则 MyBatisPlus 不会生效
  55. MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
  56. bean.setDataSource(dataSource);
  57. return bean.getObject();
  58. }
  59.  
  60. @Bean(name = "storageSqlSessionTemplate")
  61. public SqlSessionTemplate storageSqlSessionTemplate(
  62. @Qualifier("storageSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
  63. SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
  64. return sqlSessionTemplate;
  65. }
  66. }

因为我们要使用自己的数据源,所以启动类需要剔除数据源的自动配置

 3.业务代码

  1. import com.nijunyang.tx.common.entity.Order;
  2. import com.nijunyang.tx.xa.service.OrderService;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.RequestMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6.  
  7. import javax.annotation.Resource;
  8.  
  9. /**
  10. * Description:
  11. * Created by nijunyang on 2021/12/3 0:20
  12. */
  13. @RestController
  14. @RequestMapping("order")
  15. public class OrderController {
  16.  
  17. @Resource
  18. private OrderService orderService;
  19.  
  20. //127.0.0.1:8080/order?userId=1&commodityId=1&quantity=2&price=10
  21. @GetMapping
  22. public Object create(Order order) {
  23. orderService.create(order);
  24. return 1;
  25. }
  26.  
  27. }
  1. import com.nijunyang.tx.common.entity.Order;
  2. import com.nijunyang.tx.xa.mapper1.OrderMapper;
  3. import com.nijunyang.tx.xa.mapper2.StorageMapper;
  4. import org.springframework.stereotype.Service;
  5. import org.springframework.transaction.annotation.Transactional;
  6.  
  7. import javax.annotation.Resource;
  8.  
  9. /**
  10. * Description:
  11. * Created by nijunyang on 2021/12/3 0:21
  12. */
  13. @Service
  14. public class OrderService {
  15.  
  16. @Resource
  17. private OrderMapper orderMapper;
  18. @Resource
  19. private StorageMapper storageMapper;
  20.  
  21. @Transactional(rollbackFor = Exception.class)
  22. public void create(Order order) {
  23. orderMapper.insert(order);
  24. storageMapper.reduce(order.getCommodityId(), order.getQuantity());
  25. // int a = 1/0;
  26. }
  27. }

至此项目搭建完毕,访问  127.0.0.1:8080/order?userId=1&commodityId=1&quantity=2&price=10,可以发现两个数据库的t_order表和 storage数据正常写入。

当我在业务层构造一个异常 int a = 1/0时,会发现两个库均不会写入数据。

实际上通过@Transactional注解拿到的是这个事务管理器org.springframework.transaction.jta.JtaTransactionManager#doBegin,最终开启事务是由com.atomikos.icatch.jta.UserTransactionManager#begin来开启事务,这个就是Atomikos提供的事务管理器;

发生异常回滚也是com.atomikos.icatch.jta.UserTransactionManager#rollback,最终com.atomikos.icatch.imp.TransactionStateHandler#rollback会将所有的事务都回滚。

分布式事务(3)---强一致性分布式事务Atomikos实战的更多相关文章

  1. 分布式事务 XA 两段式事务 X/open CAP BASE 一次分清

    分布式事务: 分布式事务是处理多节点上 的数据保持 类似传统 ACID 事物特性的 一种事物. XA:是一种协议,一种分布式事务的协议,核心思想是2段式提交. 1 准备阶段  2 提交阶段.XA协议是 ...

  2. 谈谈分布式事务之三: System.Transactions事务详解[下篇]

    在前面一篇给出的Transaction的定义中,信息的读者应该看到了一个叫做DepedentClone的方法.该方法对用于创建基于现有Transaction对 象的“依赖事务(DependentTra ...

  3. 谈谈分布式事务之三: System.Transactions事务详解[上篇]

    在.NET 1.x中,我们基本是通过ADO.NET实现对不同数据库访问的事务..NET 2.0为了带来了全新的事务编程模式,由于所有事务组件或者类型均定义在System.Transactions程序集 ...

  4. Sql Server 中如果使用TransactionScope开启一个分布式事务,使用该事务两个并发的连接会互相死锁吗

    提问: 如果使用TransactionScope开启一个分布式事务,使用该事务两个并发的连接会互相死锁吗? 如果在.Net中用TransactionScope开启一个事务. 然后在该事务范围内启动两个 ...

  5. 搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务

    搞懂分布式技术19:使用RocketMQ事务消息解决分布式事务 初步认识RocketMQ的核心模块 rocketmq模块 rocketmq-broker:接受生产者发来的消息并存储(通过调用rocke ...

  6. Replication--无法将事务提升为分布式事务,因为在事务中有活动的保存点

    场景描述在SQL SERVER 2012上创建事务发布,发布库已搭建为可AWAYSON,分发服务器和发布服务器分离,创建发布时提示“无法将事务提升为分布式事务,因为在事务中有活动的保存点” 解决方法E ...

  7. WCF分布式开发步步为赢(12):WCF事务机制(Transaction)和分布式事务编程

    今天我们继续学习WCF分布式开发步步为赢系列的12节:WCF事务机制(Transaction)和分布式事务编程.众所周知,应用系统开发过程中,事务是一个重要的概念.它是保证数据与服务可靠性的重要机制. ...

  8. redis事务机制和分布式锁

    Redis事务机制 严格意义来讲,Redis的事务和我们理解的传统数据库(如mysql)的事务是不一样的:Redis的事务实质上是命令的集合,在一个事务中要么所有命令都被执行,要么所有事物都不执行.  ...

  9. Redis事务与可分布式锁

    1    Redis事务 1.1   Redis事务介绍 l  Redis的事务是通过MULTI,EXEC,DISCARD和WATCH这四个命令来完成的. l  Redis的单个命令都是原子性的,所以 ...

随机推荐

  1. Linux C语言链表详细分析

    链表是一种常见的基础数据结构,结构体指针在这里得到了充分的利用.链表可以动态的进行存储分配,也就是说,链表是一个功能极为强大的数组,他可以在节点中定义多种数据类型,还可以根据需要随意增添,删除,插入节 ...

  2. 转帖:新版vivado2019.2新增增量综合功能

    从 Vivado 2019.1 版本开始,Vivado 综合引擎就已经可以支持增量流程了.这使用户能够在设计变化较小时减少总的综合运行时间. Vivado IDE 和 Tcl 命令批处理模式都可以启用 ...

  3. 平衡二叉树检查 牛客网 程序员面试金典 C++ Python

    平衡二叉树检查 牛客网 程序员面试金典 C++ Python 题目描述 实现一个函数,检查二叉树是否平衡,平衡的定义如下,对于树中的任意一个结点,其两颗子树的高度差不超过1. 给定指向树根结点的指针T ...

  4. 部署自己的gitlab服务器

    一.安装依赖环境,下载gitlab的rpm包,并且安装 yum install curl policycoreutils-python openssh-server postfix wget -ywg ...

  5. request/response解决中文乱码!!!

    Request中文乱码问题以及解决方案 补充三个知识点: Get是URL解码方式.默认解码格式是Tomcat编码格式.所以URL解码是UTF-8,覆盖掉了request容器解码格式 Post是实体内容 ...

  6. webpack 打包样式资源

    webpack 打包样式资源 webpack.config.js配置文件内容为: // 用来拼接绝对路径的方法 const {resolve} = require('path') module.exp ...

  7. java语言方法中定义final类型的入参有什么用意?

    无论参数是基本数据类型,还是引用数据类型,只要加了final,不好意思,该参数不可以再赋值(实参传进来给形参,就相当于初始化完成).可以防止在方法里面不小心重新赋值,造成一些不必要的麻烦!!!参考:h ...

  8. c++学习笔记1(引用)

    引用 格式:类型名&引用名=某变量名: 概念 实例:编写交换整型变量的函数对比 不用引用 实机操作 使用引用 实机操作 实例2:用作函数的返回值 可对函数返回值赋值 常引用 使用格式,在引用前 ...

  9. QuantumTunnel:v1.0.0 正式版本发布

    经过一段时间运行,代码已经稳定是时候发布正式版本了! v1.0.0 正式版本发布 对核心能力的简要说明: 支持协议路由和端口路由:QuantumTunnel:端口路由 vs 协议路由 基于Netty实 ...

  10. 开源一个由.netcore/.net framework4.6开发的saas微商城+独立部署版本微小程序商城

    一.项目介绍 开源一款基于.NET4.6开发的一款完整的微信商城SAAS平台,前端支持小程序.h5,由前端商城,商户管理后台,平台管理后台三大块组成,sass功能完善,支持商户拖拽式零代码创建并提交上 ...