背景:

在有标注为@Transactional的类或公共方法中(传播特性,如:NOT_SUPPORTED、SUPPORTS、REQUIRED【默认值】、REQUIRES_NEW)执行数据源切换可能不成功(比如:主从数据源切换,多数据源切换等,均会发现切换不成功,或“偶尔又切换成功”),导致本应该需要查主库却查了从库,本应该查B库却仍查了A库导致表不存在等各种查询问题。

原因是什么呢?

本质原因是:因为只要添加了@Transactional (传播特性,如:NOT_SUPPORTED、SUPPORTS、REQUIRED【默认值】、REQUIRES_NEW),在事务同步上下文类型为:SYNCHRONIZATION_ALWAYS时 ,那么会在事务切面中进行初始化事务同步上下文状态【prepareTransactionStatus】(具体可分析代码位置:org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction),此时org.springframework.transaction.support.TransactionSynchronizationManager#isSynchronizationActive 是true,若需要事务时(EQUIRED【默认值】、REQUIRES_NEW)则还会org.springframework.transaction.support.AbstractPlatformTransactionManager#doBegin获取connection并开启事务且构建ConnectionHolder注册保存于事务同步上下文中,当mybatis 的SqlSessionTemplate.SqlSessionInterceptor.invoke执行时,第一次会将获取的SqlSession通过SqlSessionUtils.registerSessionHolder注册保存于事务同步上下文中,后续只要是同一个SqlSession,那么间接的就是持有同一个SpringManagedTransaction,SpringManagedTransaction是优先从ConnectionHolder获取已有connection对象,若不存在才会创建新的connection对象,并构建ConnectionHolder注册保存于事务同步上下文中,后续只要是在同一个事务同步上下文中,那么都是复用相同的SqlSession、SpringManagedTransaction、ConnectionHolder,所以单纯的改DataSource(ThreadLocal的线程变量)没有用,因为此时ConnectionHolder中保存的是Connection,而不是DataSource

Spring声明式事务源代码分析流程图

为何偶尔切换数据源成功?

当为事务传播特性为NOT_SUPPORTED、SUPPORTS时,由于此时事务管理器并不会提前打开Conneciton并开启事务(即:也不会保存到ConnectionHolder)【从上图中就可以看出】,而是在执行一条SQL语句时,触发了MyBatis的第一次获取SqlSession,间接的执行了DataSourceUtils.doGetConnection(会保存到ConnectionHolder中),如果在方法中的执行第一条SQL语句前进行数据源切换,那么就可以生效,若在执行第一条SQL语句后再尝试切换,那么由于SqlSession已不是最新的(ConnectionHolder中已有Connection),则只会复用。

解决方案:

新增数据源切换执行器工具类:DataSourceSwitchInvoker,作用:在执行前会检查要切换的数据源与当前已持有的数据源(ConnectionHolder.Connection)是否一致,一致则直接执行回调方法(即:不存在切换数据源),不一致则挂起当前事务(挂事务与资源后,会清空事务同步上下文,就像从来没有执行过事务方法一样,默认状态),然后执行回调方法,最后恢复被挂起的事务与资源,并恢复回执行前的数据源设置。即:相当于在事务执行过程中,撕开一个口子(无任何状态),执行完成后,再恢复回事务的原状态,不影响后续的执行。

DataSourceSwitchInvoker.invokeOn 代码逻辑流程图:

(注:图片部份位置有屏蔽删减是因为我实现了多个版本,本次是简化实用版,无需复杂的设置,直接方法入参传入即可)

DataSourceSwitchInvoker 实现CODE:


