GVKun编程网logo

由工作问题到Mybatis缓存与Spring事务管理(请简述mybatis的工作执行流程)

15

本文将带您了解关于由工作问题到Mybatis缓存与Spring事务管理的新内容,同时我们还将为您解释请简述mybatis的工作执行流程的相关知识,另外,我们还将为您提供关于Mybaits源码解析(十二

本文将带您了解关于由工作问题到Mybatis缓存与Spring事务管理的新内容,同时我们还将为您解释请简述mybatis的工作执行流程的相关知识,另外,我们还将为您提供关于Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?、MyBatis 不支持 Spring 事务管理的问题总结、MyBatis 在 Spring 中的事务管理、mybatis-spring事务处理机制分析的实用信息。

本文目录一览:

由工作问题到Mybatis缓存与Spring事务管理(请简述mybatis的工作执行流程)

由工作问题到Mybatis缓存与Spring事务管理(请简述mybatis的工作执行流程)

 太长不看 人士直接到  结论分析;

 

问题背景:

    项目使用SpringBoot+SpringMVC+Mybatis框架

    工作中遇到一个工作流向外同步的问题,在本地工作流操作完之后,调用接口推动其他平台的工作流流转。

  在本地工作流操作完之后,数据库中业务数据对应的工作流状态会发生变化,比如auditStatus从0转为1等。

主要现象:

    工作流的本地操作调用系统的公用接口,在本地工作流操作完之后(已经更改了数据的流程状态),调用dao层根据id去数据库检索该条业务数据。

  这时候意料之外的情况出现了,检索出的数据中流程状态仍是工作流未操作之前的

 

问题分析:

    像这种情况,首先想到的就是Spring的事务,具体点就是事务的传播机制(propagation)和数据隔离级别(ioslation)。代码结构如下

      @Override
      @Transactional(propagation = Propagation.required)
      public void executeSth(Student student) {

        Student origin = studentServiceStrong.getById(student.getId());
        System.out.println("before update:"+origin);
        ShitHandler handler = new ShitHandler();
        int i = handler.updateSomeShitByIdWithNewBean(student);
        Student dbRec = studentServiceStrong.getById(student.getId());

      }

     生产环境中的代码是使用的全局事务配置,没有直接加注解,我在这里为了直观写上了等价的注解,

     handler.updateSomeShitByIdWithNewBean(student);这一句的内部操作事务的Propagation为REQUIRES_NEW.


     现场库使用的是MysqL。下来分析的时候,ioslation走的innodb默认的repeatable read,按理不会出现这种情况,因为外层事务读取的时候内层事务已经提交了。
     在深入update方法内部看,原来在这个公共接口中,在新事务创建之前,有对该条数据根据id进行检索,此次检索与更新完之后的检索在同一事务中,根据repeatable read的特点,

        两次检索的结果一样,即没有检索到更新,是解释的通的。所以这里先将ioslation设置为read committed,理论上就可以让后一次select检索到数据了。

 

     但是,可怕的但是,系统支持多数据源,在本地用oracle库进行测试时,也出现了同样的问题,oracle默认隔离级别为read committed,按理说最后一次检索应该能检索到更新。

     所以我们发现,并不是ioslation的问题,起码不全是ioslation的问题。还有什么可能呢,mybatis缓存。

结论分析

    在以上代码中,MysqL中的同一事务的两次检索结果不一致,最直接的原因并不是repeatable read的特性导致,因为这里其实后一次并未真正落到数据库(oracle也是)---日志并没有对应的sql输出,而是读了Mybatis的一级缓存
    当我们将缓存禁用后,这时在MysqL中的不一致才是repeatable read特性导致的(此时在oracle已经正常)。

 

    Mybatis一级缓存默认开启,在开启时,同一次会话(session)中的第一次检索的对应数据会进行缓存,缓存对应的规则可以参考这篇文章: https://www.cnblogs.com/happyflyingpig/p/7739749.html
    众所周知,一级缓存在数据被update时会被对应的移除,但是,实践显示,有一个非常重要的前提:select和之前update在同一事务中,如果在不同的事务中,mybatis不会失效对应的缓存    

     关闭Mybatis一级缓存:

      全局关闭:

        mybatis.configuration.local-cache-scope=statement
      某一次检索关闭:

        在对应的statement中添加  flushCache="true"

     


   

    

    加个题外话,很多人说new 出来的对象中的事务spring不能控制,其实这样说是不全面的,这里挖个坑,以后有时间了专门开一篇说。

解决方案:    

    因此,最后的解决方案为,在select的statement中加上 flushCache="true" ,且将几个相关方法的ioslation设置为read committed

 

 

 对应资源:

    自己写的小demo,有兴趣可以跑跑看   https://gitee.com/uptotwo/ssm

 

Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?

Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?

不知道一些同学有没有这种疑问,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?那么Mybatis和Spring事务中用的Connection是同一个吗?我们常用配置如下

<!--会话工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

<!--spring事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

<!--使用注释事务 -->
<tx:annotation-driven  transaction-manager="transactionManager" />

看到没,sqlSessionFactory中配置了dataSource,transactionManager也配置了dataSource,我们来回忆一下SqlSessionFactoryBean这个类

 1 protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
 2 
 3     // 配置类
 4    Configuration configuration;
 5     // 解析mybatis-Config.xml文件,
 6     // 将相关配置信息保存到configuration
 7    XMLConfigBuilder xmlConfigBuilder = null;
 8    if (this.configuration != null) {
 9      configuration = this.configuration;
10      if (configuration.getVariables() == null) {
11        configuration.setVariables(this.configurationProperties);
12      } else if (this.configurationProperties != null) {
13        configuration.getVariables().putAll(this.configurationProperties);
14      }
15     //资源文件不为空
16    } else if (this.configLocation != null) {
17      //根据configLocation创建xmlConfigBuilder,XMLConfigBuilder构造器中会创建Configuration对象
18      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
19      //将XMLConfigBuilder构造器中创建的Configuration对象直接赋值给configuration属性
20      configuration = xmlConfigBuilder.getConfiguration();
21    } 
22    
23     //略....
24 
25    if (xmlConfigBuilder != null) {
26      try {
27        //解析mybatis-Config.xml文件,并将相关配置信息保存到configuration
28        xmlConfigBuilder.parse();
29        if (LOGGER.isDebugEnabled()) {
30          LOGGER.debug("Parsed configuration file: ''" + this.configLocation + "''");
31        }
32      } catch (Exception ex) {
33        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
34      }
35    }
36     
37    if (this.transactionFactory == null) {
38      //事务默认采用SpringManagedTransaction,这一块非常重要
39      this.transactionFactory = new SpringManagedTransactionFactory();
40    }
41     // 为sqlSessionFactory绑定事务管理器和数据源
42     // 这样sqlSessionFactory在创建sqlSession的时候可以通过该事务管理器获取jdbc连接,从而执行SQL
43    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
44     // 解析mapper.xml
45    if (!isEmpty(this.mapperLocations)) {
46      for (Resource mapperLocation : this.mapperLocations) {
47        if (mapperLocation == null) {
48          continue;
49        }
50        try {
51          // 解析mapper.xml文件,并注册到configuration对象的mapperRegistry
52          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
53              configuration, mapperLocation.toString(), configuration.getSqlFragments());
54          xmlMapperBuilder.parse();
55        } catch (Exception e) {
56          throw new NestedIOException("Failed to parse mapping resource: ''" + mapperLocation + "''", e);
57        } finally {
58          ErrorContext.instance().reset();
59        }
60 
61        if (LOGGER.isDebugEnabled()) {
62          LOGGER.debug("Parsed mapper file: ''" + mapperLocation + "''");
63        }
64      }
65    } else {
66      if (LOGGER.isDebugEnabled()) {
67        LOGGER.debug("Property ''mapperLocations'' was not specified or no matching resources found");
68      }
69    }
70 
71     // 将Configuration对象实例作为参数,
72     // 调用sqlSessionFactoryBuilder创建sqlSessionFactory对象实例
73    return this.sqlSessionFactoryBuilder.build(configuration);
74 }

