Spring配置多数据源导致事物无法回滚问题

 更新时间:2024年01月31日 16:24:17   作者:Vincilovefang  
这篇文章主要介绍了Spring配置多数据源导致事物无法回滚问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

环境 

  • 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交互流程分为三部分(对应上图三种颜色):

1.测试启动,构建spring容器,并将applicationContext注入到TestContext,构造测试上下文容器

2.TestContextManager从spring容器中获取数据源事务管理器DataSourceTransactionManager(配置多数据源的时候,如果没有特别申明会注入默认的数据源)

3.spring-test手动开启一个事务,执行用户测试用例(事务操作参考Mybatis执行流程),spring-test手动关闭事务(根据TransactionInfo中记录的sql列表对事务中的数据库操作进行回滚,避免单测对数据库造成污染)

1.2简单的流程示意图

执行示意图

2.springTest配置多数据源导致事务无法回滚

在重构大迁移的背景下,我们初步在A工程接入了新老两个数据源(请不要吐槽一个工程里面配多个数据源,手动狗头)。

简单的示例如下:

新数据源配置–可略过不看

/**
 * 新数据源
 @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);
    }
}

旧数据源配置,这个地方必须将新老数据源中的一个指定为优先项,否则spring启动会报错。

为避免影响已有功能,这里暂时将旧数据源设为首选项

No qualifying bean of type 'javax.sql.DataSource' available: expected single matching bean but found 2: newDataSource,oldDataSource

/**
 * 旧数据源
 @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对应新数据源里面的数据表

@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默认事物回滚

但数据库里面数据并未回滚

数据库未回滚

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 = '{}'...

看到数据库里面的脏数据第一反应是懵逼的🙃,日志不会说谎,数据库脏数据也是存在的。

根据日志提示,追踪TransactionContext的源码,在springTest开始之前、之后,分别会执行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));
		}
	}

继续走查源码类时序图如图4

spring-test回滚

2.4.执行回滚–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);
        }
    }

发现在在DruidPooledConnectiontransactionInfo为空,事务信息为空,所以导致未真实回滚。

google了下transactionInfo为空的case,https://github.com/alibaba/druid/issues/1635,链接是druid论坛小伙伴的一些回答。

博主的答案有点概括,看了之后也不是太明白(只能怪自己bug写多了,人变傻了,理解能力也变差了,再次手动狗头)

2.5.transactionInfo

设置transactionInfo的地方只有一处,即通过connection执行sql的时候会对事务进行记录。

###	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属性被设置成了true,connection如下。

事务conn

而在TransactionContext开启事务的时候connection如下:

transactionContext

一个为DruidPooledConnection@12036,一个为DruidPooledConnection@11838,两个DruidPooledConnection不同,所以springTest的环绕切面无法对事务进行回滚。

2.6.connection创建

现在的问题是为什么TransactionContext.startTransaction中的conn和单测执行中的conn不是一个。

接下来要做的是确定在TransactionContext和单测中,connection分别是怎么创建的。

TransactionContext.startTransaction获取connection流程如下

conn

单测中,通过代码执行栈信息分析代码逻辑执行的时候是如何获取DruidPooledConnection,这里的主要执行流程即为Mybatis执行时序图

mybatis执行流

其中mybatis中mapperProxy中记录了每个sql执行对应的数据源信息,从而找到对应的数据源进行数据库操作。

根据debug信息栈发现,在SqlSessionTemplate中没有Connection信息,但是在SqlSessionInterceptor中已经存在了(debug图中标红圈部分)

debug

根据栈信息能看出connection由SpringManagedTransaction持有,继续跟踪SpringManagedTransaction源码查看connection的创建

### 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在新数据源中,这里的this.dataSource为新数据源(mybatis的源头mapperProxy会记录每条sql需要的数据源),进一步跟踪源码我们找到是通过

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发现resources这个里面的记录的是旧数据源信息,所以返回connection为空,便新创建了一个Connection。

到这里我们基本清楚了,TransactionContext用的是旧数据源创建的连接(spring依赖注入优先注入了旧数据源),而单测中用的是新数据源创建的连接,所以TransactionContext无法对单测进行回滚。

resources的初次设置代码如下

resource

DataSourceTransactionManager设置了datasource信息,聪明的你可能马上想到,DataSourceTransactionManager是我们自己在代码中配置的。

我们把OldDataSourceTransactionManager的优先级设置成了@Primary这才导致TransactionContext用的是OldDataSourceTransactionManager来管理事务。

现在我们只需要把TransactionContext的事务管理器设置成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());
    }
} 

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 详解spring boot配置单点登录

    详解spring boot配置单点登录

    本篇文章主要介绍了详解spring boot配置单点登录,常用的安全框架有spring security和apache shiro。shiro的配置和使用相对简单,本文使用shrio对接CAS服务。
    2017-03-03
  • Java集合系列之ArrayList源码分析

    Java集合系列之ArrayList源码分析

    这篇文章主要为大家详细介绍了Java集合系列之ArrayList源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-02-02
  • Spring Boot的FailureAnalyzer机制及如何解救应用启动危机

    Spring Boot的FailureAnalyzer机制及如何解救应用启动危机

    本文探讨了FailureAnalyzer工具,它不仅能帮助我们快速识别和处理代码中的错误,还能极大地提升我们的开发效率,通过详细的实例分析,我们了解了FailureAnalyzer如何通过自定义逻辑应对不同类型的异常,让程序员能够更好地定位问题并迅速找到解决方案,感兴趣的朋友一起看看吧
    2025-01-01
  • SpringBoot如何集成Kaptcha验证码

    SpringBoot如何集成Kaptcha验证码

    本文介绍了如何在Java开发中使用Kaptcha生成验证码的功能,包括在pom.xml中配置依赖、在系统公共配置类中添加配置、在控制器中添加生成验证码的方法,以及前端页面如何引用,同时,还补充了Kaptcha的更多配置属性及其默认值
    2025-01-01
  • Spring-Boot中如何使用多线程处理任务方法

    Spring-Boot中如何使用多线程处理任务方法

    这篇文章主要介绍了Spring-Boot中如何使用多线程处理任务方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • 线程池运用不当引发的一次线上事故解决记录分析

    线程池运用不当引发的一次线上事故解决记录分析

    遇到了一个比较典型的线上问题,刚好和线程池有关,另外涉及到死锁、jstack命令的使用、JDK不同线程池的适合场景等知识点,同时整个调查思路可以借鉴,特此记录和分享一下
    2024-01-01
  • spring集成okhttp3的步骤详解

    spring集成okhttp3的步骤详解

    okhttp是一个封装URL,比HttpClient更友好易用的工具,下面这篇文章主要给大家介绍了关于spring集成okhttp3的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或工作具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2018-04-04
  • JavaWeb中过滤器Filter的用法详解

    JavaWeb中过滤器Filter的用法详解

    过滤器通常对一些web资源进行拦截,做完一些处理器再交给下一个过滤器处理,直到所有的过滤器处理器,再调用servlet实例的service方法进行处理。本文将通过示例为大家讲解JavaWeb中过滤器Filter的用法与实现,需要的可以参考一下
    2022-08-08
  • Java实现用位运算维护状态码

    Java实现用位运算维护状态码

    位运算是一种非常高效的运算方式,在算法考察中比较常见,那么业务代码中我们如何使用位运算呢,感兴趣的小伙伴快跟随小编一起学习一下吧
    2024-03-03
  • Java 多线程并发LockSupport

    Java 多线程并发LockSupport

    这篇文章主要介绍了Java 多线程并发LockSupport,LockSupport 类是用于创建锁和其他同步类的基本线程阻塞原语,更多相关内容需要得小伙伴可以参考一下下面文章内容
    2022-06-06

最新评论