/**
* @author: zuowenjun
* @description:数据源切换后执行器,解决在多数据源项目中,无法在事务方法中进行数据源切换问题
*/
@Component
public class DataSourceSwitchInvoker { private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceSwitchInvoker.class); private static final Map<String, String> DATA_SOURCE_NAME_WITH_URL_MAP = new HashMap<>(); private static final String SET_BEFORE = "BEFORE";
private static final String SET_AFTER = "AFTER"; @Value("${dataSourceSwitchInvoker.settings.datasourceJdbcUrlPattern:}")
private String datasourceJdbcUrlPattern; /**
* 初始化必要条件:数据源配置集合(数据源名称与jdbcUrl对应关系)
*/
@PostConstruct
public void initializeRequirement() {
if (StringUtils.isBlank(datasourceJdbcUrlPattern)) {
LOGGER.warn("datasourceJdbcUrlPattern is null");
return;
} DATA_SOURCE_NAME_WITH_URL_MAP.clear(); Map<String, String> configMap = getPropertiesByPattern(datasourceJdbcUrlPattern, value -> ObjectUtils.defaultIfNull(value, "").toString().trim(), (k, v) -> StringUtils.isNotEmpty(v)); if (MapUtils.isEmpty(configMap)) {
LOGGER.error("DataSourceSwitchInvoker.initializeRequirement configMap is empty ,datasourceJdbcUrlPattern: {}", datasourceJdbcUrlPattern);
return;
} DATA_SOURCE_NAME_WITH_URL_MAP.putAll(configMap); LOGGER.info("DataSourceSwitchInvoker.initializeRequirement ok");
} /**
* 在指定的数据源下执行回调方法
*
* @param getCurrentDsNameFunc
* @param setCurrentDsNameFunc
* @param invokeCallback
* @return
*/
public static <T> T invokeOn(String newDataSourceName, Supplier<String> getCurrentDsNameFunc, Consumer<String> setCurrentDsNameFunc, BiFunction<String, String, Boolean> checkSameDsNameFunc, Supplier<T> invokeCallback) {
Assert.notNull(getCurrentDsNameFunc, "执行前获取数据源配置回调方法不能为空");
Assert.notNull(setCurrentDsNameFunc, "执行前要设置的数据源配置回调方法不能为空");
Assert.notNull(invokeCallback, "具体执行回调方法不能为空"); String invokeId = "DSI" + System.currentTimeMillis(); String oldDataSourceName = getCurrentDsNameFunc.get(); setCurrentDsNameFunc.accept(newDataSourceName); LOGGER.info("DataSourceSwitchInvoker.invokeOn setCurrentDsName {} --> {} ,invokeId: {}", oldDataSourceName, newDataSourceName, invokeId); Object currentTransaction = null;
Object suspendedResourcesHolder = null;
PlatformTransactionManagerDelegateInner platformTransactionManagerDelegate = null;
try { String currentDbConnectionUrl = TransactionManagerUtils.getCurrentDbConnectionUrl(null); if (StringUtils.isEmpty(currentDbConnectionUrl) || currentDbConnectionUrl.equalsIgnoreCase(DATA_SOURCE_NAME_WITH_URL_MAP.get(newDataSourceName))) {
//若当前没有持有DB连接 或持有的DB连接与当前要设置的DB数据源相同,则表明无需额外处理,只需正常执行即可
return invokeCallback.get();
} else if (StringUtils.isNotEmpty(currentDbConnectionUrl) && checkSameDsNameFunc != null) {
String currentUsedDataSourceName = DATA_SOURCE_NAME_WITH_URL_MAP.entrySet().stream().filter(kv -> currentDbConnectionUrl.equalsIgnoreCase(kv.getValue())).map(Map.Entry::getKey).findFirst().orElse(null);
if (Boolean.TRUE.equals(checkSameDsNameFunc.apply(currentUsedDataSourceName, newDataSourceName))) {
//若当前事务连接对应的已实际使用的数据源与要设置的数据源一致,则表明无需额外处理,只需正常执行即可
return invokeCallback.get();
}
} //若持有DB连接,则需要先挂起当前事务或资源
AbstractPlatformTransactionManager platformTransactionManager = SpringUtils.getBean(AbstractPlatformTransactionManager.class);
Assert.notNull(platformTransactionManager, "not found AbstractPlatformTransactionManager bean"); platformTransactionManagerDelegate = new PlatformTransactionManagerDelegateInner(platformTransactionManager);
currentTransaction = TransactionManagerUtils.getCurrentTransaction(platformTransactionManager); if (!platformTransactionManagerDelegate.isExistingTransaction(currentTransaction)) {
currentTransaction = null;
} suspendedResourcesHolder = platformTransactionManagerDelegate.suspend(currentTransaction); LOGGER.debug("DataSourceSwitchInvoker.invokeOn suspend result is {} ,invokeId: {}", suspendedResourcesHolder != null, invokeId); return invokeCallback.get(); } finally {
String resumeSuspendedResources = null;
//前面若有挂起事务或资源,则需在执行完方法后需恢复到当前事务状态
if (currentTransaction != null || suspendedResourcesHolder != null) {
platformTransactionManagerDelegate.resume(currentTransaction, suspendedResourcesHolder);
resumeSuspendedResources = "resume suspendedResources ok";
} setCurrentDsNameFunc.accept(oldDataSourceName); LOGGER.info("DataSourceSwitchInvoker.invokeOn end {} , recover setCurrentDsName {} --> {} ,invokeId: {}", resumeSuspendedResources, newDataSourceName, oldDataSourceName, invokeId);
}
} /**
* 在指定的数据源下执行回调方法
*
* @param setCurrentDsNameFunc
* @param invokeCallback
* @param <T>
* @return
*/
public static <T> T invokeOn(Consumer<String> setCurrentDsNameFunc, Supplier<T> invokeCallback) {
return invokeOn(SET_BEFORE, () -> SET_AFTER, setCurrentDsNameFunc, null, invokeCallback);
} private static <T> Map<String, T> getPropertiesByPattern(String configPath, Function<Object, T> convertValueFunc, BiFunction<String, T, Boolean> filterFunc) {
Assert.notNull(configPath, "param configPath not be null");
Assert.notNull(convertValueFunc, "param convertValueFunc not be null"); Map<String, T> resultMap = new HashMap<>(); if (!(SpringUtils.getApplicationContext().getEnvironment() instanceof ConfigurableEnvironment)) {
return resultMap;
} ConfigurableEnvironment environment = (ConfigurableEnvironment) SpringUtils.getApplicationContext().getEnvironment();
AntPathMatcher antPathMatcher = new AntPathMatcher(".");
String configKey = "{configKey}";
// 遍历所有的属性源
for (PropertySource<?> propertySource : environment.getPropertySources()) {
if (propertySource instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource<?>) propertySource; // 遍历当前属性源中的所有属性
for (String propertyName : enumerablePropertySource.getPropertyNames()) {
if (antPathMatcher.match(configPath, propertyName)) {
String key = propertyName;
if (configPath.contains(configKey)) {
key = antPathMatcher.extractUriTemplateVariables(configPath, propertyName).getOrDefault(configKey.replaceAll("[{}]", ""), "<null>");
} T value = convertValueFunc.apply(enumerablePropertySource.getProperty(propertyName));
if (filterFunc == null || filterFunc.apply(key, value)) {
resultMap.put(key, convertValueFunc.apply(value));
}
}
}
}
}
return resultMap;
} /**
* 通过内部类在不破坏封装性、访问性的前提下,提供当前类内部的protected方法的访问能力
*/
private static class PlatformTransactionManagerDelegateInner extends PlatformTransactionManagerDelegate { public PlatformTransactionManagerDelegateInner(AbstractPlatformTransactionManager transactionManager) {
super(transactionManager);
} @Override
protected Object suspend(Object transaction) throws TransactionException {
return super.suspend(transaction);
} @Override
protected void resume(Object transaction, Object resourcesHolderObj) {
super.resume(transaction, resourcesHolderObj);
} @Override
protected boolean isExistingTransaction(Object transaction) {
return super.isExistingTransaction(transaction);
}
} }

