SSM使用AbstractRoutingDataSource后究竟如何解决跨库事务
Setting:
绑定三个数据源(XA规范),将三个实例绑定到AbStractoutingDataSource的实例MultiDataSource(自定义的)对象中,mybatis SqlSessionFactory数据源设定为MultiDataSource,DataSourceTransactionManager数据源绑定MultiDataSource,自定义注解,切面,就某个字段被crud操作涉及时,进入切面,计算目标数据库,动态切换数据库.不涉及事务时实现了动态切换,加了事务后..........
Question:
1. AbstractRoutingDataSource是什么? 为什么它能动态切换数据库?
2. 加上事务后还能动态切换数据库吗? Spring的事务是怎么开启的?
3. 结合Mybatis 如何实现动态切换的同时开启分布式事务?
My opinion:
For question 1:
我们用一个类来继承AbstractRoutingDataSource,只需实现一个方法,那就是
@Override
protected Object determineCurrentLookupKey() {
log.info("调用一次{}", MultiDataSourceHolder.getDataSourceKey());
String dataSourceKey = MultiDataSourceHolder.getDataSourceKey();
//初始化AbstractRoutingDataSource实现类时,dsRoutingSetProperties保存了多数据源名称
return dataSourceKey == null ? dsRoutingSetProperties.getDataSourceKeysMapping().get(0) : dataSourceKey;
}
我们来看一下具体方法如何作用的
//类实现了DataSource接口 getConnection() 调用此方法
protected DataSource determineTargetDataSource() {
Object lookupKey = this.determineCurrentLookupKey();
//这里的作用的resolvedDataSources也是一个map,而我们实例化多数据源时设置的是名叫targetDataSource的map
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource; } }
//我们来看另一个方法afterPropertiesSet,这个方法是实现了接口InitializingBean,在ioc实例化bean的时候,还未生成代理对象前会调用的方法,也就是还为注入到单例缓存池时 public void afterPropertiesSet() {
...
this.resolvedDataSources = new HashMap(this.targetDataSources.size());
//java8添加的迭代方式
this.targetDataSources.forEach((key, value) -> {
Object lookupKey = this.resolveSpecifiedLookupKey(key);
//实际上只有这里做了处理,如果map的值是String类型,用jndi的方式获取DataSource(我的技能树没点到这里,看不懂)
DataSource dataSource = this.resolveSpecifiedDataSource(value);
this.resolvedDataSources.put(lookupKey, dataSource);
});
if (this.defaultTargetDataSource != null) {
//这里的处理同上
this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
}
看到这里,我们大概知道了AbstractRoutingDataSource其实就是对DataSource的包装,之前baidu搜索spring的设计模式时,大部分的博文都会说到Spring动态切换数据库就
用的是装饰模式,每每看到这里就是一脸懵(为了整明白才在个人Demo上加上动态数据库切换),做完之后那么问题来了,加了事务后的结果是怎么样呢?
For question 2:
看一下分别加了没加@Transactional 和 加了注解的 两个结果 null和dataSourcexx 是切面时存入线程变量的值
分别调用三次 和调用一次,这代表着一共产生了三个连接和一个连接.为什么加了事务之后只用了一次连接 ? 切面往线程变量里存了映射,但是没起作用?
花了三天才解决这个问题,这里我们从头撸起找原因
先看下事务的三个关键性接口
PlatformTransactionManager getTransaction/commit/rollback
TransactionStatus 事务运行状态 isNewTransaction()/hasSavepoint() 是否为新事务/是否有保存点
TransactionDefinition 事务属性 定义了大量关于隔离级别/传播行为的常量
我们知道@Transactional声明式事务是基于动态代理,事务也是以aop参与责任链形式调用
从头看 @EnableTransactionManagement,点开可以看到 @Import({TransactionManagementConfigurationSelector.class}),我们点进去瞅瞅
//ImportSelector接口需要实现的方法,作用是只要返回类的全限定名即可将Bean注入到容器内
protected String[] selectImports(AdviceMode adviceMode) {
switch(adviceMode) {
//打个断点进去 发现走的是Proxy,默认走的就是Proxy,这里注册了两个bean,进去ProxyTransactionManagementConfiguration里看看,发现就是一个配置类,
//注册了三个bean,我们看一下TransactionInterceptor(拦截器,aop作用的实质)这个类
进入TransactionInterceptor类后,感觉有用的就只有一个invoke方法,方法内回调父类的 invokeWithinTransaction()方法,我们跟进去看
//方法很长,直接打一个端点在第一行,随便一个方法加上@Transactional,顺利进入
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, TransactionAspectSupport.InvocationCallback invocation) throws Throwable {
Object result;
if (...) {
...
} else {
//方法直接进到这里了
TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
try {
//执行下一层拦截器链 直到执行目标方法
result = invocation.proceedWithInvocation();
protected TransactionAspectSupport.TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
....
status = tm.getTransaction((TransactionDefinition)txAttr);
...
//把TransactionStatus封装进TransactionInfo对象中,同时绑定到线程变量
return this.prepareTransactionInfo(tm, (TransactionAttribute)txAttr, joinpointIdentification, status);
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
Object transaction = this.doGetTransaction();
//如果ConnnectionHolder不为null transactionActive为true 进入这里 ,因为已经确定存在事务了,进去就事务传播行为做判断,是否建立保存点等
if (this.isExistingTransaction(transaction)) {
return this.handleExistingTransaction((TransactionDefinition)definition, transaction, debugEnabled);
}
.....
else {
//第一次 进入这里 不需要处理其他
AbstractPlatformTransactionManager.SuspendedResourcesHolder suspendedResources = this.suspend((Object)null);
protected Object doGetTransaction() {
DataSourceTransactionManager.DataSourceTransactionObject txObject = new DataSourceTransactionManager.DataSourceTransactionObject();
txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
//这里第一次(如果事务多次访问数据库)返回的为null,下文会看到具体过程
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
//看到这里可以发现 transaction 对象就是DataSourceTransactionObject,继承自JdbcTransactionObjectSupport,有一个属性ConnectionHolder,用来干嘛的??
return txObject;
}
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
//这里的key就是 我们在声明TransactionManager注入的数据源 ,我这里就是MultiDataSource
Object value = doGetResource(actualKey);
private static Object doGetResource(Object actualKey) {
//记住这个 resources 它是一个ThreadLocal<Map<Object, Object>>对象 ,第一进来肯定直接return null了
Map<Object, Object> map = (Map)resources.get();
if (map == null) {
return null;
} else {
//这里试想 如果map不为null 通过绑定的数据源 我们能拿到什么 ?
Object value = map.get(actualKey);
if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) {
map.remove(actualKey);
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
}
至此,函数回调 ,执行doBegin()方法
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
Connection con = null;
try {
//前文发现,自身的ConnnectionHolder对象为null,这里就直接进入了
if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = this.obtainDataSource().getConnection();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
//创建连接,重新设置ConnectionHolder对象
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
//取消自动提交事务
con.setAutoCommit(false);
}
// 这里影响后面 第二次进入 是否还会进入本方法
txObject.getConnectionHolder().setTransactionActive(true);
if (txObject.isNewConnectionHolder()) {
//方法进入这里 bindResource方法是不是有点熟悉 ?前文有个方法叫doGetResource()
TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
}
public static void bindResource(Object key, Object value) throws IllegalStateException {
Map<Object, Object> map = (Map)resources.get();
//之前的map为空,这下不为空了
if (map == null) {
map = new HashMap();
resources.set(map);
}
//之前介绍的key 就是数据源实例对象,value就是刚传进来的ConnnectionHolder
Object oldValue = ((Map)map).put(actualKey, value);
看到这里大概清楚了为什么AbstractRoutingSource的方法为什么没有调用.大致总结下, 希望不会带偏读者
Spring创建事务时,每次都会
1.创建一个TransactionAspectSupport.TransactionInfo对象,该对象中封装了PlatformTransactionManager,joinpointIdentification("被调用方法名"),TransactionStatus(事务状态,封装一个DataSourceTransactionObject对象,也是就是transaction对象,里面再包一层ConnectionHolder,保存数据库连接),同时关联一个本类对象OldTransactionInfo,
2.获取transaction对象,先尝试从线程变量中获取ConnectionHolder对象,判定ConnectionHolder是否为null,如果是空值,则定义此次事务为新事务(newTransaction),从事务管理器中绑定的数据源中获取连接,绑定到ConnectionHolder对象中,并将holder存入线程变量.
3,将OldTransaction对象设为自身,并存入线程变量,执行下一层aop或目标方法
4.事务内再度调用带事务切面的代理对象时,步骤2从线程变量获取到Holder,进入handleExistingTransaction()方法,就事务传播行为决定是否挂起事务,设立保存点等,将事务属性newTransaction设为false
....
在发现这个问题时尝试了各种诡异举措来试图解决它:
1.编程式事务TransactionTemplate 可以分为两个事务,但是这并没有什么意义..能不能使两个事务一起提交?
2.自定义MyTemplate继承TransactionTemplate ,commit时 开启新线程,当两个运行都观测到目标结果(如redis)才提交,..可想而知,新的线程,存在原线程变量里的啥都成了孤岛, 卒..
3.自定义事务管理器 / 自定义SqlSessionTemplate,将三个数据源都绑定到SqlSessionTemplate中,获取线程变量中的数据库名,返回对应的数据源 .. 然而不是不会用 就是根本就没被调用.. 这方面的知识还是太浅薄了..
For question 3:
查看网上很多的案例,发现 有用JdbcTemplate控制的,有分多个SqlSessionFactory 用JTA控制的,让Dao层分开被SqlSessionFactory管理,确实可以实现跨库事务,但这一开始就决定了要落入哪个库.和我这个应用场景不一致..能否借鉴折中一下呢?
@Configuration
@MapperScan(value = "com.zuan.cinema.mapper",sqlSessionFactoryRef = "sqlSessionFactory00")
@MapperScan(value = "com.zuan.cinema.mapper.m",sqlSessionFactoryRef = "sqlSessionFactory01")
public class DataSourceConfiguration {
@Bean
@Primary
public SqlSessionFactory sqlSessionFactory00(@Qualifier("dataSource00") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/*.xml"));
Interceptor interceptor = new PageInterceptor();
sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
return sqlSessionFactoryBean.getObject();
} @Bean
public SqlSessionFactory sqlSessionFactory01(@Qualifier("multiDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/m/*.xml"));
Interceptor interceptor = new PageInterceptor();
sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
return sqlSessionFactoryBean.getObject();
}
}
因为crud操作时,只会受一个字段影响决定使用哪个数据库的连接,那么就可以定义两个sqlSessionFatory,一个管理普通mapper,引用普通数据源,另一个管理直接关联字段的mapper,应用MultiDataSource,这样进入事务后必然通过MultiDataSource动态选择数据库.这里只有用JTA事务管理才能实现切换,它和DataSourceTransactionManager实现机制不一样,建立的连接也不样.更多的我看不懂...
Epilog:
结果只在个人Demo场景下测试成功,没有经历过生产环境的洗礼.并且上面的推断也出自未经过生产环境的人之口,请自行判断...
SSM使用AbstractRoutingDataSource后究竟如何解决跨库事务的更多相关文章
- 【Java EE 学习 19】【使用过滤器实现全站压缩】【使用ThreadLocal模式解决跨DAO事务回滚问题】
一.使用过滤器实现全站压缩 1.目标:对网站的所有JSP页面进行页面压缩,减少用户流量的使用.但是对图片和视频不进行压缩,因为图片和视频的压缩率很小,而且处理所需要的服务器资源很大. 2.实现原理: ...
- Spring3.0+Hibernate+Atomikos集成构建JTA的分布式事务--解决多数据源跨库事务
一.概念 分布式事务分布式事务是指事务的参与者.支持事务的服务器.资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上.简言之,同时操作多个数据库保持事务的统一,达到跨库事务的效果. JTA ...
- mysql 跨库事务XA
前一段时间在工作中遇到了跨库事务问题,后来在网上查询了一下,现在做一下整理和总结. 1.首先要确保mysql开启XA事务支持 SHOW VARIABLES LIKE '%XA%' 如果innodb_s ...
- vue项目打包本地后通过nginx解决跨域
前言 有时候我们打包好vue项目让后端人员部署项目时可能会有小插曲,为了不麻烦后端人员和避免尴尬,最好的办法就是在本地自己先测一下,而在本地运行打包后的项目会遇到接口跨域的问题.我平时经常用的方法就是 ...
- java跨库事务Atomikos
1:引入额外的jar <dependency> <groupId>com.atomikos</groupId> <artifactId>transact ...
- mysql跨库复制: replicate_wild_do_table和replicate-wild-ignore-table
使用replicate_do_db和replicate_ignore_db时有一个隐患,跨库更新时会出错. 如设置 replicate_do_db=testuse mysql;update test. ...
- Ajax 调用webservice 解决跨域请求和发布到服务器后本地调用成功外网失败的问题
webservice 代码 /// <summary> /// MESService 的摘要说明 /// </summary> [WebService(Namespac ...
- 使用 Nginx 部署前后端分离项目,解决跨域问题
前后端分离这个问题其实松哥和大家聊过很多了,上周松哥把自己的两个开源项目部署在服务器上以帮助大家可以快速在线预览(喜大普奔,两个开源的 Spring Boot + Vue 前后端分离项目可以在线体验了 ...
- 后台访问 JS解决跨域问题
今天看了看以前做的一个小项目(其实就是一个页面),分享一下当时解决跨域问题的: 背景:公司把项目部署在多台服务器上,防止一台服务器崩溃后,其他的可以继续访问,对应本公司来说,某台服务器出问题后,技术人 ...
随机推荐
- HTTPS详解二:SSL / TLS 工作原理和详细握手过程
HTTPS 详解一:附带最精美详尽的 HTTPS 原理图 HTTPS详解二:SSL / TLS 工作原理和详细握手过程 在上篇文章HTTPS详解一中,我已经为大家介绍了 HTTPS 的详细原理和通信流 ...
- 四、Django学习之关系表介绍及使用
关系表介绍及使用 一对一关系 xx = models.OneToOneField(to='表名',to_field='字段名',on_delete=models.CASCADE) #on_delete ...
- <img>和background-img区别
1. 是否占位 background-image是背景图片,是css的一个样式,不占位 <img />是一个块状元素,它是一个图片,是html的一个标签,占位 2.否可操作 backgro ...
- DOCKER 学习笔记7 Docker Machine 在阿里云实例化ECS 以及本地Windows 实例化虚拟机实战
前言 通过以上6小节的学习,已经可以使用DOCKER 熟练的部署应用程序了.大家都可以发现使用 DOCKER 带来的方便之处,因为现在的话,只是在一台服务器上部署,这样部署,我们只需要一条命令,需要的 ...
- 【WPF学习】第三十九章 理解形状
在WPF用户界面中,绘制2D图形内容的最简单方法是使用形状(shape)——专门用于表示简单的直线.椭圆.矩形以及多变形的一些类.从技术角度看,形状就是所谓的绘图图元(primitive).可组合这些 ...
- Java原子变量类需要注意的问题
在学习多线程时,遇到了原子变量类,它是基于 CAS 和 volatile 实现的,能够保障对共享变量进行 read-modify-write 更新操作的原子性和可见性.于是我就写了一段代码试试,自认为 ...
- POJ_1485_dp
题目描述: 每组数据给n个点,点按一维坐标升序给出,要求划分成k块,在每一块中,取一个站,要求每个块中所有的点到站的距离的和的总和最小. 思路: dp题,dp[i][j]表示i个点分成j块的最小距离, ...
- Java集合中removeIf的使用
在JDK1.8中,Collection以及其子类新加入了removeIf方法,作用是按照一定规则过滤集合中的元素.这里给读者展示removeIf的用法.首先设想一个场景,你是公司某个岗位的HR,收到了 ...
- 微信小程序面试题总结
A类问题 1 请谈谈微信小程序主要目录和文件的作用? project.config.json 项目配置文件,用得最多的就是配置是否开启https校验: App.js 设置一些全局的基础数据等: App ...
- Spring ——Spring IoC容器详解(图示)
1.1 Spring IoC容器 从昨天的例子当中我们已经知道spring IoC容器的作用,它可以容纳我们所开发的各种Bean.并且我们可以从中获取各种发布在Spring IoC容器里的Bean,并 ...