我们看第39行,Mybatis集成Spring后,默认使用的transactionFactory是SpringManagedTransactionFactory,那我们就来看看其获取Transaction的方法

private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
      boolean autoCommit;
      try {
        autoCommit = connection.getAutoCommit();
      } catch (SQLException e) {
        // Failover to true, as most poor drivers
        // or databases won''t support transactions
        autoCommit = true;
      }      
      //从configuration中取出environment对象
      final Environment environment = configuration.getEnvironment();
      //从environment中取出TransactionFactory
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      //创建Transaction
      final Transaction tx = transactionFactory.newTransaction(connection);
      //创建包含事务操作的执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      //构建包含执行器的SqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
}

private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
    if (environment == null || environment.getTransactionFactory() == null) {
      return new ManagedTransactionFactory();
    }
    //这里返回SpringManagedTransactionFactory
    return environment.getTransactionFactory();
}

@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
    //创建SpringManagedTransaction
    return new SpringManagedTransaction(dataSource);
}

SpringManagedTransaction

也就是说mybatis的执行事务的事务管理器就切换成了SpringManagedTransaction,下面我们再去看看SpringManagedTransactionFactory类的源码:

public class SpringManagedTransaction implements Transaction {
    private static final Log LOGGER = LogFactory.getLog(SpringManagedTransaction.class);
    private final DataSource dataSource;
    private Connection connection;
    private boolean isConnectionTransactional;
    private boolean autoCommit;

