背景
之前有文章提供了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兩者執行緒池互動出現泄露的情況放棄了,給小伙伴們避個坑
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/469480.html
標籤:其他
上一篇:Python特性
下一篇:拓撲排序
