MyBatis transaction management in Spring environment

MyBatis design idea is very simple, can be seen as a package for JDBC and provide a powerful dynamic SQL mapping function. But because it itself has some cache, transaction management and other functions, so the actual use will still encounter some problems - In addition, the recent exposure of JFinal, their thoughts and Hibernate similar, but more concise, and MyBatis different design ideas, but the same thing: all want to simplify development by compact design to maximize and improve performance - when it comes to performance, some time ago encountered two problems:

  1. Delete a record in a top method (upper DAO method), then insert a same primary key when recording, and will report primary key violation error.
  2. DAO methods of some projects the average execution time will be a number of other projects twice .

The first problem is that occasionally appear, in any case can not reproduce the experimental environment, through the analysis of MyBatis logic, are estimated to be two DAO got two different Connection, the second statement earlier than the first one is submitted, it led to a primary key violation, pending further analysis and verification. For the second question, we will try to find its root cause by analyzing the source code and experimental, mainly related to the following:

  1. Problem Description and analysis
  2. MyBatis loading process in a Spring environment
  3. MyBatis management of environmental affairs in Spring
  4. Experimental verification

Environmental Project

The whole system is micro-service architecture, discussed here, "project" means a separate service. A single frame is substantially project Spring + MyBatis, specific version as follows:

Spring 3.2.9/4.3.5 + Mybatis 3.2.6 + mybatis-spring 1.2.2 + mysql connector 5.1.20 + commons-dbcp 1.4

And transactions related to the MyBatis configuration is as follows:

// Code. 1 
<-! # The bean. 1 -> 
 <the bean ID = "the dataSource" class = "org.apache.commons.dbcp.BasicDataSource" 
        the destroy-Method = "Close"> 
        <-! Some database configuration information -> 
        <! - Some DBCP connection pool configuration -> 
         // where settings are automatically submitted 
        <Property name = "defaultAutoCommit" value = "$ {dbcp.defaultAutoCommit}" /> 
  </ the bean> 
<! - # 2 the bean -> 
  <the bean ID = "SqlSessionFactory" class = "org.mybatis.spring.SqlSessionFactoryBean"> 
        <Property name = "the dataSource" REF = "the dataSource" /> 
        <Property name = "the mapperLocations" value = "CLASSPATH *:. path / to / mapper / ** / * xml "/>
  </bean>
<!-- bean#3 -->
  <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
  </bean>
<!-- bean#4-->
  <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value=".path.to.mapper" />
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
  </bean>
 <!-- bean5 -->
  <tx:annotation-driven transaction-manager="transactionManager" />

Problem Description and analysis

Double the time difference between the very serious, on average to each call, normally about 6-10 a few ms, slow to nearly 20 ms, due to the number of calls a lot, resulting in an overall performance will be very different. After careful than these few items, we found that the data source configuration DAO slow implementation of projects (bean # 1) in defaultAutoCommit configurations are false. And after this configuration to true restored to normal.

By inference MyBatis is in the "non-automatic submission" statement execution, wait, or submit more than once, leading to the actual number of API calls to the database increases. But this inference is also a problem, because the whole project is run in a Spring environment, but also opens the Spring's transaction management, they still need a detailed look at MyBatis in the end is how to assemble the DAO methods and management services, can be completely solved mystery.

To reproduce the problem

First, write a Service, which calls the two methods are the same mapper class twice, insertModelList () will insert two records in the database, delModels () method deletes these two records, as follows:

//代码2
//@Transactional
public void testIS(){
    List<Model> models= new ArrayList<>();
    //省略一些数据工作。。。
    modelMapper.insertModelList(50001l, models);
    modelMapper.delModels(50001);
    if (CollectionUtils.isNotEmpty(models))
        modelMapper.insertModelList(50001, models);
    modelMapper.delModels(50001);
}
public void testOther(){
    System.out.println("加载类:");
    System.out.println(modelMapper.getClass().getClassLoader());
    modelMapper.delModels(50001);
}

The actual project cat to use execution time statistics, there are also modeled cat, use a separate class that implements the computation time of AOP:

//代码3
public class DaoTimeAdvice {

  private long time = 0;
  private long num = 0;

  public Object calcTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long then = System.nanoTime();
    Object object = joinPoint.proceed();
    long now = System.nanoTime();
    setTime(getTime() + (now-then));
    setNum(getNum() + 1);
    return object;
  }
  //省略getter & setter。。。
  public void printInfo() {
    System.out.println("总共次数:" + num);
    System.out.println("总共时间:" + time);
    System.out.println("平均时间:" + time / num);
  }
}

Test code:

//代码4
public static void test(){
    System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]").format(new Date())
            + " 开始测试!");
    for (int i = 0; i < TEST_NUM; i++) {
        ItemStrategyServiceTest ist = (ItemStrategyServiceTest) context.getBean("isTS");
        ist.testIS();
        if (i % 1000 == 0) {
            System.out.println("1000次");
        }
    }
    DaoTimeAdvice ad = (DaoTimeAdvice) context.getBean("daoTimeAdvice");
    ad.printInfo();
    ItemStrategyServiceTest ist = (ItemStrategyServiceTest) context.getBean("isTS");
    ist.testOther();
    System.exit(1);
}

Test Results:

defaultAutoCommit Cycles Total elapsed time (ns) The average time (ns)
true 40000 17831088316 445777
true 40000 17881589992 447039
false 40000 27280458229 682011
false 40000 27237413893 680935

