Spring配置多數(shù)據(jù)源導(dǎo)致事物無法回滾問題
環(huán)境
- spring 4.3.13
- Druid 鏈接池1.1.0
- mysql 5.1.41
- mybatis 3.4.6
1.spring-test簡介
1.1spring-test類圖

整個(gè)spring-test交互流程分為三部分(對應(yīng)上圖三種顏色):
1.測試啟動(dòng),構(gòu)建spring容器,并將applicationContext注入到TestContext,構(gòu)造測試上下文容器
2.TestContextManager從spring容器中獲取數(shù)據(jù)源事務(wù)管理器DataSourceTransactionManager(配置多數(shù)據(jù)源的時(shí)候,如果沒有特別申明會(huì)注入默認(rèn)的數(shù)據(jù)源)
3.spring-test手動(dòng)開啟一個(gè)事務(wù),執(zhí)行用戶測試用例(事務(wù)操作參考Mybatis執(zhí)行流程),spring-test手動(dòng)關(guān)閉事務(wù)(根據(jù)TransactionInfo中記錄的sql列表對事務(wù)中的數(shù)據(jù)庫操作進(jìn)行回滾,避免單測對數(shù)據(jù)庫造成污染)
1.2簡單的流程示意圖

2.springTest配置多數(shù)據(jù)源導(dǎo)致事務(wù)無法回滾
在重構(gòu)大遷移的背景下,我們初步在A工程接入了新老兩個(gè)數(shù)據(jù)源(請不要吐槽一個(gè)工程里面配多個(gè)數(shù)據(jù)源,手動(dòng)狗頭)。
簡單的示例如下:
新數(shù)據(jù)源配置–可略過不看
/**
* 新數(shù)據(jù)源
@author vincilovfang
*/
@Configuration
@MapperScan(basePackages = "com.spring.test", sqlSessionTemplateRef = "newSqlSessionTemplate")
public class NewDataSourceConfig {
@Value("${newJdbc.url}")
private String url;
@Value("${newJdbc.username}")
private String username;
@Value("${newJdbc.password}")
private String password;
@Bean(name = "newDataSource")
public DataSource buildDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
// set其他屬性
return dataSource;
}
@Bean(name = "newSqlSessionFactory")
public SqlSessionFactory buildSqlSessionFactory(
@Qualifier("newDataSource") DataSource dataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//...set其他屬性
}
@Bean(name = "newSqlSessionTemplate")
public SqlSessionTemplate buildSqlSessionTemplate(
@Qualifier("newSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "newTransactionManager")
public DataSourceTransactionManager buildTransactionManager(
@Qualifier("newDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
舊數(shù)據(jù)源配置,這個(gè)地方必須將新老數(shù)據(jù)源中的一個(gè)指定為優(yōu)先項(xiàng),否則spring啟動(dòng)會(huì)報(bào)錯(cuò)。
為避免影響已有功能,這里暫時(shí)將舊數(shù)據(jù)源設(shè)為首選項(xiàng)
No qualifying bean of type 'javax.sql.DataSource' available: expected single matching bean but found 2: newDataSource,oldDataSource
/**
* 舊數(shù)據(jù)源
@author vincilovfang
*/
@Configuration
@MapperScan(basePackages = "com.spring.test", sqlSessionTemplateRef = "oldSqlSessionTemplate")
public class OldDataSourceConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean(name = "oldDataSource")
@Primary
public DataSource buildDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
//... set其他屬性
return dataSource;
}
@Bean(name = "oldSqlSessionFactory")
@Primary
public SqlSessionFactory buildSqlSessionFactory(@Qualifier("oldDataSource") DataSource dataSource) {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//...set其他屬性
}
@Bean(name = "oldSqlSessionTemplate")
@Primary
public SqlSessionTemplate buildSqlSessionTemplate(
@Qualifier("oldSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean(name = "oldTransactionManager")
@Primary
public DataSourceTransactionManager buildTransactionManager(
@Qualifier("oldDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
單測示例–DemoDO對應(yīng)新數(shù)據(jù)源里面的數(shù)據(jù)表
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringBootStarter.class)
public class NoRollbackDemoTest extends MockitoTimorTestBase {
@Resource
private DemoDOMapper demoDOMapper;
@Test
public void testDemo() {
DemoDO demoDO = createData(DemoDO.class);
demoDO.setCpId(2341233453L);
demoDOMapper.insertDemo(demoDO);
Demo demo = demoRepository.getDemo(2341233453L);
Assert.assertEquals(demoDO.getCpId(), demo.getCpId());
}
}
2.1.springTest默認(rèn)事物回滾
但數(shù)據(jù)庫里面數(shù)據(jù)并未回滾

2.2.跟蹤日志也顯示回滾
[main:TransactionContext.java:139] _am||traceid=||spanid=||Rolled back transaction for test context [DefaultTestContext@143640d5 testClass = NoRollbackDemoTest, testInstance = com.spring.test.xxx.infrastructure.persistence.NoRollbackDemoTest@6d0fe80c, testMethod = testNoRollbackCase@NoRollbackDemoTest, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@6295d394 testClass = NoRollbackDemoTest, locations = '{}'...
看到數(shù)據(jù)庫里面的臟數(shù)據(jù)第一反應(yīng)是懵逼的??,日志不會(huì)說謊,數(shù)據(jù)庫臟數(shù)據(jù)也是存在的。
根據(jù)日志提示,追蹤TransactionContext的源碼,在springTest開始之前、之后,分別會(huì)執(zhí)行startTransaction、endTransaction
2.3.開啟回滾–TransactionContext
### TransactionContext
void startTransaction() {
if (this.transactionStatus != null) {
throw new IllegalStateException(
"Cannot start a new transaction without ending the existing transaction first.");
}
this.flaggedForRollback = this.defaultRollback;
this.transactionStatus = this.transactionManager.getTransaction(this.transactionDefinition);
++this.transactionsStarted;
if (logger.isInfoEnabled()) {
logger.info(String.format(
"Began transaction (%s) for test context %s; transaction manager [%s]; rollback [%s]",
this.transactionsStarted, this.testContext, this.transactionManager, flaggedForRollback));
}
}
void endTransaction() {
if (logger.isTraceEnabled()) {
logger.trace(String.format(
"Ending transaction for test context %s; transaction status [%s]; rollback [%s]",
this.testContext, this.transactionStatus, this.flaggedForRollback));
}
if (this.transactionStatus == null) {
throw new IllegalStateException(String.format(
"Failed to end transaction for test context %s: transaction does not exist.", this.testContext));
}
try {
if (this.flaggedForRollback) {
this.transactionManager.rollback(this.transactionStatus);
}
else {
this.transactionManager.commit(this.transactionStatus);
}
}
finally {
this.transactionStatus = null;
}
if (logger.isInfoEnabled()) {
logger.info(String.format("%s transaction for test context %s.",
(this.flaggedForRollback ? "Rolled back" : "Committed"), this.testContext));
}
}
繼續(xù)走查源碼類時(shí)序圖如圖4

2.4.執(zhí)行回滾–DruidPooledConnection
### DruidPooledConnection
public void rollback() throws SQLException {
if (transactionInfo == null) {
return;
}
if (holder == null) {
return;
}
DruidAbstractDataSource dataSource = holder.getDataSource();
dataSource.incrementRollbackCount();
try {
conn.rollback();
} catch (SQLException ex) {
handleException(ex);
} finally {
handleEndTransaction(dataSource, null);
}
}
發(fā)現(xiàn)在在DruidPooledConnection中 transactionInfo為空,事務(wù)信息為空,所以導(dǎo)致未真實(shí)回滾。
google了下transactionInfo為空的case,https://github.com/alibaba/druid/issues/1635,鏈接是druid論壇小伙伴的一些回答。
博主的答案有點(diǎn)概括,看了之后也不是太明白(只能怪自己bug寫多了,人變傻了,理解能力也變差了,再次手動(dòng)狗頭)
2.5.transactionInfo
設(shè)置transactionInfo的地方只有一處,即通過connection執(zhí)行sql的時(shí)候會(huì)對事務(wù)進(jìn)行記錄。
### DruidPooledConnection
protected void transactionRecord(String sql) throws SQLException {
if (transactionInfo == null && (!conn.getAutoCommit())) {
DruidAbstractDataSource dataSource = holder.getDataSource();
dataSource.incrementStartTransactionCount();
transactionInfo = new TransactionInfo(dataSource.createTransactionId());
}
if (transactionInfo != null) {
List<String> sqlList = transactionInfo.getSqlList();
if (sqlList.size() < MAX_RECORD_SQL_COUNT) {
sqlList.add(sql);
}
}
}
代碼中conn的autoCommit屬性被設(shè)置成了true,connection如下。

而在TransactionContext開啟事務(wù)的時(shí)候connection如下:

一個(gè)為DruidPooledConnection@12036,一個(gè)為DruidPooledConnection@11838,兩個(gè)DruidPooledConnection不同,所以springTest的環(huán)繞切面無法對事務(wù)進(jìn)行回滾。
2.6.connection創(chuàng)建
現(xiàn)在的問題是為什么TransactionContext.startTransaction中的conn和單測執(zhí)行中的conn不是一個(gè)。
接下來要做的是確定在TransactionContext和單測中,connection分別是怎么創(chuàng)建的。
TransactionContext.startTransaction獲取connection流程如下

單測中,通過代碼執(zhí)行棧信息分析代碼邏輯執(zhí)行的時(shí)候是如何獲取DruidPooledConnection,這里的主要執(zhí)行流程即為Mybatis執(zhí)行時(shí)序圖

其中mybatis中mapperProxy中記錄了每個(gè)sql執(zhí)行對應(yīng)的數(shù)據(jù)源信息,從而找到對應(yīng)的數(shù)據(jù)源進(jìn)行數(shù)據(jù)庫操作。
根據(jù)debug信息棧發(fā)現(xiàn),在SqlSessionTemplate中沒有Connection信息,但是在SqlSessionInterceptor中已經(jīng)存在了(debug圖中標(biāo)紅圈部分)

根據(jù)棧信息能看出connection由SpringManagedTransaction持有,繼續(xù)跟蹤SpringManagedTransaction源碼查看connection的創(chuàng)建
### SpringManagedTransaction
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"JDBC Connection ["
+ this.connection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}
connetciton是通過dataSource獲取的,由于單測的DemoDO在新數(shù)據(jù)源中,這里的this.dataSource為新數(shù)據(jù)源(mybatis的源頭mapperProxy會(huì)記錄每條sql需要的數(shù)據(jù)源),進(jìn)一步跟蹤源碼我們找到是通過
TransactionSynchronizationManager里面的resource獲取connectionHolder
### TransactionSynchronizationManager
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
private static Object doGetResource(Object actualKey) {
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
debug發(fā)現(xiàn)resources這個(gè)里面的記錄的是舊數(shù)據(jù)源信息,所以返回connection為空,便新創(chuàng)建了一個(gè)Connection。
到這里我們基本清楚了,TransactionContext用的是舊數(shù)據(jù)源創(chuàng)建的連接(spring依賴注入優(yōu)先注入了舊數(shù)據(jù)源),而單測中用的是新數(shù)據(jù)源創(chuàng)建的連接,所以TransactionContext無法對單測進(jìn)行回滾。
resources的初次設(shè)置代碼如下

DataSourceTransactionManager設(shè)置了datasource信息,聰明的你可能馬上想到,DataSourceTransactionManager是我們自己在代碼中配置的。
我們把OldDataSourceTransactionManager的優(yōu)先級設(shè)置成了@Primary這才導(dǎo)致TransactionContext用的是OldDataSourceTransactionManager來管理事務(wù)。
現(xiàn)在我們只需要把TransactionContext的事務(wù)管理器設(shè)置成NewDataSourceTransactionManager即可。
2.7.最終的單測代碼
如下
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringBootStarter.class)
@Transactional(transactionManager = "newDataSourceTransactionManager")
public class NoRollbackDemoTest extends MockitoTimorTestBase {
@Resource
private DemoDOMapper demoDOMapper;
@Test
public void testDemo() {
DemoDO demoDO = createData(DemoDO.class);
demoDO.setCpId(2341233453L);
demoDOMapper.insertDemo(demoDO);
Demo demo = demoRepository.getDemo(2341233453L);
Assert.assertEquals(demoDO.getCpId(), demo.getCpId());
}
}
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Boot的FailureAnalyzer機(jī)制及如何解救應(yīng)用啟動(dòng)危機(jī)
本文探討了FailureAnalyzer工具,它不僅能幫助我們快速識別和處理代碼中的錯(cuò)誤,還能極大地提升我們的開發(fā)效率,通過詳細(xì)的實(shí)例分析,我們了解了FailureAnalyzer如何通過自定義邏輯應(yīng)對不同類型的異常,讓程序員能夠更好地定位問題并迅速找到解決方案,感興趣的朋友一起看看吧2025-01-01
線程池運(yùn)用不當(dāng)引發(fā)的一次線上事故解決記錄分析
遇到了一個(gè)比較典型的線上問題,剛好和線程池有關(guān),另外涉及到死鎖、jstack命令的使用、JDK不同線程池的適合場景等知識點(diǎn),同時(shí)整個(gè)調(diào)查思路可以借鑒,特此記錄和分享一下2024-01-01
Java實(shí)現(xiàn)用位運(yùn)算維護(hù)狀態(tài)碼
位運(yùn)算是一種非常高效的運(yùn)算方式,在算法考察中比較常見,那么業(yè)務(wù)代碼中我們?nèi)绾问褂梦贿\(yùn)算呢,感興趣的小伙伴快跟隨小編一起學(xué)習(xí)一下吧2024-03-03

