欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring配置多數(shù)據(jù)源導(dǎo)致事物無法回滾問題

 更新時間:2024年01月31日 16:24:17   作者:Vincilovefang  
這篇文章主要介紹了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類圖

spring-test類時序圖

整個spring-test交互流程分為三部分(對應(yīng)上圖三種顏色):

1.測試啟動,構(gòu)建spring容器,并將applicationContext注入到TestContext,構(gòu)造測試上下文容器

2.TestContextManager從spring容器中獲取數(shù)據(jù)源事務(wù)管理器DataSourceTransactionManager(配置多數(shù)據(jù)源的時候,如果沒有特別申明會注入默認(rèn)的數(shù)據(jù)源)

3.spring-test手動開啟一個事務(wù),執(zhí)行用戶測試用例(事務(wù)操作參考Mybatis執(zhí)行流程),spring-test手動關(guān)閉事務(wù)(根據(jù)TransactionInfo中記錄的sql列表對事務(wù)中的數(shù)據(jù)庫操作進(jìn)行回滾,避免單測對數(shù)據(jù)庫造成污染)

1.2簡單的流程示意圖

執(zhí)行示意圖

2.springTest配置多數(shù)據(jù)源導(dǎo)致事務(wù)無法回滾

在重構(gòu)大遷移的背景下,我們初步在A工程接入了新老兩個數(shù)據(jù)源(請不要吐槽一個工程里面配多個數(shù)據(jù)源,手動狗頭)。

簡單的示例如下:

新數(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ù)源配置,這個地方必須將新老數(shù)據(jù)源中的一個指定為優(yōu)先項(xiàng),否則spring啟動會報錯。

為避免影響已有功能,這里暫時將舊數(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ù)并未回滾

數(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)是懵逼的??,日志不會說謊,數(shù)據(jù)庫臟數(shù)據(jù)也是存在的。

根據(jù)日志提示,追蹤TransactionContext的源碼,在springTest開始之前、之后,分別會執(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ù)走查源碼類時序圖如圖4

spring-test回滾

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)在在DruidPooledConnectiontransactionInfo為空,事務(wù)信息為空,所以導(dǎo)致未真實(shí)回滾。

google了下transactionInfo為空的case,https://github.com/alibaba/druid/issues/1635,鏈接是druid論壇小伙伴的一些回答。

博主的答案有點(diǎn)概括,看了之后也不是太明白(只能怪自己bug寫多了,人變傻了,理解能力也變差了,再次手動狗頭)

2.5.transactionInfo

設(shè)置transactionInfo的地方只有一處,即通過connection執(zhí)行sql的時候會對事務(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如下。

事務(wù)conn

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

transactionContext

一個為DruidPooledConnection@12036,一個為DruidPooledConnection@11838,兩個DruidPooledConnection不同,所以springTest的環(huán)繞切面無法對事務(wù)進(jìn)行回滾。

2.6.connection創(chuàng)建

現(xiàn)在的問題是為什么TransactionContext.startTransaction中的conn和單測執(zhí)行中的conn不是一個。

接下來要做的是確定在TransactionContext和單測中,connection分別是怎么創(chuàng)建的。

TransactionContext.startTransaction獲取connection流程如下

conn

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

mybatis執(zhí)行流

其中mybatis中mapperProxy中記錄了每個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)紅圈部分)

debug

根據(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會記錄每條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這個里面的記錄的是舊數(shù)據(jù)源信息,所以返回connection為空,便新創(chuàng)建了一個Connection。

到這里我們基本清楚了,TransactionContext用的是舊數(shù)據(jù)源創(chuàng)建的連接(spring依賴注入優(yōu)先注入了舊數(shù)據(jù)源),而單測中用的是新數(shù)據(jù)源創(chuàng)建的連接,所以TransactionContext無法對單測進(jìn)行回滾。

resources的初次設(shè)置代碼如下

resource

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é)

