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

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

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

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

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

1.项目搭建

依赖:

        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

库存:

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data; @Data
public class Storage {
@TableId(type= IdType.AUTO)
private Integer id; private Integer commodityId; private Integer quantity; }

订单:

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import java.math.BigDecimal; @Data
@TableName("t_order")
public class Order { @TableId(type= IdType.AUTO)
private Integer id; private String userId; private Integer commodityId; private Integer quantity; private BigDecimal price; private Integer status;
}

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

CREATE TABLE `storage` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`commodity_id` int(11) NOT NULL,
`quantity` int(11) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_commodity_id` (`commodity_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
INSERT INTO `storage` (`id`, `commodity_id`, `quantity`) VALUES (1, 1, 10); CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`commodity_id` int(11) NOT NULL,
`quantity` int(11) DEFAULT 0,
`price` decimal (10,2) DEFAULT NULL ,
`status` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2.配置

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

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; /**
* Description:
* Created by nijunyang on 2021/12/2 23:57
*/
@Data
@ConfigurationProperties(prefix = "mysql.datasource1")
@Component
public class DBConfig1 {
private String url;
private String username;
private String password;
private int minPoolSize;
private int maxPoolSize;
private int maxLifetime;
private int borrowConnectionTimeout;
private int loginTimeout;
private int maintenanceInterval;
private int maxIdleTime;
private String testQuery;
}

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

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; /**
* Description:
* Created by nijunyang on 2021/12/3 0:00
*/
@Data
@ConfigurationProperties(prefix = "mysql.datasource2")
@Component
public class DBConfig2 {
private String url;
private String username;
private String password;
private int minPoolSize;
private int maxPoolSize;
private int maxLifetime;
private int borrowConnectionTimeout;
private int loginTimeout;
private int maintenanceInterval;
private int maxIdleTime;
private String testQuery;
}

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

mysql:
datasource1:
url: jdbc:mysql://localhost:3306/njytest1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
minPoolsize: 3
maxPoolSize: 25
maxLifetime: 30000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
testQuery: SELECT 1 datasource2:
url: jdbc:mysql://localhost:3306/njytest2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
minPoolsize: 3
maxPoolSize: 25
maxLifetime: 30000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
testQuery: SELECT 1
logging:
level:
com.nijunyang.tx.xa.mapper1: debug
com.nijunyang.tx.xa.mapper2: debug

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

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

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

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

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import javax.sql.DataSource;
import java.sql.SQLException; /**
* Description:
* Created by nijunyang on 2021/12/3 0:11
*/
@Configuration
/**
* 制定此mapper使用哪个sqlSessionTemplate
*/
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper1", sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class MyBatisConfig1 { //配置XA数据源
@Primary
@Bean(name = "orderDataSource")
public DataSource orderDataSource(DBConfig1 dbConfig1) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(dbConfig1.getUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(dbConfig1.getPassword());
mysqlXaDataSource.setUser(dbConfig1.getUsername());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("orderDataSource"); xaDataSource.setMinPoolSize(dbConfig1.getMinPoolSize());
xaDataSource.setMaxPoolSize(dbConfig1.getMaxPoolSize());
xaDataSource.setMaxLifetime(dbConfig1.getMaxLifetime());
xaDataSource.setBorrowConnectionTimeout(dbConfig1.getBorrowConnectionTimeout());
xaDataSource.setLoginTimeout(dbConfig1.getLoginTimeout());
xaDataSource.setMaintenanceInterval(dbConfig1.getMaintenanceInterval());
xaDataSource.setMaxIdleTime(dbConfig1.getMaxIdleTime());
xaDataSource.setTestQuery(dbConfig1.getTestQuery());
return xaDataSource;
} @Primary
@Bean(name = "orderSqlSessionFactory")
public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource) throws Exception {
// SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
// bean.setDataSource(dataSource);
// 这里用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否则 MyBatisPlus 不会生效
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
} @Primary
@Bean(name = "orderSqlSessionTemplate")
public SqlSessionTemplate orderSqlSessionTemplate(
@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
return sqlSessionTemplate;
}
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import javax.sql.DataSource;
import java.sql.SQLException; /**
* Description:
* Created by nijunyang on 2021/12/3 0:16
*/
@Configuration
/**
* 制定此mapper使用哪个sqlSessionTemplate
*/
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper2", sqlSessionTemplateRef = "storageSqlSessionTemplate")
public class MyBatisConfig2 { //配置XA数据源
@Bean(name = "storageDataSource")
public DataSource storageDataSource(DBConfig2 dbConfig2) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(dbConfig2.getUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(dbConfig2.getPassword());
mysqlXaDataSource.setUser(dbConfig2.getUsername());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("storageDataSource"); xaDataSource.setMinPoolSize(dbConfig2.getMinPoolSize());
xaDataSource.setMaxPoolSize(dbConfig2.getMaxPoolSize());
xaDataSource.setMaxLifetime(dbConfig2.getMaxLifetime());
xaDataSource.setBorrowConnectionTimeout(dbConfig2.getBorrowConnectionTimeout());
xaDataSource.setLoginTimeout(dbConfig2.getLoginTimeout());
xaDataSource.setMaintenanceInterval(dbConfig2.getMaintenanceInterval());
xaDataSource.setMaxIdleTime(dbConfig2.getMaxIdleTime());
xaDataSource.setTestQuery(dbConfig2.getTestQuery());
return xaDataSource;
} @Bean(name = "storageSqlSessionFactory")
public SqlSessionFactory storageSqlSessionFactory(@Qualifier("storageDataSource") DataSource dataSource) throws Exception {
// SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
// bean.setDataSource(dataSource);
// 这里用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否则 MyBatisPlus 不会生效
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
} @Bean(name = "storageSqlSessionTemplate")
public SqlSessionTemplate storageSqlSessionTemplate(
@Qualifier("storageSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
return sqlSessionTemplate;
}
}

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