defaultAutoCommit for the execution time when the false is true of nearly 1.5 times and 2 times not to reproduce the time consumption, it is estimated that when the cat or perform other statistical AOP there are other methods of consumption, thus expanding between the false and true the difference.

MyBatis loading process in a Spring environment

In accordance with the first profile, bean MyBatis the entire assembly should be such DAO:

  1. BasicDataSource assembly using a first data source bean (bean # 1), name is dataSource.

    This bean is very simple, that is instantiated and registered to the context in Spring.

  2. DataSource use to create sqlSessionFactory (bean # 2), scans the statement MyBatis mapping file when the bean is created and resolved.

    In MyBatis, the real database read and write operations is achieved SqlSession by way of example, and to be managed by SqlSession SQLSessionFactory. org.mybatis.spring.SqlSessionFactoryBean here realized FactoryBean class (this class is special, regardless of the topic, not repeat them here), Spring will truly SQLSessionFactory obtains an instance of this bean, display the source code, the actual return the object is an instance of DefaultSqlSessionFactory.

  3. Used to create a factory class sqlSessionFactory mapper scanner (bean # 4), and create an instance method comprising DAO.

    In order for the upper method can be used DAO method is called by an ordinary method, you need to Spring context registered in the corresponding bean, but in common usage scenario of MyBatis is no implementation class of the mapper (specific SQL statement by mapping annotations or XML file to achieve), only the interface, in MyBatis these interfaces are implemented by a dynamic proxy. As used herein, the class is org.mybatis.spring.mapper.MapperScannerConfigurer, org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor it implements the interface, it will be "all in Spring bean definitions of all registration is complete, but not yet instantiated prior to "call the method implementation class (dynamic proxy objects) to the Spring context registered mapper. Specific code as follows:

     //代码5
     @Override
     public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
       if (this.processPropertyPlaceHolders) {
         processPropertyPlaceHolders();
       }
    
       ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
       //设置一些属性
    
       scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
     }
    
     /**
    * Perform a scan within the specified base packages.
    * @param basePackages the packages to check for annotated classes
    * @return number of beans registered
    */
     public int scan(String... basePackages) {
       int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    
       doScan(basePackages);
    
       // Register annotation config processors, if necessary.
       if (this.includeAnnotationConfig) {
         AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
       }
    
       return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
     }

    Can be seen in the source code, the real implementation class mapper is org.mybatis.spring.mapper.MapperFactoryBean <Object>, a specific logic in the process org.mybatis.spring.mapper.ClassPathMapperScanner.processBeanDefinitions (Set <BeanDefinitionHolder>) in . Finally, the implementation of each method, eventually falling into a certain method of org.mybatis.spring.SqlSessionTemplate, and the interceptor intercepts the following:

     //代码6
       /**
        * Proxy needed to route MyBatis method calls to the proper SqlSession got
        * from Spring's Transaction Manager
        * It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
        * pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
        */
     private class SqlSessionInterceptor implements InvocationHandler {
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       SqlSession sqlSession = getSqlSession(
           SqlSessionTemplate.this.sqlSessionFactory,
           SqlSessionTemplate.this.executorType,
           SqlSessionTemplate.this.exceptionTranslator);
       try {
         Object result = method.invoke(sqlSession, args);
         if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
           // force commit even on non-dirty sessions because some databases require
           // a commit/rollback before calling close()
           sqlSession.commit(true);
         }
         return result;
       } catch (Throwable t) {
         //省略一些错误处理
         throw unwrapped;
       } finally {
         if (sqlSession != null) {
           closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
         }
       }
     }
     }
  4. MyBatis management of environmental affairs in Spring

    Know the source code in real SqlSessionFactory using examples org.apache.ibatis.session.defaults.DefaultSqlSessionFactory the same time, transaction management using org.mybatis.spring.transaction.SpringManagedTransactionFactory. But in the configuration code 1, also added to configure Spring transaction management, that is, with the @Transactional comment on a Service method (or some other method that can be scanned), then Spring's transaction management is automatically created transaction, then MyBatis transaction between it and the collaboration is how it?

    Methods can be seen in the code 6 isSqlSessionTransactional (), which returns whether a Spring transaction upper code, if any, will not be executed below the commit (). The actual situation in my project is without Spring transaction, so definitely come to the following commit (), this method eventually fell SpringManagedTransactionFactory the commit (), look at the code:

     //代码7
     private void openConnection() throws SQLException {
       this.connection = DataSourceUtils.getConnection(this.dataSource);
       this.autoCommit = this.connection.getAutoCommit();
       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();
       }
     }

    It can be seen here if you want to execute commit () operation is determined by three variables, if the DataSource autoCommit is false, the result is certainly true, the console will see a line log: Committing JDBC Connection [xxxxxx] , just as in the case encountered in the project. This action is required to submit and database interaction, more time-consuming.

Experimental verification

The last one analysis, the cause of DAO method execution time is longer will be more a commit, then if the upper method is hosting Spring transaction manager (or data source defaultAutoCommit is true, this condition is already at the beginning of problems reproduced is verified), the operation of MyBatis not commit, the DAO method should respective execution time becomes shorter. So the method plus @transactional Service notes, test cases are true and false. result:

We can see the execution time has been basically close, which is almost certain due to this reason. There are still a number of puzzles, especially in no time consumed twice when the problem recurs, if you have other ideas, but also welcome to discuss.

Guess you like

Origin www.linuxidc.com/Linux/2019-08/159829.htm