依赖CODE(注意包名路径需与AbstractPlatformTransactionManager、DataSourceTransactionManager一致):

//author: zuowenjun
//注意包名必需是如下,因为要访问protected方法
package org.springframework.jdbc.datasource; public class PlatformTransactionManagerDelegate {
private final AbstractPlatformTransactionManager delegate; public PlatformTransactionManagerDelegate(AbstractPlatformTransactionManager transactionManager) {
this.delegate = transactionManager;
} protected Object suspend(Object transaction) throws TransactionException {
return delegate.suspend(transaction);
} protected void resume(Object transaction, Object resourcesHolderObj) {
AbstractPlatformTransactionManager.SuspendedResourcesHolder resourcesHolder = (AbstractPlatformTransactionManager.SuspendedResourcesHolder) resourcesHolderObj;
delegate.resume(transaction, resourcesHolder);
} protected boolean isExistingTransaction(Object transaction) {
return delegate.isExistingTransaction(transaction);
} } //author: zuowenjun
//注意包名必需是如下,因为要访问protected方法
package org.springframework.transaction.support; public class TransactionManagerUtils { public static String getCurrentDbConnectionUrl(String threadLocalDbNameIfNoSet) {
DataSource dataSource = SpringUtils.getBean(DataSource.class);
if (dataSource == null) {
return threadLocalDbNameIfNoSet;
} ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection()) {
return threadLocalDbNameIfNoSet;
} try {
return conHolder.getConnection().getMetaData().getURL();
} catch (Throwable e) {
LOGGER.warn("TransactionManagerUtils.getCurrentDbConnectionUrl error", e);
} return threadLocalDbNameIfNoSet;
} public static Object getCurrentTransaction(AbstractPlatformTransactionManager transactionManager) {
if (!(transactionManager instanceof DataSourceTransactionManager)) {
throw new RuntimeException("only support DataSourceTransactionManager doGetTransaction");
} DataSourceTransactionManager dsTransactionManager = (DataSourceTransactionManager) transactionManager;
return dsTransactionManager.doGetTransaction();
} }