 3.业务代码

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

至此项目搭建完毕,访问  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. cf13B Letter A(分类+简单计算几何,,)

    题意: 给三个线段(每个线段的两个端点的坐标),问这三个线段能否组成字母A. 组成字母A的条件: 1.两个线段有公共端点. 2.这两个线段夹角小于等于90度. 3.第三个线段的两个端点分别在这两个线段 ...

  2. 力扣 - 剑指 Offer 66. 构建乘积数组

    题目 剑指 Offer 66. 构建乘积数组 思路1 按照一般的思路就是将所有的相乘,然后除以每一位数字就是答案,但是题目要求我们不能使用除法,因此我们会想到每次遍历到每个数字的时候,在遍历一遍数组, ...

  3. DDTP 分布式数据传输协议白皮书

    声明 本文非本人原创,主要参考文献[1]编写的阅读笔记.本博客仅发表在博客园,作者LightningStar,其他平台均为转载. 摘要 本白皮书对全球现有主要个人信息可携带权的实践模式进行梳理,分析其 ...

  4. Spring一套全通4—持久层整合

    百知教育 - Spring系列课程 - 持久层整合 第一章.持久层整合 1.Spring框架为什么要与持久层技术进行整合 1. JavaEE开发需要持久层进行数据库的访问操作. 2. JDBC Hib ...

  5. k8s入坑之路(13)kubernetes重要资源(namespace隔离 resources资源管理 label)

    Namespace --- 集群的共享与隔离 语言中namespace概念 namespace核心作用隔离 以上是隔离的代码.namespace隔离的是: 1.资源对象的隔离:Service.Depl ...

  6. mongodb安装教程(一)

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/fengtingYan/article/de ...

  7. BAT面试必问细节:关于Netty中的ByteBuf详解

    在Netty中,还有另外一个比较常见的对象ByteBuf,它其实等同于Java Nio中的ByteBuffer,但是ByteBuf对Nio中的ByteBuffer的功能做了很作增强,下面我们来简单了解 ...

  8. Dapr-状态管理

    前言: 前一篇对Dapr的服务调用方式进行了解,本篇继续对状态管理进行了解. 一.状态管理-解决的问题 在分布式应用程序中跟踪状态存在一下问题: 应用程序可能需要不同类型的数据存储. 访问和更新数据时 ...

  9. [luogu7207]Sob

    为了方便,先将$n$减小1,即两者范围分别为$[0,n]$和$[m,m+n]$ 结论:取$u=\min_{i\in [m,m+n],n\& i=n}i$,则$\forall 0\le i\le ...

  10. [loj3341]时代的眼泪

    题意即求在区间$[l,r]$中且权值在$[x,y]$中的逆序对个数 考虑分块,逆序对个数包含4部分: 1.左/右块外内部,预处理出$i$到其所在块的块首/尾,小于/小于等于$j$(需要对$j$离散)的 ...