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
UserServiceImpl
application-service.xml
为什么会出现这种情况呢?
之前的一篇文章已经大概讲了原因 spring动态数据源和事务配合的调研,简而言之,就是说 spring在执行service方法之前,会先执行事务逻辑的拦截从而获取当前的事务,这个过程中会去拿当前的数据源. 而在service的执行逻辑中,来回切换数据源的时候,如果当前的操作有事务,则优先从事务去获取数据源,而不是根据路由key去获取.这样会导致 一个事务一旦起作用,则他的数据源就已经决定,不会动态切换. 所以被事务拦截的方法中,多数据源的操作不支持**
上面的代码中 service 中的mapper方法 运行时可以看到其实是MapperProxy生成的代理对象,请看下面截图
下面我们看下 mybatis 对一个请求的处理调用流程
可以看出来, 一个普通的调用,是sqlSessiontemplate拿到sqlSession,然后执行doUpdate.
我们看下,controller中拦截到的service实例是什么样的
可以看出来, 在有事务拦截的方法里面,其实service已经是一个代理的形态存在了 .下面请看他的 请求流程:
很简单,事务其实是一个拦截器,有transactionInterceptor执行拦截,在执行target之前,拿到事务,然后执行目标方法(editCase),最后提交事务,执行清理.
那么 ,在拦截的时候创建的时候,和执行dao层方法的时候获取的事务 ,有什么联系呢?
严格来说,其实没关系,互不干涉,但是他们的联系却是connection,事务拿到了数据源,dao层的mapper执行的sql也用的同样的connection,自动提交设置为false,然后交给spring统一提交,仅此而已
spring在执行事务拦截的时候,会获取数据源 把当前数据源绑定到当前的线程
TransactionSynchronizationManager是一个静态工具方法,里面保留的线程本地变量,例如sessionFactory,datasource数据等.其实事务拦截的过程中 ,往TransactionSynchronizationManager是一个静态工具方法保存数据源变量.最后执行完,提交,就提交这个connection,然后调用connection自身的commit,执行 事务的提交. 下面截图是 进入service之后,执行service代码之前的 事务初始拦截阶段,在事务管理器中绑定了的数据源的截图.可以看出 保存了一对 DriverManagerDatasource 和 ConnectionHolder 一对. 将来提交就依赖保存的这一对数据.
原来是根据 TransactionSynchronizationManager 的getResource,因为在进入service之前进行事务拦截的时候,spring已经把datasource和sessionHolder绑定了一起,如果根据mybatis中sessionFactory中的datasource能够从TransactionSynchronizationManager获取datasource,则表示有事务,否则执行正常 获取数据源的流程
依据上面的分析,先看下mybatis在(service方法)没有事务,和带有事务两种情况获取connection的过程.
没有事务
包含事务
可以看到 ,两种情况仅仅是mybaits的数据源获取的地方不同而已.
很容易看得出来,提交的connection是根据在createTransactionIfNecessary创建的transaction 中的connectionHolder 拿到connection 并进行commit,完成一次事务.
有没有不用分布式事务,又能实现事务回滚,和正常提交
这就是本文最终要说的问题 .
我找到一种方式 ,也许值得大家一试. 配置两份数据源,两份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,最终完成正确提交.
我这么说,是因为我已经测试通过了 下面是我的本地样例配置,仅供参考
上面就是全部配置,每个都是两份.这样能实现多事务的提交和回滚.
但是,这种方式的配置实用吗? 有什么缺点?
缺点蛮大的.
两个数据源还好说,更多的数据源要多少配置
每个方法都是两个事务 ,大部分别的业务方法,没有都操作两个数据源,岂不是多余?
方法上莫名添加了多个数据源,性能还能保证吗?
这种配置方法,是理论上可行的.如果您只有少量的多数据源 ,并且不太考虑性能 ,是可以参考 .要看用户怎么取舍 .要不舍弃事务 用动态数据源.要么使用事务,牺牲一些灵活性和性能.
其实很多情况可以绕过多数据源问题,多数据源必然引入不可控的复杂性,可以考虑项目拆分,利用微服务的概念,根据不同的数据源使用不同的app提供rpc服务,或者多数据源保持在一个mysql实例中(如果可能的话),这样sql语句加上schema也能使用一个mysql连接访问到多个数据源.
总的来说 ,针对同一个问题 解决方案挺多的 .要么就是直接面对,要么就是迂回包抄,要么就是绕过问题. 解决方案根据不同的环境因素(人力,时间,项目复杂度,维护成本)必定会评估出不同的结果.重点是:遇到的问题解决了
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连接访问到多个数据源.
总的来说 ,针对同一个问题 解决方案挺多的 .要么就是直接面对,要么就是迂回包抄,要么就是绕过问题. 解决方案根据不同的环境因素(人力,时间,项目复杂度,维护成本)必定会评估出不同的结果.重点是:遇到的问题解决了
相关文章推荐
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring动态切换多数据源事务开启后,动态数据源切换失效解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- spring动态数据源和事务配合的调研
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- Spring配置多数据源在配置事务后无效完美解决方案
- spring动态数据源和事务配合的调研
- spring3.0事务的多数据源的annotation-driven用法
- Spring多数据源解决方案
- Spring和hibernate多个数据源的事务管理
- Spring+Mybatis整合事务不起作用之解决方案汇总
- Spring3+Hibernate3(Jpa) 配置多个数据源的解决方案(基于注解)