    public SpringManagedTransaction(DataSource dataSource) {
        Assert.notNull(dataSource, "No DataSource specified");
        this.dataSource = dataSource;
    }

    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            this.openConnection();
        }

        return this.connection;
    }

    private void openConnection() throws SQLException {
        //通过DataSourceUtils获取connection,这里和JdbcTransaction不一样
        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 void commit() throws SQLException {
        if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
            }
            //通过connection提交,这里和JdbcTransaction一样
            this.connection.commit();
        }

    }

    public void rollback() throws SQLException {
        if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
            }
            //通过connection回滚,这里和JdbcTransaction一样
            this.connection.rollback();
        }

    }

    public void close() throws SQLException {
        DataSourceUtils.releaseConnection(this.connection, this.dataSource);
    }

    public Integer getTimeout() throws SQLException {
        ConnectionHolder holder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.dataSource);
        return holder != null && holder.hasTimeout() ? holder.getTimeToLiveInSeconds() : null;
    }
}

org.springframework.jdbc.datasource.DataSourceUtils#getConnection

public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
    try {
        return doGetConnection(dataSource);
    }
    catch (SQLException ex) {
        throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
    }
}

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    Assert.notNull(dataSource, "No DataSource specified");
    //TransactionSynchronizationManager重点!!!有没有很熟悉的感觉??
    //还记得我们前面Spring事务源码的分析吗?@Transaction会创建Connection,并放入ThreadLocal中
    //这里从ThreadLocal中获取ConnectionHolder
    ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
    if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
        logger.debug("Fetching JDBC Connection from DataSource");
        //如果没有使用@Transaction,那调用Mapper接口方法时,也是通过Spring的方法获取Connection
        Connection con = fetchConnection(dataSource);
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            logger.debug("Registering transaction synchronization for JDBC Connection");
            ConnectionHolder holderToUse = conHolder;
            if (conHolder == null) {
                holderToUse = new ConnectionHolder(con);
            } else {
                conHolder.setConnection(con);
            }

            holderToUse.requested();
            TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
            holderToUse.setSynchronizedWithTransaction(true);
            if (holderToUse != conHolder) {
                //将获取到的ConnectionHolder放入ThreadLocal中,那么当前线程调用下一个接口,下一个接口使用了Spring事务,那Spring事务也可以直接取到Mybatis创建的Connection
                //通过ThreadLocal保证了同一线程中Spring事务使用的Connection和Mapper代理类使用的Connection是同一个
                TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
            }
        }

        return con;
    } else {
        conHolder.requested();
        if (!conHolder.hasConnection()) {
            logger.debug("Fetching resumed JDBC Connection from DataSource");
            conHolder.setConnection(fetchConnection(dataSource));
        }

        //所以如果我们业务代码使用了@Transaction注解,在Spring中就已经通过dataSource创建了一个Connection并放入ThreadLocal中
        //那么当Mapper代理对象调用方法时,通过SqlSession的SpringManagedTransaction获取连接时,就直接获取到了当前线程中Spring事务创建的Connection并返回
        return conHolder.getConnection();
    }
}

想看怎么获取connHolder 

org.springframework.transaction.support.TransactionSynchronizationManager#getResource

//保存数据库连接的ThreadLocal
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
@Nullable
public static Object getResource(Object key) {
    Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
    //获取ConnectionHolder
    Object value = doGetResource(actualKey);
    ....
    return value;
}

