誒,我的動態資料來源怎麼失效了

eaglelihh發表於2022-01-16

背景

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

而不帶事務的方法中,conHoldernull,每次都會獲取一個新的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

相關文章