背景 之前有文章提供了springboot多數據源動態註冊切換的整合方案,在後續使用過程中,發現在事務控制中有多種bug發生,決定對此問題進行分析與解決 前情提要 多數據源切換流程結構圖如下所示,包含幾個組成元素 自定義的數據源配置處理,通過DruidDataSource對象動態註冊到系統中 自定義 ...
背景
之前有文章提供了springboot多數據源動態註冊切換的整合方案,在後續使用過程中,發現在事務控制中有多種bug發生,決定對此問題進行分析與解決
前情提要
多數據源切換流程結構圖如下所示,包含幾個組成元素
-
自定義的數據源配置處理,通過DruidDataSource對象動態註冊到系統中
-
自定義數據源標識註解與切麵
-
數據源切換時的上下文線程變數持有者
-
自定義AbstractRoutingDataSource,實現數據源路由切換
問題分析
在Controller加入@Transitional註解後,數據源切換會失效,只會操作主庫,查詢資料後解決方案是將切麵的Order設置為-1使之執行順序在事務控制攔截之前,修改後證實有效,但是後續再次切換別的庫或者進行主庫操作無效,拿到的connection始終是第一次切換後的庫對應的連接
分析代碼後發現AbstractRoutingDataSource只負責提供getConnection這一層級,但是後續對connection的操作無法跟蹤,項目框架mybatis和jdbcTemplate混合使用,後續操作在spring層面對於事務/數據源/連接這三者的邏輯層面操作是相同的,jdbcTemplate代碼較為簡單,所以以此為切入點進一步分析
通過斷點調試會發現sql語句的執行最終會落到execute方法,方法中開始就是通過DataSourceUtils.getConnection獲取連接,這裡就是我們需要追蹤的地方,點進去發現跳轉到doGetConnection方法,這裡面就是我們需要分析的具體邏輯
第一行獲取的ConnectionHolder就是當前事務對應的線程持有對象,因為我們知道,事務的本質就是方法內部的sql執行時對應的是同一個資料庫connection,對於不同的嵌套業務方法,唯一相同的是當前線程ID一致,所以我們將connection與線程綁定就可以實現事務控制
點進getResource方法,發現dataSource是作為一個key去一個Map集合里取出對應的contextHolder
到這裡我們好像發現點什麼,之前對jdbcTemplatechu實例化設定數據源直接賦值自定義的DynamicDataSource,所以在事物中每次我們獲取connection依據就是DynamicDataSource這個對象作為key,所以每次都會一樣了!!
@Bean
public JdbcTemplate jdbcTemplate(){
JdbcTemplate jdbcTemplate = null;
try{
jdbcTemplate = new JdbcTemplate(dynamicDataSource());
}catch (Exception e){
e.printStackTrace();
}
return jdbcTemplate;
}
後續針對mybatis查找了相關資料,事務控制預設實現是SpringManagedTransaction,源碼查看後發現了熟悉的DataSourceUtils.getConnection,證明我們的分析方向是正確的
解決方案
jdbcTemplate
自定義操作類繼承jdbcTemplate重寫getDataSource,將我們獲取的DataSource這個對應的key指定到實際切換庫的數據源對象上即可
public class DynamicJdbcTemplate extends JdbcTemplate {
@Override
public DataSource getDataSource() {
DynamicDataSource router = (DynamicDataSource) super.getDataSource();
DataSource acuallyDataSource = router.getAcuallyDataSource();
return acuallyDataSource;
}
public DynamicJdbcTemplate(DataSource dataSource) {
super(dataSource);
}
}
public DataSource getAcuallyDataSource() {
Object lookupKey = determineCurrentLookupKey();
if (null == lookupKey) {
return this;
}
DataSource determineTargetDataSource = this.determineTargetDataSource();
return determineTargetDataSource == null ? this : determineTargetDataSource;
}
mybatis
自定義事務操作類,實現Transaction介面,替換TransitionFactory,這裡的實現與網上的解決方案略有不同,網上是定義三個變數,datasource(動態數據源對象)/connection(主連接)/connections(從庫連接),但是框架需要mybatis和jdbctemplate進行統一,mybatis是從connection層面控制,jdbctemplate是從datasource層面控制,所以全部使用鍵值對存儲
public class DynamicTransaction implements Transaction {
private final DynamicDataSource dynamicDataSource;
private ConcurrentHashMap<String, DataSource> dataSources;
private ConcurrentHashMap<String, Connection> connections;
private ConcurrentHashMap<String, Boolean> autoCommits;
private ConcurrentHashMap<String, Boolean> isConnectionTransactionals;
public DynamicTransaction(DataSource dataSource) {
this.dynamicDataSource = (DynamicDataSource) dataSource;
dataSources = new ConcurrentHashMap<>();
connections = new ConcurrentHashMap<>();
autoCommits = new ConcurrentHashMap<>();
isConnectionTransactionals = new ConcurrentHashMap<>();
}
public Connection getConnection() throws SQLException {
String dataBaseID = DBContextHolder.getDataSource();
if (!dataSources.containsKey(dataBaseID)) {
DataSource dataSource = dynamicDataSource.getAcuallyDataSource();
dataSources.put(dataBaseID, dataSource);
}
if (!connections.containsKey(dataBaseID)) {
Connection connection = DataSourceUtils.getConnection(dataSources.get(dataBaseID));
connections.put(dataBaseID, connection);
}
if (!autoCommits.containsKey(dataBaseID)) {
boolean autoCommit = connections.get(dataBaseID).getAutoCommit();
autoCommits.put(dataBaseID, autoCommit);
}
if (!isConnectionTransactionals.containsKey(dataBaseID)) {
boolean isConnectionTransactional = DataSourceUtils.isConnectionTransactional(connections.get(dataBaseID), dataSources.get(dataBaseID));
isConnectionTransactionals.put(dataBaseID, isConnectionTransactional);
}
return connections.get(dataBaseID);
}
public void commit() throws SQLException {
for (String dataBaseID : connections.keySet()) {
Connection connection = connections.get(dataBaseID);
boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
boolean autoCommit = autoCommits.get(dataBaseID);
if (connection != null && !isConnectionTransactional && !autoCommit) {
connection.commit();
}
}
}
public void rollback() throws SQLException {
for (String dataBaseID : connections.keySet()) {
Connection connection = connections.get(dataBaseID);
boolean isConnectionTransactional = isConnectionTransactionals.get(dataBaseID);
boolean autoCommit = autoCommits.get(dataBaseID);
if (connection != null && !isConnectionTransactional && !autoCommit) {
connection.rollback();
}
}
}
public void close() {
for (String dataBaseID : connections.keySet()) {
Connection connection = connections.get(dataBaseID);
DataSource dataSource = dataSources.get(dataBaseID);
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
public Integer getTimeout() {
return null;
}
}
public class DynamicTransactionFactory extends SpringManagedTransactionFactory {
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
return new DynamicTransaction(dataSource);
}
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
//SpringBootExecutableJarVFS.addImplClass(SpringBootVFS.class);
final PackagesSqlSessionFactoryBean sessionFactory = new PackagesSqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource());
sessionFactory.setTransactionFactory(new DynamicTransactionFactory());
sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mybatis/**/*Mapper.xml"));
//關閉駝峰轉換,防止帶下劃線的欄位無法映射
sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(false);
return sessionFactory.getObject();
}
事務管理器
事務中庫動態切換的問題解決了,但是只針對了主庫事務,如果從庫操作也需要事務的特性該如何操作呢,這裡就需要在註冊數據源時針對每個數據源手動註冊一個事務管理器
主庫是固定的,可以直接在配置Bean中聲明masterTransitionManage並設置為預設
@Bean("masterTransactionManager")
@Primary
public DataSourceTransactionManager MasterTransactionManager() {
return new DataSourceTransactionManager(masterDataSource());
}
從庫的事務管理器我們可以拿到dataSource初始化對象,然後向Spring容器註冊單例對象
public static void registerSingletonBean(String beanName, Object singletonObject) {
//將applicationContext轉換為ConfigurableApplicationContext
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) context;
//獲取BeanFactory
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getAutowireCapableBeanFactory();
if(configurableApplicationContext.containsBean(beanName)) {
defaultListableBeanFactory.destroySingleton(beanName);
}
//動態註冊bean.
defaultListableBeanFactory.registerSingleton(beanName, singletonObject);
}
SpringBootBeanUtil.registerSingletonBean(key + "TransactionManager", new DataSourceTransactionManager(druidDataSource));
在使用時只要對@Transitional註解指定transitionFactory名字即可
總結
解決這個問題花費了三天的時間,查了很多資料和解決方案,很多都是只有參考性或者特異性的,所以還是需把握問題的核心加上部分源碼的追蹤,比如本文中需要清晰的認識到Transition-Connection-LocalThread三者的關聯關係,才能找對排查的方向
後續實現了集成基於JMS(atomikos)的XA兩段式提交的全局事務,使用DruidXADataSrouce出現了druid和atomikos兩者線程池交互出現泄露的情況放棄了,給小伙伴們避個坑