@Nullable
private static Object doGetResource(Object actualKey) {
    /**
     * 从threadlocal <Map<Object, Object>>中取出来当前线程绑定的map
     * map里面存的是<dataSource,ConnectionHolder>
     */
    Map<Object, Object> map = resources.get();
    if (map == null) {
        return null;
    }
    //map中取出来对应dataSource的ConnectionHolder
    Object value = map.get(actualKey);
    // Transparently remove ResourceHolder that was marked as void...
    if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
        map.remove(actualKey);
        // Remove entire ThreadLocal if empty...
        if (map.isEmpty()) {
            resources.remove();
        }
        value = null;
    }
    return value;
}

我们看到直接从ThreadLocal中取出来的conn,而spring自己的事务也是操作的这个ThreadLocal中的conn来进行事务的开启和回滚,由此我们知道了在同一线程中Spring事务中的Connection和Mybaits中Mapper代理对象中操作数据库的Connection是同一个,当取出来的conn为空时候,调用org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取,然后把从数据源取出来的连接返回

private static Connection fetchConnection(DataSource dataSource) throws SQLException {
    //从数据源取出来conn
    Connection con = dataSource.getConnection();
    if (con == null) {
        throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
    }
    return con;
}

我们再来回顾一下上篇文章中的SqlSessionInterceptor

 1 private class SqlSessionInterceptor implements InvocationHandler {
 2     private SqlSessionInterceptor() {
 3     }
 4 
 5     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 6         SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
 7 
 8         Object unwrapped;
 9         try {
10             Object result = method.invoke(sqlSession, args);
11             // 如果当前操作没有在一个Spring事务中,则手动commit一下
12             // 如果当前业务没有使用@Transation,那么每次执行了Mapper接口的方法直接commit
13             // 还记得我们前面讲的Mybatis的一级缓存吗,这里一级缓存不能起作用了,因为每执行一个Mapper的方法,sqlSession都提交了
14             // sqlSession提交,会清空一级缓存
15             if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
16                 sqlSession.commit(true);
17             }
18 
19             unwrapped = result;
20         } catch (Throwable var11) {
21             unwrapped = ExceptionUtil.unwrapThrowable(var11);
22             if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
23                 SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
24                 sqlSession = null;
25                 Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
26                 if (translated != null) {
27                     unwrapped = translated;
28                 }
29             }
30 
31             throw (Throwable)unwrapped;
32         } finally {
33             if (sqlSession != null) {
34                 SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
35             }
36 
37         }
38         return unwrapped;
39     }
40 }

看第15和16行,如果我们没有使用@Transation,Mapper方法执行完后,sqlSession将会提交,也就是说通过org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取到的Connection将会commit,相当于Connection是自动提交的,也就是说如果不使用@Transation,Mybatis将没有事务可言。

Mybatis和Spring整合后SpringManagedTransaction和Spring的Transaction的关系:

  • 如果开启Spring事务,则先有Spring的Transaction,然后mybatis创建sqlSession时,会创建SpringManagedTransaction并加入sqlSession中,SpringManagedTransaction中的connection会从Spring的Transaction创建的Connection并放入ThreadLocal中获取
  • 如果没有开启Spring事务或者第一个方法没有事务后面的方法有事务,则SpringManagedTransaction创建Connection并放入ThreadLocal中

spring结合mybatis后mybaits一级缓存失效分为两种情况:

  • 如果没有开启事务,每一次sql都是用的新的SqlSession,这时mybatis的一级缓存是失效的。
  • 如果有事务,同一个事务中相同的查询使用的相同的SqlSessioon,此时一级缓存是生效的。

如果使用了@Transation呢?那在调用Mapper代理类的方法之前就已经通过Spring的事务生成了Connection并放入ThreadLocal,并且设置事务不自动提交,当前线程多个Mapper代理对象调用数据库操作方法时,将从ThreadLocal获取Spring创建的connection,在所有的Mapper方法调用完后,Spring事务提交或者回滚,到此mybatis的事务是怎么被spring管理的就显而易见了

还有文章开头的问题,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?

因为Spring事务在没调用Mapper方法之前就需要开一个Connection,并设置事务不自动提交,那么transactionManager中自然要配置dataSource。那如果我们的Service没有用到Spring事务呢,难道就不需要获取数据库连接了吗?当然不是,此时通过SpringManagedTransaction调用org.springframework.jdbc.datasource.DataSourceUtils#getConnection#fetchConnection方法获取,并将dataSource作为参数传进去,实际上获取的Connection都是通过dataSource来获取的。

 

原文出处:https://www.cnblogs.com/java-chen-hao/p/11839993.html

MyBatis 不支持 Spring 事务管理的问题总结

