【十九】Spring Boot之分布式事务(JTA、Atomikos、Druid、Mybatis)

本文详细介绍如何使用SpringBoot结合JTA和Atomikos实现跨数据库的分布式事务处理,包括配置多数据源、整合Druid连接池、Mybatis以及具体代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、介绍
1.分布式、本地事务
1.本地事务:普通事务。只能保证在一个数据库上的操作ACID。

JDBC事务就是本地事务,通过connection对象管理。

2.分布式事务:两个及以上数据库源的事务(由每台数据库的本地事务组成的), 使事务可以跨越多个数据库。比如,A库的a1表和B库的b1表,在一个事务中,如果B库的b1表回滚了,A库的a1表也要回滚。

JTA事务支持分布式事务。JTA指Java事务API(Java Transaction API),它本身只是为事务管理提供了接口,Atomikos是其中一种实现。 

2.分布式事务实现方案
1.资源层的分布式事务:代表JTA,钢性事务,强一致性,跟交易、结算、钱有关的,对数据一致性要求高的可以考虑用这个。

2.服务层分布式事务:代表TCC,柔性事务,弱一致性。

3.钢性事务、强一致性满足四个原则ACID:
1.原子性(Atomicity):即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做。

2.一致性(Consistency):在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是应该处于正确的状态,即数据完整性约束没有被破坏;如银行转帐,A转帐给B,必须保证A的钱一定转给B,一定不会出现A的钱转了但B没收到,否则数据库的数据就处于不一致(不正确)的状态。

3.隔离性(Isolation):并发事务执行之间互不影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性。

4.持久性(Durability):事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失。

关于事务的隔离机制,详细解释请看

【十六】Spring Boot之事务(事务传播机制、嵌套事务、事务隔离机制详解) 

4.柔性事务、弱一致性介绍
4.1.CAP理论
CAP理论:在一个分布式系统中,最多只能满足C、A、P中的2个。

CAP含义:

C:Consistency 一致性:同一数据的多个副本是否实时相同。

A:Availability 可用性:一定时间内,系统能返回一个明确的结果 则称为该系统可用。

P:Partition tolerance 分区容错性:将同一服务分布在多个系统中,从而保证某一个系统宕机,仍然有其他系统提供相同的服务。

而通常情况下,我们都必须要满足AP,所以只能牺牲C。

牺牲一致性换取可用性和分区容错性。

牺牲一致性的意思是,把强一致换成弱一致。只要数据最终能一致就好了,并不要实时一致。

4.2.BASE理论
主要就是分布式系统中最CAP怎么取舍怎么平衡的一个理论

BA:Basic Available 基本可用  一定时间内能够返回一个明确的结果。

基本可用BA和高可用HA的区别是:

1.响应时间可以更长。

2.给部分用户返回一个降级页面。返回降级页面仍然是返回明确结果。

S:Soft State:柔性状态。同一数据的不同副本的状态,不用实时一致。

E:Eventual Consisstency:最终一致性。 同一数据的不同副本的状态,不用实时一致,但一定要保证经过一定时间后最终是一致的。

二、DTP分布式事务模型
2.1 JTA(XA)协议
分布式事务的规范。

XA规范主要定义了:事务管理器(Transaction Manager)和资源管理器(Resource Manager)之间的接口。

XA接口是双向的系统接口。在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。

在分布式系统中,从理论上讲,两台机器无法达到一致的状态,需要引入一个单点进行协调。

XA中大致分为两个部分:

1.事务管理器(Transaction Manager),全局事务管理器。

事务管理器控制着全局事务,管理事务生命周期,并协调资源。负责各个本地资源的提交和回滚。

2.资源管理器(Resource Manager),可以理解成是一个局部的事务管理器,一个本地事务管理器或者消息队列。

 

2.2 XA支持二阶段提交 2PC
第一阶段:全局事务(协调者)通知各个本地事务(参与者)干活,本地事务干活的时候将Undo信息和Redo信息写入日志,各个本地事务干完活通知全局事务。

该阶段,报错:只要有一个本地事务干活失败,就会通知全局事务not ready,全局事务只要收到一个本地事务执行失败not ready的通知,全局事务就会通知所有本地事务全部回滚rollback。

该阶段,超时:只要有一个本地事务,很久没有告诉全局事务自己干完活ready,过了等待超时的时间,全局事务则通知所有本地事务回滚rollback。

第二阶段:全局事务收到所有本地事务干完活ready 的通知,全局事务通知各个本地事务提交commit。各个本地事务执行commit。 

该阶段,报错:只要有一个本地事务commit失败,就会通知全局事务全局事务只要收到一个本地事务commit失败的通知,全局事务就会通知所有本地事务全部回滚rollback。

该阶段,超时:只要有一个本地事务,很久没有告诉全局事务自己commit成功,过了等待超时的时间,全局事务则通知所有本地事务回滚rollback。

缺点:

1.同步阻塞。一个本地事务执行完了,要等同一个全局事务内所有本地事务执行完才提交,等待过程中没有释放资源。

2.协调者单点故障,如果协调者故障,参与者会一直阻塞下去。这个可以想办法把协调者搞成多点。

