本文用到的框架组合及其版本号

  • 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连接访问到多个数据源.

总的来说 ,针对同一个问题 解决方案挺多的 .要么就是直接面对,要么就是迂回包抄,要么就是绕过问题. 解决方案根据不同的环境因素(人力,时间,项目复杂度,维护成本)必定会评估出不同的结果.重点是:遇到的问题解决了