MyBatis 不支持 Spring 事务管理的问题总结

之前有个同事问我, 关于 MyBatis 不支持 Spring 事务管理的问题:
对 spring,mybatis 进行整合时发现事务不能进行回滚处理,上网查了很多资料依旧还没解释,很多都是说要抛出一个 runtimeException 才能回滚的,但尝试过多种还不能。

这里我给出我的解决方法和答案,我自己尝试成功
我用的是 SSM3 的框架 Spring MVC 3.1 + Spring 3.1 + Mybatis3.1 + Oracle 数据库

第一种情况
Spring MVC 和 Spring 整合的时候,SpringMVC 的 springmvc.xml 文件中 配置扫描包,不要包含 service 的注解,Spring 的 applicationContext.xml 文件中 配置扫描包时,不要包含 controller 的注解,如下所示:
SpringMVC 的 xml 配置:

<context:component-scan base-package="com.insigma">

  <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>

</context:component-scan>

Spring MVC 启动时的配置文件,包含组件扫描、url 映射以及设置 freemarker 参数,让 spring 不扫描带有 @Service 注解的类。为什么要这样设置?因为 springmvc.xml 与 applicationContext.xml 不是同时加载,如果不进行这样的设置,那么,spring 就会将所有带 @Service 注解的类都扫描到容器中,等到加载 applicationContext.xml 的时候,会因为容器已经存在 Service 类,使得 cglib 将不对 Service 进行代理,直接导致的结果就是在 applicationContext 中的事务配置不起作用,发生异常时,无法对数据进行回滚。以上就是原因所在。
同样的在 Spring 的 xml 配置如下:

<context:component-scan base-package="com.insigma">           

 <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>

</context:component-scan>

扫描包路径,不扫描带有 @Controller 注解的类。因为这些类已经随容器启动时,在 springmvc.xml 中扫描过一遍了。
完成以上工作,我的 Spring 就支持事务回滚了。OK

第二种情况:使用了 MySQL 数据库,如果用 mysql 数据库,数据库表你如果是自动建表,那么就需要把建表的 Engine 设置为 InnoDB 格式,自动建表的格式为:MyISAM,这中格式的是不支持事务管理的。 

第三种情况:在测试代码中,一定要在 catch 块中抛出 异常 ,以便 Spring 事务能发现异常。

注意以上几点就 OK 了。

MyBatis 在 Spring 中的事务管理

MyBatis 在 Spring 中的事务管理

项目中经常遇到 MyBatis 与 Spring 的组合开发,并且相应的事务管理交给 Spring。今天我这里记录一下 Spring 中 Mybatis 的事务管理。

先看代码:

spring-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--开启注解-->
    <context:annotation-config/>
    <!--加载属性文件-->
    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
        <property name="locations">
            <list>
                <value>classpath:db.properties</value>
            </list>
        </property>
    </bean>
    <!--扫描组建-->
    <context:component-scan base-package="com.xwszt.txdemo"/>
    <!--开启事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

    <aop:aspectj-autoproxy proxy-target-class="true"/>

    <!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${mysql.jdbc.url}"/>
        <property name="user" value="${mysql.jdbc.user}"/>
        <property name="password" value="${mysql.jdbc.password}"/>
        <!--Connection Pooling Info -->
        <property name="initialPoolSize" value="3"/>
        <property name="minPoolSize" value="2"/>
        <property name="maxPoolSize" value="15"/>
        <property name="acquireIncrement" value="3"/>
        <property name="maxStatements" value="8"/>
        <property name="maxStatementsPerConnection" value="5"/>
        <property name="maxIdleTime" value="1800"/>
        <property name="autoCommitOnClose" value="false"/>
    </bean>

    <!--mybatis配置-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="mapperLocations" value="classpath:mapper/*"/>
    </bean>
    <!--mybatis扫描mapper对应类的配置-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.xwszt.txdemo.dao"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>
    <!--事务配置-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

db.properties

##mysql
jdbc.driver=com.mysql.jdbc.Driver
mysql.jdbc.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&useAffectedRows=true&allowPublicKeyRetrieval=true
mysql.jdbc.user=root
mysql.jdbc.password=*********(这里根据自己修改)

