Spring 和 Mybatis 使用不同的數(shù)據(jù)源會怎樣?
本篇文章要討論的一個(gè)問題點(diǎn), 給Spring和Mybatis設(shè)置不同的數(shù)據(jù)庫數(shù)據(jù)源會怎樣?
注意. 正常情況下一定要給Spring和Mybatis設(shè)置相同的數(shù)據(jù)庫數(shù)據(jù)源.
案例代碼位置?
https://github.com/infuq/spring-framework/tree/main/infuq-t/src/main/java/com/infuq/mybatis
案例代碼結(jié)構(gòu)

//AppConfig.javaimport com.alibaba.druid.pool.DruidDataSource;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.mybatis.spring.annotation.MapperScan;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.jdbc.datasource.DriverManagerDataSource;import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.sql.DataSource;("com.infuq.mybatis.mapper")public class AppConfig {// 數(shù)據(jù)源public DataSource druidDataSource() {DruidDataSource dataSource = new DruidDataSource();dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");dataSource.setUrl("jdbc:mysql://localhost:3306/test_0?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true");dataSource.setUsername("root");dataSource.setPassword("9527");return dataSource;}// 數(shù)據(jù)源public DataSource dataSource() {DriverManagerDataSource dataSource = new DriverManagerDataSource();dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");dataSource.setUrl("jdbc:mysql://localhost:3306/test_1?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true");dataSource.setUsername("root");dataSource.setPassword("9527");return dataSource;}// Mybatis需要一個(gè)SqlSessionFactory, 因此向容器中注入一個(gè)SqlSessionFactorypublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dataSource);return factoryBean.getObject();}// 事務(wù)管理器, 用于事務(wù)管理public DataSourceTransactionManager druidTransactionManager(DataSource druidDataSource) {return new DataSourceTransactionManager(druidDataSource);}}
通過圖形的方式, 描述上面AppConfig.java代碼的結(jié)構(gòu)

據(jù)庫數(shù)據(jù)源分別設(shè)置到SqlSessionFactory和事務(wù)管理器.
SqlSessionFactory用于Mybatis操作數(shù)據(jù)庫時(shí)使用,比如insert,update等.
事務(wù)管理器用于Spring開啟事務(wù)等操作.
// UserServiceImpl.javaimport com.infuq.mybatis.mapper.UserMapper;import org.springframework.beans.BeansException;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.transaction.annotation.Transactional;public class UserServiceImpl implements UserService {private UserMapper userMapper;(transactionManager = "druidTransactionManager")public void getList() {userMapper.getList();}}
代碼中使用了事務(wù)管理器, 但使用了select作為案例講解,并沒有使用insert/update作為案例講解,讀者不要太在意.
程序運(yùn)行之后,看一下,Spring容器中存在的UserServiceImpl實(shí)例和UserMapper實(shí)例`長啥樣`.

在容器中存放的是Service的代理對象, 代理對象中存在真正的被代理對象(即真正的UserServiceImpl實(shí)例), 在被代理對象內(nèi)部, 又有mapper代理對象, mapper代理對象持有sqlSessionFactory對象, sqlSessionFactory持有數(shù)據(jù)源.

Service的代理對象內(nèi)部還有一個(gè)事務(wù)攔截器TransactionInterceptor

在調(diào)用鏈路上,在Service代理對象和Service被代理對象之間, 還有一個(gè)事務(wù)攔截器會被調(diào)用到.
開始運(yùn)行程序