其中:SpringUtils工具类是一个简单的实现了Spring上下文织入的接口然后赋值给静态字段,最终实现可以直接使用applicationContext.getBean(type)

使用示例CODE:

//假设这里是数据源的设置,tips:多数据源一般都是自定义实现了AbstractRoutingDataSource,然后使用ThreadLocal来保存设置当前要使用的数据源配置名称

private ThreadLocal<String> dataSourceHolder = new ThreadLocal<>();

@Transactional
public doWithTx(){
//第一种方法:【推荐第一种】
//假设之前是read_db 数据源,现在需要切换成master_db
DataSourceSwitchInvoker.invokeOn("master_db", () -> dataSourceHolder.get(), (dsName) -> dataSourceHolder.set(dsName), null, () -> {
Object demo = null; //模拟 demoMapper.get(123L);
return demo;
}); //第二种方法:(重载方法,一个设置数据源方法处理执行前、执行后的数据源设置)
//假设之前是read_db 数据源,现在需要切换成master_db
AtomicReference<String> dsName = new AtomicReference<>();
DataSourceSwitchInvoker.invokeOn(eventName -> {
if (SET_BEFORE.equals(eventName)) {
//执行前,自行记录之前的数据源
dsName.set(dataSourceHolder.get());
//设置新数据源
dataSourceHolder.set("master_db");
} else if (SET_AFTER.equals(eventName)) {
//执行后,还原设置数据源
dataSourceHolder.set(dsName.get());
} }, () -> {
Object demo = null; //模拟 demoMapper.get(123L);
return demo;
});
}

编码建议:

切换虽好用,但建议不要在切换的方法中进行写数据的操作,更适合仅用于临时需要查询其他数据源的数据时使用,以免破坏spring事务的完整性,因为invokeOn方法本身就是先挂起一个事务,然后开新连接执行新的操作DB的方法,最后还原恢复事务,若在其中又进行了其他的操作,可能存在未知风险,虽然理论做什么都可以但非常不建议。

经多种测试,无论是普通方法 OR 在事务中的方法,均能正常执行,简直就是YYDS!原创不易,如有帮助关注+点个赞吧v

事务中无法切换数据源?DataSourceSwitchInvoker:轻松实现多数据源切换执行工具类的更多相关文章

  1. 时间工具类之“ JDK1.8中 LocalDate、LocalTime、LocalDateTime、LocalDateTimeUtil四个时间工具类”

    一.使用的原因 在JDK8发布的时候,推出了LocalDate.LocalTime.LocalDateTime这个三个时间处理类,以此来弥补之前的日期时间类的不足,简化日期时间的操作. 在Java8之 ...

  2. 转:轻松把玩HttpClient之封装HttpClient工具类(一)(现有网上分享中的最强大的工具类)

    搜了一下网络上别人封装的HttpClient,大部分特别简单,有一些看起来比较高级,但是用起来都不怎么好用.调用关系不清楚,结构有点混乱.所以也就萌生了自己封装HttpClient工具类的想法.要做就 ...

  3. 轻松把玩HttpClient之封装HttpClient工具类(五),携带Cookie的请求

    近期更新了一下HttpClientUtil工具类代码,主要是加入了一个參数HttpContext,这个是用来干嘛的呢?事实上是用来保存和传递Cookie所须要的. 由于我们有非常多时候都须要登录.然后 ...

  4. Java中使用google.zxing快捷生成二维码(附工具类源码)

    移动互联网时代,基于手机端的各种活动扫码和收付款码层出不穷:那我们如何在Java中生成自己想要的二维码呢?下面就来讲讲在Java开发中使用 google.zxing 生成二维码. 一般情况下,Java ...

  5. (转)Android中px与dip,sp与dip等的转换工具类

    功能 通常在代码中设置组件或文字大小只能用px,通过这个工具类我们可以把dip(dp)或sp为单位的值转换为以px为单位的值而保证大小不变.方法中的参数请参考http://www.cnblogs.co ...

  6. redis事务中的WATCH命令和基于CAS的乐观锁

    转自:http://blog.sina.com.cn/s/blog_ae8441630101cgy3.html 在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能. ...

  7. 五、在事务中使用Mybatis缓存的那些坑

    场景: 1.同一个事务中 2.使用mybatis执行同一个sql @Transactional(rollbackFor = { Exception.class }) public void getIn ...

  8. hibernate框架学习第二天:核心API、工具类、事务、查询、方言、主键生成策略等

    核心API Configuration 描述的是一个封装所有配置信息的对象 1.加载hibernate.properties(非主流,早期) Configuration conf = new Conf ...

  9. LY.JAVA面向对象编程.工具类中使用静态、说明书的制作过程、API文档的使用过程

    2018-07-08 获取数组中的最大值 某个数字在数组中第一次出现时的索引 制作说明书的过程 对工具类的使用 获取数组中的最大值 获取数字在数组中第一次出现的索引值 API的使用过程 Math

  10. java高并发系列 - 第16天:JUC中等待多线程完成的工具类CountDownLatch,必备技能

    这是java高并发系列第16篇文章. 本篇内容 介绍CountDownLatch及使用场景 提供几个示例介绍CountDownLatch的使用 手写一个并行处理任务的工具类 假如有这样一个需求,当我们 ...

随机推荐

  1. 【原创】xenomai环境下开源实时数控系统LinuxCNC编译安装

    linuxcnc 在xenomai下的构建简单记录,参考链接https://www.linuxcnc.org/docs/devel/html/code/building-linuxcnc.html 1 ...

  2. SpringSecurity认证流程分析

    重要组件 SecurityContext 上下文对象,Authentication(认证)对象会放在里面 SecurityContextHolder 用于拿到上下文对象的静态工具类 Authentic ...

  3. Spring注解之-@ConditionalOnExpression表达式

    @ConditionalOnExpression("'true") 当括号中的内容为true时,使用该注解的类被实例化,支持语法如下: @ConditionalOnExpressi ...

  4. Http2服务调用排坑记

    原文作者:陈友行原文链接:https://www.nginx.org.cn/article/detail/89转载来源:NGINX开源社区著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...

  5. 转载 mybatis-plus配置控制台打印完整带参数SQL语句

    问题背景 通常我们开发的时候,需要联合控制台和Navicat/PLSQL等工具进行语句的拼接检查,如果只是输出了一堆???,那么将极大降低我们的效率.因此我们需要输出完整的SQL语句以便调试. Upd ...

  6. Qt/C++编写视频监控系统82-自定义音柱显示

    一.前言 通过音柱控件实时展示当前播放的声音产生的振幅的大小,得益于音频播放组件内置了音频振幅的计算,可以动态开启和关闭,开启后会对发送过来的要播放的声音数据,进行运算得到当前这个音频数据的振幅,类似 ...

  7. Qt编写安防视频监控系统59-子模块3图文警情

    一.前言 图文警情子模块是为了适应现在各种人脸识别报警应用而增加的,参照现在各种视频监控手机app报警提示信息,基本上都是带了时间.内容.图片缩略图(单击可以查看大图),这种信息排列形式在现代的软件中 ...

  8. CentOS 集群初始化设置

    0. 前置操作 centos-7.9.2009-isos-x86_64安装包下载_开源镜像站-阿里云 下载CentOS-7-x86_64-DVD-2009.iso即可 1. 配置静态网络 1.1 查看 ...

  9. 循规蹈矩--从零开始建设k8s监控(一)

    前言 监控k8s集群,目前主流就是使用prometheus以及其周围的生态,本文开始介绍怎么一步步完成k8s监控的建设 环境准备 组件 版本 操作系统 Ubuntu 22.04.4 LTS minik ...

  10. CDS标准视图:分配到任务清单的维护包数据 I_PckgTaskListOpalLocData

    视图名称:分配到任务清单的维护包数据 I_PckgTaskListOpalLocData 视图类型:基础 视图代码: 点击查看代码 @AbapCatalog.sqlViewName: 'IPCKTLO ...