db.sql

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT NULL,
  `password` varchar(50) DEFAULT NULL,
  `salt` varchar(50) DEFAULT NULL,
  `sex` varchar(10) DEFAULT NULL,
  `address` varchar(50) DEFAULT NULL,
  `cellphone` varchar(30) DEFAULT NULL,
  `email` varchar(30) DEFAULT NULL,
  `islock` smallint(1) unsigned NOT NULL DEFAULT ''0'',
  `isvalidate` smallint(1) unsigned NOT NULL DEFAULT ''1'',
  `isdel` smallint(1) unsigned NOT NULL DEFAULT ''0'',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=124 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

SET FOREIGN_KEY_CHECKS = 1

UserDAO.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xwszt.txdemo.dao.UserDAO">
    <insert id="insert" parameterType="com.xwszt.txdemo.entities.User">
        insert into `user`
        (`id`, `username`, `password`, `salt`, `sex`, `address`, `cellphone`, `email`, `islock`,`isvalidate`,`isdel`)
        values
        (#{id}, #{username},#{password},#{salt},#{sex},#{address},#{cellphone},#{email},#{lock},#{validate},#{del})
    </insert>
</mapper>

UserDAO.java

package com.xwszt.txdemo.dao;

import com.xwszt.txdemo.entities.User;

public interface UserDAO {
    void insert(User user);
}

User.java

package com.xwszt.txdemo.entities;

import lombok.Data;

import java.io.Serializable;

@Data
public class User implements Serializable {
    private Long id;
    private String username;
    private String password;
    private String salt;
    private String sex;
    private String address;
    private String cellphone;
    private String email;
    private boolean lock;
    private boolean validate;
    private boolean del;
}

UserService.java

package com.xwszt.txdemo.service;

public interface UserService {
    void doSomething()  throws Exception;
    boolean saveUser() throws Exception;
}

UserServiceImpl.java

package com.xwszt.txdemo.service.impl;

import com.xwszt.txdemo.dao.UserDAO;
import com.xwszt.txdemo.entities.User;
import com.xwszt.txdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserService target;

    @Autowired
    private UserDAO userDAO;

    @Override
    public void doSomething() throws Exception {
        target.saveUser();

    }

    @Transactional
    @Override
    public boolean saveUser() throws Exception {

        User user = new User();
        user.setId(123l);
        user.setUsername("zhangsan");
        user.setPassword("123");
        user.setSalt("456");
        user.setSex("FEMAIL");
        user.setAddress("上海市张江高科");
        user.setCellphone("13582911229");
        user.setEmail("978732467@qq.com");
        user.setLock(false);
        user.setValidate(true);
        user.setDel(false);

        userDAO.insert(user);
        
        return true;
    }
}

UserTest.java

package com.xwszt.txdemo;

import com.xwszt.txdemo.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-content.xml")
public class UserTest {

    @Autowired
    private UserService userService;

