背景
專案中是有用到多資料來源的,是用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
。