3.协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

2.3 三阶段提交 3PC


第一阶段:cancommit阶段

协调者询问每一个参与者是否可以执行事务提交操作。

参与者收到cancommit命名后,判断自己是否可用顺利执行事务,返回yes或者no。

第二阶段:precommit阶段

协调者收到每个参与者的yes后,协调者向每个参与者发送precommit预提交命令。

参与者收到precommit预提交命令后,开始执行事务操作,并将undo和redo信息记录到事务日志中。

参与者如果成功的执行了事务操作,则返回ACK响应。

该阶段,报错:协调者只要收到一个no,协调则通知所有参与者中断事务abort。

该阶段,超时:只要有一个参与者,很久没有告诉协调者yes,协调则通知所有参与者中断事务abort。

第三阶段:docommit阶段

协调者收到ACK后,向所有参与者发起docommit命令。

参与者接收到doCommit请求之后,提交事务,释放资源。通知协调者,已提交。

该阶段,报错:协调者只要收到一个no,协调则通知所有参与者中断事务abort。

该阶段,超时:只要有一个参与者,很久没有告诉协调者yes,协调则通知所有参与者中断事务abort。

三、Spring Boot+JTA+Atomikos+Druid+Mybatis使用
场景:现有两个不同的数据库,一个叫sid,一个叫lee。操作sid库中是账户余额表,lee库中是支出金额表。

一个比支出操作,要同时更新sid的账户余额表和lee的支出金额表。失败,两个一起回滚。

项目目录

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.sid</groupId>
    <artifactId>jta-atomikos</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/>
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <!-- spring-boot的web启动的jar包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
 
        <!-- mysql数据库连接包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>
 
        <!-- alibaba的druid数据库连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
 
        <!-- jta-atomikos 分布式事务管理 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.14</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
 
            <!-- mybatis generator 自动生成代码插件 -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.2</version>
                <configuration>
                    <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>
                    <overwrite>true</overwrite>
                    <verbose>true</verbose>
                </configuration>
            </plugin>
 
        </plugins>
    </build>
 
</project>
application.yml

server:
  port: 8080
  context-path: /sid
 
spring:
  datasource:
    druid:
      one:  #数据源1
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/sid
        username: root
        password: root
        #初始化时建立物理连接的个数
        initialSize: 1
        #池中最大连接数
        maxActive: 20
        #最小空闲连接
        minIdle: 1
        #获取连接时最大等待时间,单位毫秒
        maxWait: 60000
        #有两个含义:
        #1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
        #2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
        timeBetweenEvictionRunsMillis: 60000
        #连接保持空闲而不被驱逐的最小时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        #使用该SQL语句检查链接是否可用。如果validationQuery=null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
        validationQuery: SELECT 1 FROM DUAL
        #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
        testWhileIdle: true
        #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        testOnBorrow: false
        #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        testOnReturn: false
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,slf4j
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        #connectionProperties.druid.stat.mergeSql: true
        #connectionProperties.druid.stat.slowSqlMillis: 5000
        # 合并多个DruidDataSource的监控数据
        #useGlobalDataSourceStat: true
        #default-auto-commit: true 默认
        #default-auto-commit: false
      two: #数据源2
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/lee
        username: root
        password: root
        #初始化时建立物理连接的个数
        initialSize: 1
        #池中最大连接数
        maxActive: 20
        #最小空闲连接
        minIdle: 1
        #获取连接时最大等待时间,单位毫秒
        maxWait: 60000
        #有两个含义:
        #1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
        #2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
        timeBetweenEvictionRunsMillis: 60000
        #连接保持空闲而不被驱逐的最小时间,单位是毫秒
        minEvictableIdleTimeMillis: 300000
        #使用该SQL语句检查链接是否可用。如果validationQuery=null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
        validationQuery: SELECT 1 FROM DUAL
        #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
        testWhileIdle: true
        #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        testOnBorrow: false
        #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
        testOnReturn: false
        # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
        filters: stat,wall,slf4j
        # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        #connectionProperties.druid.stat.mergeSql: true
        #connectionProperties.druid.stat.slowSqlMillis: 5000
        # 合并多个DruidDataSource的监控数据
        #useGlobalDataSourceStat: true
        #default-auto-commit: true 默认
        #default-auto-commit: false
 