    @Test
    public void saveUserTest() {
        try {
            userService.doSomething();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

到此为止,代码已经贴完了。

那么,问题在哪儿呢?

在 Service 层,如果你仔细看,发现在 service 层 saveUser 方法上加了注解 @Transactional,那么运行测试代码,不出意外(网络断了等)的情况下数据库里肯定会插入一条数据。

假如,我把 @Transactional 这个注解去掉了,也就是说 saveUser 不再使用 spring 的事务管理了,那么数据库里是不是没有插入数据呢?答案是否定的。数据库里依然会插入一条数据

那这又是为什么呢?

如果使用 Spring 进行事务管理,这里提交的时候是 Spring 的事务管理 commit 了事务。

在没有使用 Spring 管理的事务时,是没有使用 Spring 容器管理的 SqlSession 提交了事务。

==================================================

接下来,一个新的问题。

在 saveUser 方法上使用了 @Transactional 注解,表明这个方法是 Spring 容器管理的事务,那么我在 userDAO.insert (user); 之后抛出异常,那么插入的数据会回滚吗?

    @Transactional
    @Override
    public boolean saveUser() throws Exception {

        User user = new User();
        user.setId(123l);
        user.setUsername("zhangsan");
        user.setPassword("123");
        user.setSalt("456");
        user.setSex("FEMAIL");
        user.setAddress("上海市张江高科");
        user.setCellphone("13582911229");
        user.setEmail("978732467@qq.com");
        user.setLock(false);
        user.setValidate(true);
        user.setDel(false);

        userDAO.insert(user);

        if (true) {
            throw new Exception("破坏性测试");
        }
        return true;
    }

答案是:不会回滚。

那怎样才会回滚呢?配置 rollback 即可。即:

@Transactional(rollbackFor = Exception.class)

 

mybatis-spring事务处理机制分析

mybatis-spring事务处理机制分析

Spring官方并没有提供对MyBatis的集成方案,于是MyBatis项目组自己写了一个项目mybatis-spring专门用于在spring中使用MyBatis。

mybatis-spring的实现很大程度上依赖spring jdbc的事务管理,所以我们先看一下在spring中直接使用jdbc访问数据库时是如何处理事务的。无论你是使用@Transactional注解这样的AOP配置方式,还是TransactionTemplate这样的编码方式,最终执行的操作事务的代码都会是类似下面这样

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
PlatformTransactionManager txManager = new DataSourceTransactionManager(dataSource);

TransactionStatus status = txManager.getTransaction(def);
try {
    //get jdbc connection...
    //execute sql...

    txManager.commit(status);
}
catch (Exception e) {
    txManager.rollback(status);
    throw e;
}

 可以看到PlatformTransactionManager的getTransaction(), rollback(), commit()是spring处理事务的核心api,分别对应事务的开始,提交和回滚。

spring事务处理的一个关键是保证在整个事务的生命周期里所有执行sql的jdbc connection和处理事务的jdbc connection始终是同一个。然后执行sql的业务代码一般都分散在程序的不同地方,如何让它们共享一个jdbc connection呢?这里spring做了一个前提假设:即一个事务的操作一定是在一个thread中执行,且一个thread中如果有多个不同jdbc connection生成的事务的话,他们必须顺序执行,不能同时存在。(这个假设在绝大多数情况下都是成立的)。基于这个假设,spring在transaction创建时,会用ThreadLocal把创建这个事务的jdbc connection绑定到当前thread,接下来在事务的整个生命周期中都会从ThreadLocal中获取同一个jdbc connection。

我们看一下详细调用过程

  • TransactionSynchronizationManager负责从ThreadLocal中存取jdbc connection
  • 创建事务的时候会通过dataSource.getConnection()获取一个新的jdbc connection,然后绑定到ThreadLocal
  • 在业务代码中执行sql时,通过DataSourceUtils.getConnection()从ThreadLocal中获取当前事务的jdbc connection, 然后在该jdbc connection上执行sql
  • commit和rollback事务时,从ThreadLocal中获取当前事务的jdbc connection,然后对该jdbc connection进行commit和rollback

对spring jdbc的事务处理有了了解后,我们来看mybatis是如何通过spring处理事务的。

先看一下配置

<bean id="transactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

<bean id="sqlSessionFactory">
  <property name="dataSource" ref="dataSource" />
  <property name="transactionFactory">
    <bean/>
  </property> 
</bean>

<bean id="sqlSession">
  <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
  • mybatis-spring依赖DataSourceTransactionManager来处理事务,并没有创建自己的PlatformTransactionManager实现。
  • mybatis通过SqlSessionFactoryBuilder创建SqlSessionFactory,而mybatis-spring通过SqlSessionFactoryBean创建SqlSessionFactory。
  • 配置使用SpringManagedTransactionFactory来创建MyBatis的Transaction实现SpringManagedTransaction
  • 配置使用SqlSessionTemplate代替通过SqlSessionFactory.openSession()获取SqlSession

然后看其调用过程

可以看到mybatis-spring处理事务的主要流程和spring jdbc处理事务并没有什么区别,都是通过DataSourceTransactionManager的getTransaction(), rollback(), commit()完成事务的生命周期管理,而且jdbc connection的创建也是通过DataSourceTransactionManager.getTransaction()完成,mybatis并没有参与其中,mybatis只是在执行sql时通过DataSourceUtils.getConnection()获得当前thread的jdbc connection,然后在其上执行sql。

 

下面结合代码来看

<SqlSessionUtils>:  

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }

    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }


  private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
    SqlSessionHolder holder;
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      Environment environment = sessionFactory.getConfiguration().getEnvironment();

      if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
        }

        holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
        TransactionSynchronizationManager.bindResource(sessionFactory, holder);
        TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        holder.requested();
      } else {
        if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
          }
        } else {
          throw new TransientDataAccessResourceException(
              "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
      }
    }
}

执行sql时调用sqlSessionTemplate的insert,update,delete方法,sqlSessionTemplate是DefaultSqlSession的一个代理类,它通过SqlSessionUtils.getSqlSession()试图从ThreadLocal获取当前事务所使用的SqlSession。如果是第一次获取时会调用SqlSessionFactory.openSession()创建一个SqlSession并绑定到ThreadLocal,同时还会通过TransactionSynchronizationManager注册一个SqlSessionSynchronization。

