本篇内容:通过原理和大量案例带大家吃透 Spring 多数据源事务。

Spring 中通过事务管理器来控制事务,每个数据源都需要指定一个事务管理器,如果我们的项目中需要操作多个数据库,那么需要我们配置多个数据源,也就需要配置多个数据管理器。

多数据源事务使用 2 个步骤

1、为每个数据源定义一个事务管理器

如下面代码,有 2 个数据源分别连接数据库 ds1 和 ds2,然后为每个数据源定义了 1 个事务管理器,此时 spring 容器中有 2 个数据源和 2 个事务管理器。

//数据源1
@Bean
public DataSource dataSource1() {
     org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/ds1?characterEncoding=UTF-8");
    dataSource.setUsername("root");
    dataSource.setPassword("root123");
    dataSource.setInitialSize(5);
    return dataSource;
}

//事务管理器1,对应数据源1
@Bean
public PlatformTransactionManager transactionManager1(@Qualifier("dataSource1")DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

//数据源2
@Bean
public DataSource dataSource2() {
    org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/ds2?characterEncoding=UTF-8");
    dataSource.setUsername("root");
    dataSource.setPassword("root123");
    dataSource.setInitialSize(5);
    return dataSource;
}

//事务管理器2,对应数据源2
@Bean
public PlatformTransactionManager transactionManager2(@Qualifier("dataSource2")DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

2、指定事务的管理器 bean 名称

使用@Transaction 中时,需通过@Transaction 注解的 value 或 transactionManager 属性指定事务管理器 bean 名称,如:

@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void required(String name) {
    this.jdbcTemplate1.update("insert into user1(name) VALUES (?)", name);
}

这里补充一下,之前我们使用@Transactional 的时候,并没有通过 value 或者 transactionManager 设置事务管理器,这是为什么?

这是因为我们在 spring 容器中只定义了一个事务管理器,spring 启动事务的时候,默认会按类型在容器中查找事务管理器,刚好容器中只有一个,就拿过来用了,如果有多个的时候,如果你不指定,spring 是不知道具体要用哪个事务管理器的。

多数据源事务的使用就这么简单,下面我们来看案例,案例才是精华。

事务管理器运行过程

这里先给大家解释一下 REQUIRED 传播行为下,事务管理器的大致的运行过程,方便理解后面的案例代码。

Service1中:
@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void m1(){
    this.jdbcTemplate1.update("insert into user1(name) VALUES ('张三')");
 service2.m2();
}

Service2中:
@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void m2(){
    this.jdbcTemplate1.update("insert into user1(name) VALUES ('李四')");
}

spring 事务中有个 resources 的 ThreadLocal,static 修饰的,用来存放共享的资源,稍后过程中会用到。

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

下面看 m1 方法简化版的事务过程:

1、TransactionInterceptor拦截m1方法
2、获取m1方法的事务配置信息:事务管理器bean名称:transactionManager1,事务传播行为:REQUIRED
3、从spring容器中找到事务管理器transactionManager1,然后问一下transactionManager1,当前上下文中有没有事务,显然现在是没有的
4、创建一个新的事务
    //获取事务管理器对应的数据源,即dataSource1
    DataSource dataSource1 = transactionManager1.getDataSource();
    //即从dataSource1中获取一个连接
    Connection conn = transactionManager1.dataSource1.getConnection();
    //开启事务手动提交
    conn.setAutoCommit(false);
    //将dataSource1->conn放入map中
    map.put(dataSource1,conn);
 //将map丢到上面的resources ThreadLocal中
    resources.set(map);
5、下面来带m1放的第一行代码:this.jdbcTemplate1.update("insert into user1(name) VALUES ('张三')");
6、jdbctemplate内部需要获取数据连接,获取连接的过程
    //从resources这个ThreadLocal中获取到map
    Map map = resources.get();
    //通过jdbcTemplate1.datasource从map看一下没有可用的连接
    Connection conn = map.get(jdbcTemplate1.datasource);
    //如果从map没有找到连接,那么重新从jdbcTemplate1.datasource中获取一个
    //大家应该可以看出来,jdbcTemplate1和transactionManager1指定的是同一个dataSource,索引这个地方conn是不为null的
    if(conn==null){
     conn = jdbcTemplate1.datasource.getConnection();
    }
7、通过上面第6步获取的conn执行db操作,插入张三
8、下面来到m1方法的第2行代码:service2.m2();
9、m2方法上面也有@Transactional,TransactionInterceptor拦截m2方法
10、获取m2方法的事务配置信息:事务管理器bean名称:transactionManager1,事务传播行为:REQUIRED
11、从spring容器中找到事务管理器transactionManager1,然后问一下transactionManager1,当前上下文中有没有事务,显然是是有的,m1开启的事务正在执行中,所以m2方法就直接加入这个事务了
12、下面来带m2放的第一行代码:this.jdbcTemplate1.update("insert into user1(name) VALUES ('李四')");
13、jdbctemplate内部需要获取数据连接,获取连接的过程
    //从resources这个ThreadLocal中获取到map
    Map map = resources.get();
    //通过jdbcTemplate1.datasource从map看一下没有可用的连接
    Connection conn = map.get(jdbcTemplate1.datasource);
    //如果从map没有找到连接,那么重新从jdbcTemplate1.datasource中获取一个
    //大家应该可以看出来,jdbcTemplate1和transactionManager1指定的是同一个dataSource,索引这个地方conn是不为null的
    if(conn==null){
        conn = jdbcTemplate1.datasource.getConnection();
    }
14、通过第13步获取的conn执行db操作,插入李四
15、最终TransactionInterceptor发现2个方法都执行完毕了,没有异常,执行事务提交操作,如下
    //获取事务管理器对应的数据源,即dataSource1
    DataSource dataSource1 = transactionManager1.getDataSource();
    //从resources这个ThreadLocal中获取到map
    Map map = resources.get();
    //通过map拿到事务管理器开启的连接
    Connection conn = map.get(dataSource1);
    //通过conn提交事务
    conn.commit();
    //管理连接
    conn.close();
16、清理ThreadLocal中的连接:通过map.remove(dataSource1)将连接从resource ThreadLocal中移除
17、清理事务

从上面代码中可以看出:整个过程中有 2 个地方需要用到数据库连接 Connection 对象,第 1 个地方是:spring 事务拦截器启动事务的时候会从 datasource 中获取一个连接,通过这个连接开启事务手动提交,第 2 个地方是:最终执行 sql 操作的时候,也需要用到一个连接。那么必须确保这两个连接必须是同一个连接的时候,执行 sql 的操作才会受 spring 事务控制,那么如何确保这 2 个是同一个连接呢?从代码中可以看出必须让事务管理器中的 datasource 和 JdbcTemplate 中的 datasource 必须是同一个,那么最终 2 个连接就是同一个对象。

这里顺便回答一下群友问的一个问题:什么是事务挂起操作?

这里以事务传播行为 REQUIRED_NEW 为例说明一下,REQUIRED_NEW 表示不管当前事务管理器中是否有事务,都会重新开启一个事务,如果当前事务管理器中有事务,会把当前事务挂起。

所谓挂起,你可以这么理解:对当前存在事务的现场生成一个快照,然后将事务现场清理干净,然后重新开启一个新事务,新事务执行完毕之后,将事务现场清理干净,然后再根据前面的快照恢复旧事务

下面我们再回到本文的内容,多数据源事务管理。

事务管理器如何判断当前是否有事务?

简化版的过程如下:

Map map=resource的ThreadLocal.get();
DataSource datasource = transactionManager.getDataSource();
Connection conn = map.get(datasource);
//如果conn不为空,就表示当前有事务
if(conn!=null){
}

从这段代码可以看出:判断是否存在事务,主要和 datasource 有关,和事务管理器无关,即使是不同的事务管理器,只要事务管理器的 datasource 是一样的,那么就可以发现当前存在的事务。

事务管理器的运行过程和如何判断是否有事务,这 2 点大家一定要理解,这个理解了,后面的案例理解起来会容易很多。

下面上案例。

案例源码

git地址:
https://gitee.com/javacode2018/spring-series

本文案例对应源码:
案例1:spring-series\lesson-002-tx\src\main\java\com\javacode2018\tx\demo7
案例2:spring-series\lesson-002-tx\src\main\java\com\javacode2018\tx\demo8

案例 1

准备案例代码

1、准备 db

2 个数据库:ds1、ds2

每个库中 2 个表:user1、user2

DROP DATABASE IF EXISTS ds1;
CREATE DATABASE if NOT EXISTS ds1;

USE ds1;
DROP TABLE IF EXISTS user1;
CREATE TABLE user1(
  id int PRIMARY KEY AUTO_INCREMENT,
  name varchar(64) NOT NULL DEFAULT '' COMMENT '姓名'
);

DROP TABLE IF EXISTS user2;
CREATE TABLE user2(
  id int PRIMARY KEY AUTO_INCREMENT,
  name varchar(64) NOT NULL DEFAULT '' COMMENT '姓名'
);

DROP DATABASE IF EXISTS ds2;
CREATE DATABASE if NOT EXISTS ds2;

USE ds2;
DROP TABLE IF EXISTS user1;
CREATE TABLE user1(
  id int PRIMARY KEY AUTO_INCREMENT,
  name varchar(64) NOT NULL DEFAULT '' COMMENT '姓名'
);

DROP TABLE IF EXISTS user2;
CREATE TABLE user2(
  id int PRIMARY KEY AUTO_INCREMENT,
  name varchar(64) NOT NULL DEFAULT '' COMMENT '姓名'
);

2、spring 配置类

定义 2 个数据源:dataSource1、dataSource2,分别用来连接数据库 ds1 和 ds2

定义 2 个 JdbcTemplate:jdbcTemplate1、jdbcTemplate2,分别关联 dataSource1 和 dataSource2

2 个数据源对应 2 个事务管理器:transactionManager1、transactionManager2,分别用来管理 2 个数据源的事务

6 个 bean 的名称

数据源JdbcTemplate事务管理器
dataSource1jdbcTemplate1transactionManager1
dataSource2jdbcTemplate2transactionManager2

源码如下:

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@EnableTransactionManagement //开启spring事务管理功能
@Configuration //指定当前类是一个spring配置类
@ComponentScan //开启bean扫描注册
public class MainConfig7 {
    //定义数据源1,连接数据库:ds1
    @Bean
    public DataSource dataSource1() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/ds1?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    //定义一个JdbcTemplate,对应数据源dataSource1,用来操作数据库:ds1
    @Bean
    public JdbcTemplate jdbcTemplate1(@Qualifier("dataSource1") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    //定义事务管理器transactionManager1,对应数据源dataSource1,用来管理数据库ds1中的事务
    @Bean
    public PlatformTransactionManager transactionManager1(@Qualifier("dataSource1") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    //定义数据源2,连接数据库:ds2
    @Bean
    public DataSource dataSource2() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/ds2?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    //定义一个JdbcTemplate,对应数据源dataSource2,用来操作数据库:ds2
    @Bean
    public JdbcTemplate jdbcTemplate2(@Qualifier("dataSource2") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    //定义事务管理器transactionManager2,对应数据源dataSource2,用来管理数据库ds2中的事务
    @Bean
    public PlatformTransactionManager transactionManager2(@Qualifier("dataSource2") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

下面来定义 4 个 service,分别用来操作 2 个库中的 4 张表。

3、Ds1User1Service

用来操作ds1.user1表,注意下面代码中@Transactional 注解中 transactionManager 的值为transactionManager1

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class Ds1User1Service {
    @Autowired
    private JdbcTemplate jdbcTemplate1;

    @Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
    public void required(String name) {
        this.jdbcTemplate1.update("insert into user1(name) VALUES (?)", name);
    }

}

同样,再来定义操作另外 3 个表的 service

4、Ds1User2Service

用来操作ds1.user2表,事务管理器也是transactionManager1

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class Ds1User2Service {
    @Autowired
    private JdbcTemplate jdbcTemplate1;

    @Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
    public void required(String name) {
        this.jdbcTemplate1.update("insert into user2(name) VALUES (?)", name);
    }

}

5、Ds2User1Service

用来操作ds2.user1表,对应事务管理器transactionManager2

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class Ds2User1Service {
    @Autowired
    private JdbcTemplate jdbcTemplate2;

    @Transactional(transactionManager = "transactionManager2", propagation = Propagation.REQUIRED)
    public void required(String name) {
        this.jdbcTemplate2.update("insert into user1(name) VALUES (?)", name);
    }

}

6、Ds2User2Service

用来操作ds2.user2表,对应事务管理器transactionManager2

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class Ds2User2Service {
    @Autowired
    private JdbcTemplate jdbcTemplate2;

    @Transactional(transactionManager = "transactionManager2", propagation = Propagation.REQUIRED)
    public void required(String name) {
        this.jdbcTemplate2.update("insert into user2(name) VALUES (?)", name);
    }

}

7、Tx1Service

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Tx1Service {
    @Autowired
    private Ds1User1Service ds1User1Service;
    @Autowired
    private Ds1User2Service ds1User2Service;
}

8、Tx2Service

package com.javacode2018.tx.demo7;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Tx2Service {
    @Autowired
    private Ds2User1Service ds2User1Service;
    @Autowired
    private Ds2User2Service ds2User2Service;
}

9、测试类 Demo7Test

package com.javacode2018.tx.demo7;

import org.junit.After;
import org.junit.Before;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

public class Demo7Test {

    private Tx1Service txService1;
    private JdbcTemplate jdbcTemplate1;
    private JdbcTemplate jdbcTemplate2;

    //@Before标注的方法会在任意@Test方法执行之前执行,我们这在里清理一下2库中4张表的数据
    @Before
    public void before() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig7.class);
        txService1 = context.getBean(Tx1Service.class);
        this.jdbcTemplate1 = context.getBean("jdbcTemplate1", JdbcTemplate.class);
        this.jdbcTemplate2 = context.getBean("jdbcTemplate2", JdbcTemplate.class);
        jdbcTemplate1.update("truncate table ds1.user1");
        jdbcTemplate1.update("truncate table ds1.user2");
        jdbcTemplate2.update("truncate table ds2.user1");
        jdbcTemplate2.update("truncate table ds2.user2");
    }

    //@After标注的方法会在任意@Test方法执行完毕之后执行,我们在此处输出4张表的数据,用来查看测试案例之后,表中的数据清空
    @After
    public void after() {
        System.out.println("ds1.user1表数据:" + this.jdbcTemplate1.queryForList("SELECT * from user1"));
        System.out.println("ds1.user2表数据:" + this.jdbcTemplate1.queryForList("SELECT * from user2"));
        System.out.println("ds2.user1表数据:" + this.jdbcTemplate2.queryForList("SELECT * from user1"));
        System.out.println("ds2.user2表数据:" + this.jdbcTemplate2.queryForList("SELECT * from user2"));
    }


}

代码验证

1、场景 1

外围方法和内部方法使用相同的事务管理器,传播行为都是 REQUIRED。

Tx1Service 中添加代码

@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void test1() {
    this.ds1User1Service.required("张三");
    this.ds1User2Service.required("李四");
    throw new RuntimeException();
}

方法、事务管理器、事务管理器对应数据源、操作 db 的 jdbctemplate 中数据源对应关系。

方法事务管理器事务管理器对应数据源jdbctemplate 对应数据源
test1transactionManager1datasource1-
ds1User1Service.requiredtransactionManager1datasource1datasource1
this.ds1User2Service.requiredtransactionManager1datasource1datasource1

Demo7Test 中添加测试用例

@Test
public void test1() {
    this.txService1.test1();
}

运行输出

ds1.user1表数据:[]
ds1.user2表数据:[]
ds2.user1表数据:[]
ds2.user2表数据:[]

结论分析

数据库结果结果分析
“张三”、“李四”均为插入外围方法和内部方法使用同一个事务管理器 transactionManager1,且事务管理器和 jdbctemplate 的 datasource 都是同一个,外围方法会开启事务,内部方法加入外围方法事务,外围方法弹出异常导致事务回滚,内部方法跟着回滚了。

2、场景 2

外部方法和内部方法使用不同的事务管理器。

Tx1Service 中添加代码

@Transactional(transactionManager = "transactionManager2", propagation = Propagation.REQUIRED)
public void test2() {
    this.ds1User1Service.required("张三");
    this.ds1User2Service.required("李四");
    throw new RuntimeException();
}

方法、事务管理器、事务管理器对应数据源、操作 db 的 jdbctemplate 中数据源对应关系。

方法事务管理器事务管理器对应数据源jdbctemplate 对应数据源
test2transactionManager2datasource2-
ds1User1Service.required("张三");transactionManager1datasource1datasource1
this.ds1User2Service.required("李四");transactionManager1datasource1datasource1

Demo7Test 中添加测试用例

@Test
public void test2() {
    this.txService1.test2();
}

运行输出

ds1.user1表数据:[{id=1, name=张三}]
ds1.user2表数据:[{id=1, name=李四}]
ds2.user1表数据:[]
ds2.user2表数据:[]

结论分析

数据库结果结果分析
"张三"、"李四"均插入外围方法 test2 和内部两个 required 方法用到的不是同一个事务管理器,内部的 2 个方法在各自的事务中执行,不受外部方法事务的控制。

3、场景 3

Tx1Service 中添加代码

@Autowired
private Ds2User1Service ds2User1Service;
@Autowired
private Ds2User2Service ds2User2Service;

@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void test3() {
    this.ds1User1Service.required("张三");
    this.ds1User2Service.required("李四");
    this.ds2User1Service.required("王五");
    this.ds2User2Service.required("赵六");
    throw new RuntimeException();
}

方法、事务管理器、事务管理器对应数据源、操作 db 的 jdbctemplate 中数据源对应关系。

方法事务管理器事务管理器对应数据源jdbctemplate 对应数据源
test3transactionManager1datasource1-
this.ds1User1Service.required("张三");transactionManager1datasource1datasource1
this.ds1User2Service.required("李四");transactionManager1datasource1datasource1
this.ds2User1Service.required("王五");transactionManager2datasource2datasource2
this.ds2User2Service.required("赵六");transactionManager2datasource2datasource2

Demo7Test 中添加测试用例

@Test
public void test3() {
    this.txService1.test3();
}

运行输出

ds1.user1表数据:[]
ds1.user2表数据:[]
ds2.user1表数据:[{id=1, name=王五}]
ds2.user2表数据:[{id=1, name=赵六}]

结论分析

"张三"、"李四"都未插入,"王五"、“赵六”插入成功。

外围方法和内部的前 2 个 required 方法事务管理器都是 transactionManager1,所以他们 3 个在一个事务中执行;而内部的后 2 个 required 方法事务管理器是 transactionManager2,他们分别在自己的事务中执行,不受外围方法事务的控制,外围方法感受到了异常,回滚事务,只会导致内部的前 2 个 required 方法回滚。

4、场景 4

Tx2Service 中加入代码

@Transactional(transactionManager = "transactionManager2", propagation = Propagation.REQUIRED)
public void test1() {
    this.ds2User1Service.required("王五");
    this.ds2User2Service.required("赵六");
}

Tx1Service 中加入代码

@Autowired
private Tx2Service tx2Service;

@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void test4() {
    this.ds1User1Service.required("张三");
    this.ds1User2Service.required("李四");
    this.tx2Service.test1();
    throw new RuntimeException();
}

方法、事务管理器、事务管理器对应数据源、操作 db 的 jdbctemplate 中数据源对应关系。

方法事务管理器事务管理器对应数据源jdbctemplate 对应数据源
test4transactionManager1datasource1-
this.ds1User1Service.requiredtransactionManager1datasource1datasource1
this.ds1User2Service.requiredtransactionManager1datasource1datasource1
this.tx2Service.test1()transactionManager2datasource2-
this.ds2User1Service.requiredtransactionManager2datasource2datasource2
this.ds2User2Service.requiredtransactionManager2datasource2datasource2

Demo7Test 中添加测试用例

@Test
public void test4() {
    this.txService1.test4();
}

运行输出

ds1.user1表数据:[]
ds1.user2表数据:[]
ds2.user1表数据:[{id=1, name=王五}]
ds2.user2表数据:[{id=1, name=赵六}]

结论分析

"张三"、"李四"都未插入,"王五"、“赵六”插入成功。

分析一下过程

1、test4在事务管理器transactionManager1中开启事务tm1,并将连接放入resourceThreadLocal中(datasource1->conn1)
2、this.ds1User1Service.required("张三")事务管理器是transactionManager1,所以会加入事务tm1中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource1,所以会获取到threadlocal中的conn1来插入数据
3、this.ds1User2Service.required("李四")事务管理器是transactionManager1,所以会加入事务tm1中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource1,所以会获取到threadlocal中的conn1来插入数据
4、执行this.tx2Service.test1(),这个方法事务管理器是transactionManager2,所以会重新开启一个事务tm2,并将连接放入resourceThreadLocal中(datasource2->conn2)
5、this.ds2User1Service.required("王五")事务管理器是transactionManager2,通过所以会加入事务tm2中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource2,所以会获取到threadlocal中的conn2来插入数据
6、this.ds2User2Service.required("赵六")事务管理器是transactionManager2,所以会加入事务tm2中,通过jdbctemplate1插入张三,由于jdbctemplate1.datasource是datasource2,所以会获取到threadlocal中的conn2来插入数据
7、tm2提交
8、tm1发现test4抛出异常,tm1执行回滚

5、场景 5

Tx2Service 中加入代码

@Transactional(transactionManager = "transactionManager2", propagation = Propagation.REQUIRED)
public void test2() {
    this.ds2User1Service.required("王五");
    this.ds2User2Service.required("赵六");
    throw new RuntimeException();
}

Tx1Service 中加入代码

@Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
public void test5() {
    this.ds1User1Service.required("张三");
    this.ds1User2Service.required("李四");
    this.tx2Service.test2();
}

方法、事务管理器、事务管理器对应数据源、操作 db 的 jdbctemplate 中数据源对应关系。

方法事务管理器事务管理器对应数据源jdbctemplate 对应数据源
test4transactionManager1datasource1-
this.ds1User1Service.requiredtransactionManager1datasource1datasource1
this.ds1User2Service.requiredtransactionManager1datasource1datasource1
this.tx2Service.test1()transactionManager2datasource2-
this.ds2User1Service.requiredtransactionManager2datasource2datasource2
this.ds2User2Service.requiredtransactionManager2datasource2datasource2

Demo7Test 中添加测试用例

@Test
public void test5() {
    this.txService1.test5();
}

运行输出

ds1.user1表数据:[]
ds1.user2表数据:[]
ds2.user1表数据:[]
ds2.user2表数据:[]

结论分析

4 个表都未插入数据。

外围方法 test5 通过事务管理器 transactionManager1 开启了事务 tm1,内部方法插入“张三”,“李四”加入了 tm1 事务,而 test2 通过事务管理器 transactionManager2 又开启了一个事务 tm2,test2 内部方法插入“王五”,“赵六”加入了 tm2 事务,test2 内部抛出了异常,tm2 和 tm1 都感受到了这个异常,所以 2 个事务都进行了回滚操作。

案例 2

spring 配置类

package com.javacode2018.tx.demo8;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@EnableTransactionManagement //开启spring事务管理功能
@Configuration //指定当前类是一个spring配置类
@ComponentScan //开启bean扫描注册
public class MainConfig8 {
    //定义数据源1,连接数据库:ds1
    @Bean
    public DataSource dataSource1() {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/ds1?characterEncoding=UTF-8");
        dataSource.setUsername("root");
        dataSource.setPassword("root123");
        dataSource.setInitialSize(5);
        return dataSource;
    }

    //定义一个jdbcTemplate1
    @Bean
    public JdbcTemplate jdbcTemplate1(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    //定义事务管理器transactionManager1
    @Bean
    public PlatformTransactionManager transactionManager1(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    //定义jdbcTemplate2
    @Bean
    public JdbcTemplate jdbcTemplate2(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    //定义事务管理器transactionManager2
    @Bean
    public PlatformTransactionManager transactionManager2(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

上面代码中

  • 定义了 1 个数据源:dataSource1

  • 2 个 jdbctemplate:jdbcTemplate1 和 jdbcTemplate2,他们的 datasource 都是 dataSource1

  • 2 个事务管理器:transactionManager1 和 transactionManager2,他们的 datasource 都是 dataSource1

有同学发现这样写是不是很奇怪,不是说一个数据源定义一个事务管理器么,这什么操作?

不急,我们这样写,是为了让你更深入了解其原理。

User2Service

内部的 required 方法操作 db 用的是 jdbcTemplate2,事务管理器为 transactionManager2

package com.javacode2018.tx.demo8;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class User2Service {
    @Autowired
    private JdbcTemplate jdbcTemplate2;

    @Transactional(transactionManager = "transactionManager2", propagation = Propagation.REQUIRED)
    public void required() {
        this.jdbcTemplate2.update("insert into user2(name) VALUES (?)", "李四");
    }

}

User2Service

内部的 required 方法操作 db 用的是 jdbcTemplate1,事务管理器为 transactionManager1,并且会调用 user2Service 的 required 方法。

package com.javacode2018.tx.demo8;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
public class User1Service {
    @Autowired
    private JdbcTemplate jdbcTemplate1;

    @Autowired
    private User2Service user2Service;

    @Transactional(transactionManager = "transactionManager1", propagation = Propagation.REQUIRED)
    public void required() {
        this.jdbcTemplate1.update("insert into user1(name) VALUES (?)", "张三");
        this.user2Service.required();
        throw new RuntimeException();
    }

}

大家觉得 required 方法执行完毕之后,会是什么结果?

A:张三未插入、李四插入成功
B:张三、李四均为插入

大家先思考一下,先别看下面的执行结果,可以参考事务管理器的执行过程分析一下结果。

好了,我们上测试用例。

Demo8Test

package com.javacode2018.tx.demo8;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;

public class Demo8Test {

    private User1Service user1Service;
    private JdbcTemplate jdbcTemplate1;

    //@Before标注的方法会在任意@Test方法执行之前执行,我们这在里清理一下2库中4张表的数据
    @Before
    public void before() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig8.class);
        this.user1Service = context.getBean(User1Service.class);
        this.jdbcTemplate1 = context.getBean("jdbcTemplate1", JdbcTemplate.class);
        jdbcTemplate1.update("truncate table ds1.user1");
        jdbcTemplate1.update("truncate table ds1.user2");
    }

    //@After标注的方法会在任意@Test方法执行完毕之后执行,我们在此处输出4张表的数据,用来查看测试案例之后,表中的数据清空
    @After
    public void after() {
        System.out.println("ds1.user1表数据:" + this.jdbcTemplate1.queryForList("SELECT * from user1"));
        System.out.println("ds1.user2表数据:" + this.jdbcTemplate1.queryForList("SELECT * from user2"));
    }

    @Test
    public void test1() {
        this.user1Service.required();
    }

}

运行输出

ds1.user1表数据:[]
ds1.user2表数据:[]

结果是都没有插入。

结果分析

分析一下执行过程

1、this.user1Service.required();
2、事务拦截器拦截user1Service.required()方法,事务配置信息:(事务管理器:transactionManager1,传播行为REQUIRED)
3、问一下transactionManager1,当前是否有事务,transactionManager2看了一下,发现没有,那么重新创建一个事务tm1,通过transactionManager1中的datasource,即datasource1重新获取一个连接:conn1,然后丢到resourceThreadLocal中(datasource1->conn1)
4、执行this.jdbcTemplate1.update("insert into user1(name) VALUES (?)", "张三"),由于jdbcTemplate1中的datasource是datasource1,所以会从resourceThreadLocal中拿到conn1连接来执行sql
5、执行this.user2Service.required();
6、事务拦截器拦截user1Service.required()方法,事务配置信息:(事务管理器:transactionManager2,传播行为REQUIRED)
7、问一下transactionManager2,当前是否有事务?大家在回头看一下事务管理器是如何判断当前是否有事务的,由于transactionManager2和transactionManager1用到的都是datasource1,所以transactionManager2会发现当前是存在事务的,即tm1
8、执行this.jdbcTemplate2.update("insert into user2(name) VALUES (?)", "李四"),由于jdbcTemplate2中的datasource也是datasource1,所以会从resourceThreadLocal中拿到conn1连接来执行sql
9、最终整个操作过程中只有一个事务tm1,一个连接conn1,通过conn1执行2个插入操作
10、执行throw new RuntimeException();抛出异常
11、tm1感受到了异常,所以会执行回滚操作,最终都插入失败

总结一下

1、本文介绍了多数据源事务的使用,2 个步骤:先为每个数据源定义一个事务管理器,然后在@Transactional 中指定具体要使用哪个事务管理器。

2、事务管理器运行过程、事务管理器如何判断当前是否有事务,这 2 点非常非常重要,大家再看一下

案例源码

git地址:
https://gitee.com/javacode2018/spring-series

本文案例对应源码:
    spring-series\lesson-002-tx\src\main\java\com\javacode2018\tx\demo7
    spring-series\lesson-002-tx\src\main\java\com\javacode2018\tx\demo8
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