前言
此文章仅供扩展知识点。一般一个系统就只有一个数据源。如果有多个那就是这个系统职能过多,应该要拆分了。
当然如果是读写分离,请参考《mybatis配置读写分离》,使用AbstractRoutingDataSource
重写determineCurrentLookupKey
方法。
在切换数据源之前 @Transactional 先执行,此时会去获取数据源,而此时数据源还没有切换,就会获取默认的数据源。这种情况会导致数据源切换失败。
多数据源事务使用2个步骤
1. 为每个数据源定义一个事务管理器
如下面代码,有2个数据源分别连接数据库ds1和ds2,然后为每个数据源定义了1个事务管理器,此时spring容器中有2个数据源和2个事务管理器
//数据源1
@Bean
public DataSource dataSource1() {
DataSource dataSource = new 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() {
DataSource dataSource = new 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);
}
Ps: 使用@Primary注解可以让事务管理器成为默认,在@Transaction中不需要加事务管理器名称默认使用
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);
}
3. 事务管理器运行过程
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表示不管当前事务管理器中是否有事务,都会重新开启一个事务,如果当前事务管理器中有事务,会把当前事务挂起。所谓挂起,可以这么理解:对当前存在事务的现场生成一个快照,然后将事务现场清理干净,然后重新开启一个新事务,新事务执行完毕之后,将事务现场清理干净,然后再根据前面的快照恢复旧事务。
4.事务管理器如何判断当前是否有事务
Map map=resource的ThreadLocal.get();
DataSource datasource = transactionManager.getDataSource();
Connection conn = map.get(datasource);
//如果conn不为空,就表示当前有事务
if(conn!=null){
}
从这段代码可以看出:判断是否存在事务,主要和datasource有关,和事务管理器无关,即使是不同的事务管理器,只要事务管理器的datasource是一样的,那么就可以发现当前存在的事务。
原文