<SqlSessionSynchronization>:

 public void beforeCommit(boolean readOnly) {
      // Connection commit or rollback will be handled by ConnectionSynchronization or
      // DataSourceTransactionManager.
      // But, do cleanup the SqlSession / Executor, including flushing BATCH statements so
      // they are actually executed.
      // SpringManagedTransaction will no-op the commit over the jdbc connection
      // TODO This updates 2nd level caches but the tx may be rolledback later on! 
      if (TransactionSynchronizationManager.isActualTransactionActive()) {
        try {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Transaction synchronization committing SqlSession [" + this.holder.getSqlSession() + "]");
          }
          this.holder.getSqlSession().commit();
        } catch (PersistenceException p) {
          if (this.holder.getPersistenceExceptionTranslator() != null) {
            DataAccessException translated = this.holder
                .getPersistenceExceptionTranslator()
                .translateExceptionIfPossible(p);
            if (translated != null) {
              throw translated;
            }
          }
          throw p;
        }
      }

SqlSessionSynchronization是一个事务生命周期的callback接口,mybatis-spring通过SqlSessionSynchronization在事务提交和回滚前分别调用DefaultSqlSession.commit()和DefaultSqlSession.rollback()

<BaseExecutor>:

public void commit(boolean required) throws SQLException {
    if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }

  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }
<SpringManagedTransaction>:

this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);  

  public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
      }
      this.connection.commit();
    }
  }

  public void rollback() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
      }
      this.connection.rollback();
    }
  }
	<DataSourceUtils>:

	/**
	 * Determine whether the given JDBC Connection is transactional, that is,
	 * bound to the current thread by Spring''s transaction facilities.
	 * @param con the Connection to check
	 * @param dataSource the DataSource that the Connection was obtained from
	 * (may be {@code null})
	 * @return whether the Connection is transactional
	 */
	public static boolean isConnectionTransactional(Connection con, DataSource dataSource) {
		if (dataSource == null) {
			return false;
		}
		ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
		return (conHolder != null && connectionEquals(conHolder, con));
	}

这里的DefaultSqlSession只会进行一些自身缓存的清理工作,并不会真正提交事务给数据库,原因是这里的DefaultSqlSession使用的Transaction实现为SpringManagedTransaction,SpringManagedTransaction在提交事务前会检查当前事务是否应该由spring控制,如果是,则不会自己提交事务,而将提交事务的任务交给spring,所以DefaultSqlSession并不会自己处理事务。

<SpringManagedTransaction>: 

 public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  /**
   * Gets a connection from Spring transaction manager and discovers if this
   * {@code Transaction} should manage connection or let it to Spring.
   * <p>
   * It also reads autocommit setting because when using Spring Transaction MyBatis
   * thinks that autocommit is always false and will always call commit/rollback
   * so we need to no-op that calls.
   */
  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");
    }
  }

DefaultSqlSession执行sql时,会通过SpringManagedTransaction调用DataSourceUtils.getConnection()从ThreadLocal中获取jdbc connection并在其上执行sql。

总结:mybatis-spring处理事务的主要流程和spring jdbc处理事务并没有什么区别,都是通过DataSourceTransactionManager的getTransaction(), rollback(), commit()完成事务的生命周期管理,而且jdbc connection的创建也是通过DataSourceTransactionManager.getTransaction()完成,mybatis并没有参与其中,mybatis只是在执行sql时通过DataSourceUtils.getConnection()获得当前thread的jdbc connection,然后在其上执行sql。

mybatis-spring做的最主要的事情是:

  1. 在SqlSession执行sql时通过用SpringManagedTransaction代替mybatis的JdbcTransaction,让SqlSession从spring的ThreadLocal中获取jdbc connection。
  2. 通过注册事务生命周期callback接口SqlSessionSynchronization,让SqlSession有机会在spring管理的事务提交或回滚时清理自己的内部缓存。

我们今天的关于由工作问题到Mybatis缓存与Spring事务管理请简述mybatis的工作执行流程的分享已经告一段落,感谢您的关注,如果您想了解更多关于Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?、MyBatis 不支持 Spring 事务管理的问题总结、MyBatis 在 Spring 中的事务管理、mybatis-spring事务处理机制分析的相关信息,请在本站查询。

本文标签: