您的位置:首页 > 编程语言 > Java开发

SPRING事务逻辑探究和多数据源解决方案调研

2017-05-21 11:09 309 查看
本文用到的框架组合及其版本号

spring 4.0.2.RELEASE

mybatis 3.4.1

mysql-connector-java 5.1.38

datasource:org.springframework.jdbc.datasource.DriverManagerDataSource

本文默认的代码逻辑

springMvc+mybatis

service层包裹事务

service操作mapper,mapper无事务拦截

本文springMvc演示代码片段如下

UserController

/**
* add user
*
* @param request
* @param user
* @return
*/
@ResponseBody
@RequestMapping(value = "/test")
public Result<Boolean> test(HttpServletRequest request, User user) {
Result<Boolean> result = userService.editCase();
return result;
}


UserServiceImpl

public Result<Boolean> editCase() {
//add casebase #{here db =zuosh}
User user = new User();
user.setUsername("user001");
userMapper.insertSelective(user);
//add doc //#{here db = std} xx
DocNoticeExample noticeExample = new DocNoticeExample();
noticeExample.createCriteria().andNumNoticeIdEqualTo(2);
//mapper是操作数据库的dao,没有事务拦截
docNoticeMapper.selectByExample(noticeExample);
return Result.buildResult(ResponseCode.SUCCESS, "ok", "ok");
}


application-service.xml

<!-- 事务配置 -->
<tx:advice id="TestAdvice" transaction-manager="transactionManagerKa">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="edit*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="REQUIRED"/>
<tx:method name="apply*" propagation="REQUIRED"/>
<tx:method name="select*" propagation="SUPPORTS"/>
<!--<tx:method name="*" propagation="SUPPORTS"/>-->
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="allTestServiceMethod" expression="execution(* com.zuosh.service.*.*(..))"/>
<aop:advisor pointcut-ref="allTestServiceMethod" advice-ref="TestAdvice"/>
</aop:config>


spring多数据源常用解决方案

spring在项目中如果遇到多个数据源,最常用的解决方案恐怕就是动态数据源的切换这种解决方案,但是这种方式有个缺点:和事务配合的不好,在操作多数据源的service中,不能有事务.否则会提示 表找不到的错误.所以如果多数据源又要在多数据源保持事务,就要考虑spring提供的分布式事务管理器解决方案了.

为什么会出现这种情况呢?

之前的一篇文章已经大概讲了原因 spring动态数据源和事务配合的调研,简而言之,就是说 spring在执行service方法之前,会先执行事务逻辑的拦截从而获取当前的事务,这个过程中会去拿当前的数据源. 而在service的执行逻辑中,来回切换数据源的时候,如果当前的操作有事务,则优先从事务去获取数据源,而不是根据路由key去获取.这样会导致 一个事务一旦起作用,则他的数据源就已经决定,不会动态切换. 所以被事务拦截的方法中,多数据源的操作不支持**

上面的代码中 service 中的mapper方法 运行时可以看到其实是MapperProxy生成的代理对象,请看下面截图



下面我们看下 mybatis 对一个请求的处理调用流程



可以看出来, 一个普通的调用,是sqlSessiontemplate拿到sqlSession,然后执行doUpdate.

下面看下spring执行待事务的service时候 ,所走的流程

事务是aop通过对目标方法进行拦截,然后生成代理对象,在代理对象中做一些事情而已.

我们看下,controller中拦截到的service实例是什么样的



可以看出来, 在有事务拦截的方法里面,其实service已经是一个代理的形态存在了 .下面请看他的 请求流程:



很简单,事务其实是一个拦截器,有transactionInterceptor执行拦截,在执行target之前,拿到事务,然后执行目标方法(editCase),最后提交事务,执行清理.

那么 ,在拦截的时候创建的时候,和执行dao层方法的时候获取的事务 ,有什么联系呢?

严格来说,其实没关系,互不干涉,但是他们的联系却是connection,事务拿到了数据源,dao层的mapper执行的sql也用的同样的connection,自动提交设置为false,然后交给spring统一提交,仅此而已

spring在执行事务拦截的时候,会获取数据源 把当前数据源绑定到当前的线程

//事务拦截 首先执行到 transactionInterceptor 的invoke
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
@Override
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
});
}

//其次执行,TransactionAspectSupport的invokeWithinTransaction
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {

// If the transaction attribute is null, the method is non-transactional.
//------省略---------------------

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
//会再次执行 事务的创建
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
//省略======================
}}

//然后执行 事务的创建逻辑
protected TransactionInfo createTransactionIfNecessary(
PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {

// If no name specified, apply method identification as transaction name.
//.............部分代码省略...节约篇幅.............
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

// 接着在 事务管理器里面拿事务AbstractPlatformTransactionManager的getTransaction
@Override
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
//get 事务
Object transaction = doGetTransaction();

//.............省略代码...........
}
//然后 根据配置 查找DataSourceTransactionManager的doGetTransaction
@Override
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
txObject.setSavepointAllowed(isNestedTransactionAllowed());
//TransactionSynchronizationManager 的静态方法 获取数据源 绑定到当前事务
ConnectionHolder conHolder =
(ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);
txObject.setConnectionHolder(conHolder, false);
return txObject;
}


TransactionSynchronizationManager是一个静态工具方法,里面保留的线程本地变量,例如sessionFactory,datasource数据等.其实事务拦截的过程中 ,往TransactionSynchronizationManager是一个静态工具方法保存数据源变量.最后执行完,提交,就提交这个connection,然后调用connection自身的commit,执行 事务的提交. 下面截图是 进入service之后,执行service代码之前的 事务初始拦截阶段,在事务管理器中绑定了的数据源的截图.可以看出 保存了一对 DriverManagerDatasource 和 ConnectionHolder 一对. 将来提交就依赖保存的这一对数据.



mybatis 获取事务

说完了spring获取事务的原理,再来看下mybatis获取事务(如果有的话)的流程,mybaits怎么知道这个方法有没有事务呢?

原来是根据 TransactionSynchronizationManager 的getResource,因为在进入service之前进行事务拦截的时候,spring已经把datasource和sessionHolder绑定了一起,如果根据mybatis中sessionFactory中的datasource能够从TransactionSynchronizationManager获取datasource,则表示有事务,否则执行正常 获取数据源的流程

依据上面的分析,先看下mybatis在(service方法)没有事务,和带有事务两种情况获取connection的过程.

没有事务



包含事务



可以看到 ,两种情况仅仅是mybaits的数据源获取的地方不同而已.

//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");
}
}

//获取连接核心逻辑
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
//根据数据源从事务管理器获取连接,获取不到,要么是第一次获取 要么是没有事务.
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(dataSource.getConnection());
}
return conHolder.getConnection();
}
// Else we either got no holder or an empty thread-bound holder here.

logger.debug("Fetching JDBC Connection from DataSource");
Connection con = dataSource.getConnection();
//获取过连接之后,还问下当前是否有激活的事务,有的话就把当前的拿到的连接保存到本地县城变量里面.
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
// Use same Connection for further JDBC actions within the transaction.
// Thread-bound object will get removed by synchronization at transaction completion.
ConnectionHolder holderToUse = conHolder;
if (holderToUse == null) {
holderToUse = new ConnectionHolder(con);
}
else {
holderToUse.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(
new ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}

return con;
}


spring提交事务

spring获取事务和mybatis获取连接并执行的一些流程已经说完了. 剩下的就是怎么提交的问题了.请继续看下流程图:



很容易看得出来,提交的connection是根据在createTransactionIfNecessary创建的transaction 中的connectionHolder 拿到connection 并进行commit,完成一次事务.

双重事务问题

上面的铺垫讲完了 ,那么回到本文开头锁提出的多数据源问题,我们想必都能理解 为什么动态切换数据源不能和事务搭配使用的原理了把 .话有说回来,有没有办法,能让多数据源在单个service中配合事务起作用呢? 这就是分布式事务了

有没有不用分布式事务,又能实现事务回滚,和正常提交

这就是本文最终要说的问题 .

我找到一种方式 ,也许值得大家一试. 配置两份数据源,两份sessionfactory,两个transactionManager,然后在拦截的地方配置两份 ,就是说 同一个service中的方法,使两个事务管理器同时起作用. 这样就能保证 每隔事务统一提交,统一回滚. 为什么呢?

统一提交: 两个事务都作用到同一个方法上,service前半部分执行数据源a的逻辑,后半部分执行的时候数据源b的逻辑,方法不执行完,是不会执行事务的commit的,所以能实现统一提交 .当然也分一个先后,就看配置文件里面的顺序了.

统一回滚: 因为代码没有走到结尾,不管执行到那个数据源出现问题,两个数据源的操作都操作都会执行回滚逻辑.

这是为什么呢,为什么没有切换,为什么提交事务能同时提交两个数据源?

上面文章分析的比较清楚了,事务其实就是一个拦截器,多个拦截器其实就是一个责任链模式 .大家可以回想一下责任链的流程和样板代码是什么样的. 起核心逻辑是 ,比如有两个事务 a,b .则事务的拦截和提交逻辑顺序是 a.before->b.before->target->b.after->a.after. 可知,b.before执行了拦截,得到的数据源是b,然后service中执行的方法,有往connection a中写sql的,有往conneciton b中写sql的 .service执行完之后, b.after的逻辑就是 提交b.connection 中的sql,执行完之后,轮到a.after 则a拦截器执行 b connection中的sql commit操作.这样以来,两个事务都作用到service a 中,但是提交的过程 却是各自提交各自的connection,service中也可以往不同的connection中写sql,最终完成正确提交.

我这么说,是因为我已经测试通过了 下面是我的本地样例配置,仅供参考

<!-- DataSource定义。 -->
<bean name="dataSourceL" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${lawsuit.jdbc.url}"/>
<property name="username" value="${lawsuit.jdbc.username}"/>
<property name="password" value="${lawsuit.jdbc.password}"/>
<!-- 监控数据库 -->
</bean>

<!-- DataSource定义 -->
<bean id="dataSourceK" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<!-- 这个属性driverClassName为什么在DriverManagerDataSource及父类中找不到呢 -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

<!-- ========================================针对myBatis的配置项============================== -->

<bean id="sqlSessionFactoryS" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
<property name="dataSource" ref="dataSourceL"/>

<property name="mapperLocations" value="classpath*:sqlmap/s/**/*.xml"/>
<property name="plugins">
<list></list>
</property>
</bean>

<!-- 配置sqlSessionFactory -->
<bean id="sqlSessionFactoryK" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
<property name="dataSource" ref="dataSourceK"/>
<!-- 自动扫描me/gacl/mapping/目录下的所有SQL映射的xml文件, 省掉Configuration.xml里的手工配置 value="classpath:me/gacl/mapping/*.xml"指的是classpath(类路径)下me.gacl.mapping包中的所有xml文件
UserMapper.xml位于me.gacl.mapping包下,这样UserMapper.xml就可以被自动扫描 -->
<property name="mapperLocations" value="classpath*:sqlmap/k/**/*.xml"/>
<property name="plugins">
<list></list>
</property>
</bean>

<!-- =========================================事务配置========================================== -->
<bean id="transactionManagerS"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceL"/>
</bean>
<bean id="transactionManagerK"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceK"/>
</bean>

<!-- 事务配置 -->
<tx:advice id="TestAdvice" transaction-manager="transactionManagerK">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="edit*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="REQUIRED"/>
<tx:method name="apply*" propagation="REQUIRED"/>
<tx:method name="select*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>

<tx:advice id="TestAdviceS" transaction-manager="transactionManagerS">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="edit*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="REQUIRED"/>
<tx:method name="apply*" propagation="REQUIRED"/>
<tx:method name="select*" propagation="SUPPORTS"/>
</tx:attributes>
</tx:advice>

<!--  配置参与事务的类 -->
<aop:config>
<aop:pointcut id="allTestServiceMethod" expression="execution(* com.zuosh.service.*.*(..))"/>
<aop:advisor pointcut-ref="allTestServiceMethod" advice-ref="TestAdvice"/>
</aop:config>

<aop:config>
<aop:pointcut id="allTestServiceMethod" expression="execution(* com.zuosh.service.*.*(..))"/>
<aop:advisor pointcut-ref="allTestServiceMethod" advice-ref="TestAdviceSuit"/>
</aop:config>


上面就是全部配置,每个都是两份.这样能实现多事务的提交和回滚.

但是,这种方式的配置实用吗? 有什么缺点?

缺点蛮大的.

两个数据源还好说,更多的数据源要多少配置

每个方法都是两个事务 ,大部分别的业务方法,没有都操作两个数据源,岂不是多余?

方法上莫名添加了多个数据源,性能还能保证吗?

这种配置方法,是理论上可行的.如果您只有少量的多数据源 ,并且不太考虑性能 ,是可以参考 .要看用户怎么取舍 .要不舍弃事务 用动态数据源.要么使用事务,牺牲一些灵活性和性能.

其实很多情况可以绕过多数据源问题,多数据源必然引入不可控的复杂性,可以考虑项目拆分,利用微服务的概念,根据不同的数据源使用不同的app提供rpc服务,或者多数据源保持在一个mysql实例中(如果可能的话),这样sql语句加上schema也能使用一个mysql连接访问到多个数据源.

总的来说 ,针对同一个问题 解决方案挺多的 .要么就是直接面对,要么就是迂回包抄,要么就是绕过问题. 解决方案根据不同的环境因素(人力,时间,项目复杂度,维护成本)必定会评估出不同的结果.重点是:遇到的问题解决了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息