最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

  • <pre id="ij4x2"></pre>

    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ù)源的時候,如果沒有特別申明會注入默認的數(shù)據(jù)源)

    3.spring-test手動開啟一個事務(wù),執(zhí)行用戶測試用例(事務(wù)操作參考Mybatis執(zhí)行流程),spring-test手動關(guān)閉事務(wù)(根據(jù)TransactionInfo中記錄的sql列表對事務(wù)中的數(shù)據(jù)庫操作進行回滾,避免單測對數(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)先項,否則spring啟動會報錯。

    為避免影響已有功能,這里暫時將舊數(shù)據(jù)源設(shè)為首選項

    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默認事物回滾

    但數(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)致未真實回滾。

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

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

    2.5.transactionInfo

    設(shè)置transactionInfo的地方只有一處,即通過connection執(zhí)行sql的時候會對事務(wù)進行記錄。

    ###	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ù)進行回滾。

    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ù)源進行數(shù)據(jù)庫操作。

    根據(jù)debug信息棧發(fā)現(xiàn),在SqlSessionTemplate中沒有Connection信息,但是在SqlSessionInterceptor中已經(jīng)存在了(debug圖中標紅圈部分)

    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ù)源),進一步跟蹤源碼我們找到是通過

    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無法對單測進行回滾。

    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)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

    相關(guān)文章

    最新評論

    梅州市| 澄迈县| 大连市| 商丘市| 上饶县| 迭部县| 西乌珠穆沁旗| 塘沽区| 额尔古纳市| 锦屏县| 张掖市| 新建县| 寻甸| 九龙城区| 台州市| 报价| 萨嘎县| 陆丰市| 萨嘎县| 通州区| 西平县| 蒙城县| 岳池县| 黑河市| 新郑市| 湘乡市| 巩留县| 遵义市| 英山县| 射阳县| 海宁市| 新疆| 婺源县| 平江县| 大石桥市| 玉门市| 巧家县| 衡东县| 丹凤县| 凭祥市| 三河市|