## 该配置节点为独立的节点,不是在在spring的节点下
mybatis:
  mapper-locations: classpath:mapping/*/*.xml  #注意:一定要对应mapper映射xml文件的所在路径
  type-aliases-package: com.sid.model  # 注意:对应实体类的路径
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #控制台打印sql
启动类

@SpringBootApplication
@MapperScan("com.sid.mapper.*.*")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
第一个数据源配置Properties

@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.one")
public class OneDataSourceProperties {
    private String driverClassName;
 
    private String url;
 
    private String username;
 
    private String password;
 
    private Integer initialSize;
 
    private Integer maxActive;
 
    private Integer minIdle;
    private Integer maxWait;
    private Integer timeBetweenEvictionRunsMillis;
 
    private Integer minEvictableIdleTimeMillis;
 
    private String validationQuery;
    private Boolean testWhileIdle;
    private Boolean testOnBorrow;
    private Boolean testOnReturn;
 
    private String filters;
 
}
第二个数据源配置Properties

@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.two")
public class TwoDataSourceProperties {
    private String driverClassName;
 
    private String url;
 
    private String username;
 
    private String password;
 
    private Integer initialSize;
 
    private Integer maxActive;
 
    private Integer minIdle;
    private Integer maxWait;
    private Integer timeBetweenEvictionRunsMillis;
 
    private Integer minEvictableIdleTimeMillis;
 
    private String validationQuery;
    private Boolean testWhileIdle;
    private Boolean testOnBorrow;
    private Boolean testOnReturn;
 
    private String filters;
}
第一个数据源配置

@Configuration
//这里要指明这个数据适用于哪些mapper,和这个数据源的sqlsessionFactory
@MapperScan(basePackages = "com.sid.mapper.sid", sqlSessionFactoryRef = "oneSqlSessionFactory")
public class OneDataSourceConfiguration {
    @Autowired
    public OneDataSourceProperties oneDataSourceProperties;
 
    //配置第一个数据源
    @Primary
    @Bean(name = "oneDataSource")
    public DataSource oneDataSource() {
        // 这里datasource要使用阿里的支持XA的DruidXADataSource
        DruidXADataSource datasource = new DruidXADataSource();
        BeanUtils.copyProperties(oneDataSourceProperties,datasource);
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(datasource);
        xaDataSource.setUniqueResourceName("oneDataSource");
        return xaDataSource;
    }
 
    //配置第一个sqlsessionFactory
    @Primary
    @Bean(name = "oneSqlSessionFactory")
    public SqlSessionFactory oneSqlSessionFactory(@Qualifier("oneDataSource") DataSource oneDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(oneDataSource);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resolver.getResources("classpath:mapping/sid/*.xml"));
        return bean.getObject();
    }
}
第二个数据源配置

@Configuration
@MapperScan(basePackages = "com.sid.mapper.lee", sqlSessionFactoryRef = "twoSqlSessionFactory")
public class TwoDataSourceConfiguration {
    @Autowired
    public TwoDataSourceProperties twoDataSourceProperties;
 
    @Bean(name = "twoDataSource")
    public DataSource twoDataSource() {
        DruidXADataSource datasource = new DruidXADataSource();
        BeanUtils.copyProperties(twoDataSourceProperties,datasource);
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(datasource);
        xaDataSource.setUniqueResourceName("twoDataSource");
        return xaDataSource;
    }
 
    @Bean(name = "twoSqlSessionFactory")
    public SqlSessionFactory twoSqlSessionFactory(@Qualifier("twoDataSource") DataSource twoDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(twoDataSource);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        bean.setMapperLocations(resolver.getResources("classpath:mapping/lee/*.xml"));
        return bean.getObject();
    }
}
第一个数据源mapper

@Mapper
public interface AccountMapper {
    int deleteByPrimaryKey(Integer id);
 
    int insert(Account record);
 
    int insertSelective(Account record);
 
    Account selectByPrimaryKey(Integer id);
 
    int updateByPrimaryKeySelective(Account record);
 
    int updateByPrimaryKey(Account record);
}
mapping.xml就不贴了,就是Mybatis的插件生生了 

第二个数据源mapper

@Mapper
public interface ExpenditureMapper {
    int deleteByPrimaryKey(Integer id);
 
    int insert(Expenditure record);
 
    int insertSelective(Expenditure record);
 
    Expenditure selectByPrimaryKey(Integer id);
 
    int updateByPrimaryKeySelective(Expenditure record);
 
    int updateByPrimaryKey(Expenditure record);
}
model类

public class Account {
    private Integer id;
 
    private BigDecimal accountBalance;
 
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public BigDecimal getAccountBalance() {
        return accountBalance;
    }
 
    public void setAccountBalance(BigDecimal accountBalance) {
        this.accountBalance = accountBalance;
    }
}
public class Expenditure {
    private Integer id;
 
    private BigDecimal money;
 
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public BigDecimal getMoney() {
        return money;
    }
 
    public void setMoney(BigDecimal money) {
        this.money = money;
    }
}
service层使用事务回滚演示

@Service
public class TestServiceImpl implements TestService {
 
    @Resource
    private AccountMapper accountMapper;
 
    @Resource
    private ExpenditureMapper expenditureMapper;
 
    @Override
    @Transactional
    public String testJtaAtomikos(){
        Account account = new Account();
        account.setAccountBalance(new BigDecimal(560.56));
        accountMapper.insertSelective(account);
 
        Expenditure expenditure = new Expenditure();
        expenditure.setMoney(new BigDecimal(3.3));
        expenditureMapper.insertSelective(expenditure);
        int i = 1 / 0;
        return "done";
    }
}
 
--------------------- 
作者:jy02268879 
来源:优快云 
原文:https://blog.youkuaiyun.com/jy02268879/article/details/84398657 
版权声明:本文为博主原创文章,转载请附上博文链接!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值