背景
项目中是有用到多数据源的,是用AbstractRoutingDataSource这个类来实现数据源的切换。
在使用的过程中,发现在一个事务中,是没办法切换数据源的。
下面就简单介绍一下场景及原因。
模拟现场
Mapper类如下:
@Mapper
public interface UserMapper {
@Results(value = {
@Result(property = "id", column = "id", javaType = Long.class, jdbcType = JdbcType.BIGINT),
@Result(property = "age", column = "age", javaType = Integer.class, jdbcType = JdbcType.INTEGER),
@Result(property = "name", column = "name", javaType = String.class, jdbcType = JdbcType.VARCHAR),
@Result(property = "createTime", column = "create_time", javaType = Date.class, jdbcType = JdbcType.DATE)
})
@Select("SELECT id, age, name, create_time FROM user WHERE id = #{id}")
User selectUser(Long id);
}
自定义AbstractRoutingDataSource:
public class MyDynamicDataSource extends AbstractRoutingDataSource {
@Setter
@Getter
private String key;
@Override
protected Object determineCurrentLookupKey() {
return key;
}
}
一些配置:
@Configuration
@MapperScan("cn.eagle.li.spring.datasource")
@EnableTransactionManagement
public class Config {
@Bean
public MyBean myBean() {
return new MyBean();
}
@Bean(name = "sqlSessionFactory")
@ConditionalOnMissingBean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("myDynamicDataSource") DataSource dataSource) throws Exception {
final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
return sessionFactory.getObject();
}
@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("myDynamicDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean("myDynamicDataSource")
public MyDynamicDataSource dataSource() {
MyDynamicDataSource myDynamicDataSource = new MyDynamicDataSource();
Map<Object, Object> targetDataSource = Maps.newHashMap();
targetDataSource.put("d1", getDataSource1());
targetDataSource.put("d2", getDataSource2());
myDynamicDataSource.setDefaultTargetDataSource(getDataSource1());
myDynamicDataSource.setTargetDataSources(targetDataSource);
return myDynamicDataSource;
}
private static DataSource getDataSource1() {
MysqlConnectionPoolDataSource dataSource = new MysqlConnectionPoolDataSource();
dataSource.setUser("root");
dataSource.setPassword("root");
dataSource.setUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8");
return dataSource;
}
private static DataSource getDataSource2() {
MysqlConnectionPoolDataSource dataSource = new MysqlConnectionPoolDataSource();
dataSource.setUser("root");
dataSource.setPassword("root");
dataSource.setUrl("jdbc:mysql://localhost:3306/test2?useUnicode=true&characterEncoding=utf-8");
return dataSource;
}
}
一个测试的bean:
@Slf4j
@Component
public class MyBean {
@Autowired
private UserMapper userMapper;
@Autowired
private MyDynamicDataSource myDynamicDataSource;
public void test1() {
myDynamicDataSource.setKey("d1");
log.info("user:{}", userMapper.selectUser(1L));
myDynamicDataSource.setKey("d2");
log.info("user:{}", userMapper.selectUser(1L));
}
@Transactional
public void test2() {
test1();
}
}
测试类:
@Slf4j
public class DataSourceMain {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Config.class);
System.out.println(context.getBean(PlatformTransactionManager.class));
MyBean myBean = context.getBean(MyBean.class);
myBean.test1();
myBean.test2();
}
}
返回结果如下:
其中有两个数据库
test数据库里的数据如下:

test2数据库里的数据如下:

运行结果如下:
1473 [main] INFO c.eagle.li.spring.datasource.MyBean - user:User(age=2, name=3, id=1, createTime=Thu Nov 04 00:00:00 CST 2021)
1482 [main] INFO c.eagle.li.spring.datasource.MyBean - user:User(age=2, name=kkk, id=1, createTime=Sat Nov 20 00:00:00 CST 2021)
1494 [main] INFO c.eagle.li.spring.datasource.MyBean - user:User(age=2, name=kkk, id=1, createTime=Sat Nov 20 00:00:00 CST 2021)
1495 [main] INFO c.eagle.li.spring.datasource.MyBean - user:User(age=2, name=kkk, id=1, createTime=Sat Nov 20 00:00:00 CST 2021)
从结果可以看出,第一个不带事务的方法,分别从test和test2两个数据库选出了数据
而第二个带事务的方法,只从test2这一个数据库选出了两条相同的数据。
原因
入口

我们从这里打断点,看谁调到了这里
不带事务的调用顺序如下:
SimpleExecuto.doQuery
SimpleExecutor.prepareStatement
BaseExecutor.getConnection
SpringManagedTransaction.getConnection
SpringManagedTransaction.openConnection
DataSourceUtils.getConnection
DataSourceUtils.doGetConnection
DataSourceUtils.fetchConnection
AbstractRoutingDataSource.getConnection
AbstractRoutingDataSource.determineTargetDataSource
MyDynamicDataSource.determineCurrentLookupKey
SimpleExecutor.prepareStatement如下:

带事务的调用顺序如下:
TransactionAspectSupport.invokeWithinTransaction
TransactionAspectSupport.createTransactionIfNecessary
AbstractPlatformTransactionManager.getTransaction
AbstractPlatformTransactionManager.startTransaction
DataSourceTransactionManager.doBegin
AbstractRoutingDataSource.getConnection
AbstractRoutingDataSource.determineTargetDataSource
MyDynamicDataSource.determineCurrentLookupKey
DataSourceTransactionManager.doBegin如下:

找不同
从SimpleExecutor.prepareStatement开始
不带事务情况下:

带事务的情况下:

可以看到带事务的方法中,conHolder不为null,从这里是可以直接获得Connection
而不带事务的方法中,conHolder为null,每次都会获取一个新的Connection
什么时候放入到conHolder的
看一下是怎么获取conHolder的,DataSourceUtils.doGetConnection如下:

进去看一下,TransactionSynchronizationManager.getResource如下:

继续调用,TransactionSynchronizationManager.doGetResource如下:

从上图可以看到,conHolder是从resources中获取的
resources的定义如下:
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
经过调试,调用顺序是这样的:
DataSourceTransactionManager.doBegin
TransactionSynchronizationManager.bindResource
DataSourceTransactionManager.doBegin如下:

TransactionSynchronizationManager.bindResource如下:

结论
结论就是在事务的方法中,会提前获得一个connection放到ThreadLocal里,然后整个事务都会使用同一个connection。
而不带事务的的方法,不会这么做,每次都去获得新的connection。