以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 詳解spring boot配置單點(diǎn)登錄

    詳解spring boot配置單點(diǎn)登錄

    本篇文章主要介紹了詳解spring boot配置單點(diǎn)登錄,常用的安全框架有spring security和apache shiro。shiro的配置和使用相對簡單,本文使用shrio對接CAS服務(wù)。
    2017-03-03
  • Java集合系列之ArrayList源碼分析

    Java集合系列之ArrayList源碼分析

    這篇文章主要為大家詳細(xì)介紹了Java集合系列之ArrayList源碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-02-02
  • Spring Boot的FailureAnalyzer機(jī)制及如何解救應(yīng)用啟動危機(jī)

    Spring Boot的FailureAnalyzer機(jī)制及如何解救應(yīng)用啟動危機(jī)

    本文探討了FailureAnalyzer工具,它不僅能幫助我們快速識別和處理代碼中的錯誤,還能極大地提升我們的開發(fā)效率,通過詳細(xì)的實(shí)例分析,我們了解了FailureAnalyzer如何通過自定義邏輯應(yīng)對不同類型的異常,讓程序員能夠更好地定位問題并迅速找到解決方案,感興趣的朋友一起看看吧
    2025-01-01
  • SpringBoot如何集成Kaptcha驗(yàn)證碼

    SpringBoot如何集成Kaptcha驗(yàn)證碼

    本文介紹了如何在Java開發(fā)中使用Kaptcha生成驗(yàn)證碼的功能,包括在pom.xml中配置依賴、在系統(tǒng)公共配置類中添加配置、在控制器中添加生成驗(yàn)證碼的方法,以及前端頁面如何引用,同時,還補(bǔ)充了Kaptcha的更多配置屬性及其默認(rèn)值
    2025-01-01
  • Spring-Boot中如何使用多線程處理任務(wù)方法

    Spring-Boot中如何使用多線程處理任務(wù)方法

    這篇文章主要介紹了Spring-Boot中如何使用多線程處理任務(wù)方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2019-01-01
  • 線程池運(yùn)用不當(dāng)引發(fā)的一次線上事故解決記錄分析

    線程池運(yùn)用不當(dāng)引發(fā)的一次線上事故解決記錄分析

    遇到了一個比較典型的線上問題,剛好和線程池有關(guān),另外涉及到死鎖、jstack命令的使用、JDK不同線程池的適合場景等知識點(diǎn),同時整個調(diào)查思路可以借鑒,特此記錄和分享一下
    2024-01-01
  • spring集成okhttp3的步驟詳解

    spring集成okhttp3的步驟詳解

    okhttp是一個封裝URL,比HttpClient更友好易用的工具,下面這篇文章主要給大家介紹了關(guān)于spring集成okhttp3的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。
    2018-04-04
  • JavaWeb中過濾器Filter的用法詳解

    JavaWeb中過濾器Filter的用法詳解

    過濾器通常對一些web資源進(jìn)行攔截,做完一些處理器再交給下一個過濾器處理,直到所有的過濾器處理器,再調(diào)用servlet實(shí)例的service方法進(jìn)行處理。本文將通過示例為大家講解JavaWeb中過濾器Filter的用法與實(shí)現(xiàn),需要的可以參考一下
    2022-08-08
  • Java實(shí)現(xiàn)用位運(yùn)算維護(hù)狀態(tài)碼

    Java實(shí)現(xiàn)用位運(yùn)算維護(hù)狀態(tài)碼

    位運(yùn)算是一種非常高效的運(yùn)算方式,在算法考察中比較常見,那么業(yè)務(wù)代碼中我們?nèi)绾问褂梦贿\(yùn)算呢,感興趣的小伙伴快跟隨小編一起學(xué)習(xí)一下吧
    2024-03-03
  • Java?多線程并發(fā)LockSupport

    Java?多線程并發(fā)LockSupport

    這篇文章主要介紹了Java?多線程并發(fā)LockSupport,LockSupport?類是用于創(chuàng)建鎖和其他同步類的基本線程阻塞原語,更多相關(guān)內(nèi)容需要得小伙伴可以參考一下下面文章內(nèi)容
    2022-06-06

最新評論