運(yùn)行程序之后,首先調(diào)用到service代理對象, 在調(diào)用到事務(wù)攔截器TransactionInterceptor, 就在這個(gè)事務(wù)攔截器中拿到了容器中的事務(wù)管理器TransactionManager, 而這個(gè)事務(wù)管理器就是我們之前配置的.
//AppConfig.javapublic DataSourceTransactionManager druidTransactionManager(DataSource druidDataSource) {return new DataSourceTransactionManager(druidDataSource);}
這個(gè)事務(wù)管理器有一個(gè)很重要的事情需要做. 它需要獲取一個(gè)數(shù)據(jù)庫連接, 并開啟事務(wù).
那么這個(gè)數(shù)據(jù)庫連接從哪里得到呢?
在配置事務(wù)管理器的時(shí)候,給它設(shè)置了一個(gè)數(shù)據(jù)源, 那么事務(wù)管理器就從這個(gè)數(shù)據(jù)源中得到一個(gè)數(shù)據(jù)庫連接. 而且它是通過ThreadLocal實(shí)現(xiàn)的. 如果一個(gè)線程在執(zhí)行的過程使用了多個(gè)數(shù)據(jù)庫數(shù)據(jù)源, 那么一個(gè)數(shù)據(jù)源對應(yīng)一條數(shù)據(jù)庫連接的關(guān)系會被保存到ThreadLocal中, 保證線程在操作一個(gè)數(shù)據(jù)庫的時(shí)候只會使用一條相同的數(shù)據(jù)庫連接. 具體實(shí)現(xiàn)在?
org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
protected void doBegin(Object transaction, TransactionDefinition definition) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;Connection con = null;try {if (!txObject.hasConnectionHolder() ||txObject.getConnectionHolder().isSynchronizedWithTransaction()) {// 拿到事務(wù)管理器中設(shè)置的數(shù)據(jù)源,并根據(jù)這個(gè)數(shù)據(jù)源創(chuàng)建一個(gè)數(shù)據(jù)庫連接Connection newCon = obtainDataSource().getConnection();txObject.setConnectionHolder(new ConnectionHolder(newCon), true);}txObject.getConnectionHolder().setSynchronizedWithTransaction(true);con = txObject.getConnectionHolder().getConnection();Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);txObject.setPreviousIsolationLevel(previousIsolationLevel);txObject.setReadOnly(definition.isReadOnly());if (con.getAutoCommit()) {txObject.setMustRestoreAutoCommit(true);// 開啟事務(wù)con.setAutoCommit(false);}prepareTransactionalConnection(con, definition);txObject.getConnectionHolder().setTransactionActive(true);int timeout = determineTimeout(definition);if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {txObject.getConnectionHolder().setTimeoutInSeconds(timeout);}if (txObject.isNewConnectionHolder()) {// 將 dataSource -> connection 關(guān)系存到ThreadLocal中TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());}}}
總結(jié). Spring會將Service的代理對象放入容器中, 當(dāng)調(diào)用代理對象的方法時(shí), 首先會調(diào)用到事務(wù)攔截器TransactionInterceptor中,這個(gè)事務(wù)攔截器會拿到容器中的事務(wù)管理器, 事務(wù)管理器會根據(jù)設(shè)置的數(shù)據(jù)源, 創(chuàng)建一個(gè)數(shù)據(jù)庫連接, 并開啟事務(wù). 同時(shí)也會把數(shù)據(jù)源->數(shù)據(jù)庫連接保存到ThreadLocal.
接下來看Mybatis層面的代碼邏輯.

經(jīng)過層層調(diào)用, Mybatis也需要拿到數(shù)據(jù)庫連接,為接下來的操作數(shù)據(jù)庫. 那么它這個(gè)連接是怎么拿到的呢?

Mybatis本來是想從ThreadLocal中拿到一個(gè)數(shù)據(jù)庫連接的, 但是Mybatis持有的這個(gè)數(shù)據(jù)源在ThreadLocal中沒有對應(yīng)的數(shù)據(jù)庫連接, 而ThreadLocal中已存在的數(shù)據(jù)源是在事務(wù)管理器的時(shí)候放入的, 它們不是同一個(gè)數(shù)據(jù)源.
因此, Mybatis 需要根據(jù)自己拿到的數(shù)據(jù)源自己去創(chuàng)建一個(gè)數(shù)據(jù)庫連接了. 并把它也放到ThreadLocal中.

如上圖, 由于文章開頭, 在配置事務(wù)管理器和SqlSessionFactory時(shí),分別設(shè)置了不同的數(shù)據(jù)源, 最終就導(dǎo)致, 事務(wù)管理器開啟事務(wù)的時(shí)候, 使用的數(shù)據(jù)源A創(chuàng)建的一個(gè)數(shù)據(jù)庫連接. 而Mybatis在進(jìn)行實(shí)際操作數(shù)據(jù)庫的時(shí)候, 使用的數(shù)據(jù)源B創(chuàng)建的一個(gè)數(shù)據(jù)庫連接. 造成了開啟事務(wù)和進(jìn)行實(shí)際數(shù)據(jù)庫操作的連接不是同一個(gè)連接.
因此,在配置的時(shí)候,需要將SqlSessionFactory和事務(wù)管理器設(shè)置成相同的數(shù)據(jù)源.
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dataSource);return factoryBean.getObject();}public DataSourceTransactionManager druidTransactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
這樣的話, mybatis 根據(jù)數(shù)據(jù)源在拿取數(shù)據(jù)庫連接的時(shí)候, 發(fā)現(xiàn)ThreadLocal中已經(jīng)有對應(yīng)數(shù)據(jù)源的數(shù)據(jù)庫連接了, 因?yàn)樵谑聞?wù)管理器的時(shí)候, 由事務(wù)管理器已經(jīng)把數(shù)據(jù)源對應(yīng)的數(shù)據(jù)庫連接放入到ThreadLocal中了.
