MyBatis is designed to be a simple encapsulation of JDBC and provides powerful dynamic SQL mapping capabilities. However, because it also has some caching, transaction management and other functions, so there will be some problems in the actual use — in addition, recently contacted with JFinal, its idea is similar to Hibernate, but more concise, and different from MyBatis design idea, but the same: The goal is to make development as simple as possible and improve performance as much as possible through simple design.

  1. Primary key conflict errors are reported when deleting a record in an upper-level method (the upper level of the DAO method) and then inserting a record with the same primary key.
  2. The average execution time of DAO methods in some projects is twice as long as in others.

The first problem is that it occasionally occurs and cannot be repeated in the experimental environment in any case. After analyzing the logic of MyBatis, it is estimated that two DAOs got two different connections respectively, and the second statement was submitted earlier than the first, resulting in primary key conflict, which needs further analysis and verification. For the second question, this paper will try to find its root cause through source code analysis and experiments, mainly involving the following contents:

  1. Problem description and analysis
  2. Loading process of MyBatis in Spring environment
  3. MyBatis transaction management in Spring environment
  4. Experimental verification

Project environment

The entire system is a microservice architecture, and the “project” in question here refers to a single service. The framework of a single project is basically Spring+MyBatis, and the specific version is 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

Configurations related to MyBatis and transactions are as follows:

// code 1 <! -- bean#1--> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <! -- Some database information configuration --> <! <property name="defaultAutoCommit" value="${dbcp.defaultAutoCommit}" /> </bean> <! -- bean#2--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="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" />Copy the code

Problem description and analysis

The time difference of doubling is quite serious, averaging to each call. The normal time is about 6 to 10 ms, while the slow time is nearly 20 ms. Due to the number of calls, the overall performance will have a great difference. A careful comparison of these projects shows that defaultAutoCommit is configured as false in the data source configuration (bean#1) of projects where the DAO executes slowly. And when you change this configuration to true, it’s back to normal.

Therefore, it can be inferred that when MyBatis executes the “non-automatic commit” statement, it waits or commits one more time, which leads to the increase of actual database API calls. However, there is a problem with this inference. Since the whole project is run in the Spring environment and Spring transaction management is enabled, it is necessary to take a detailed look at how MyBatis assembs DAO methods and manages transactions in order to thoroughly solve the mystery.

Problem reproduction

InsertModelList () will insert two records into the database, delModels() will delete the two records, code as follows:

// @transactional Public void testIS(){List<Model> models= new ArrayList<>(); // Omit some data work... modelMapper.insertModelList(50001l, models); modelMapper.delModels(50001); if (CollectionUtils.isNotEmpty(models)) modelMapper.insertModelList(50001, models); modelMapper.delModels(50001); } public void testOther(){system.out.println (" load class: "); System.out.println(modelMapper.getClass().getClassLoader()); modelMapper.delModels(50001); }Copy the code

In real projects, cat is used to calculate execution time. Here, a separate AOP class is used to calculate execution time, similar to CAT:

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; } // omit getter & setter... Public void printInfo() {system.out.println (" count: "+ num); System.out.println(" total time: "+ time); System.out.println(" average time: "+ time/num); }}Copy the code

Test code:

Public static void test(){system.out.println (new SimpleDateFormat("[YYYY-MM-DD HH: MM :ss]").format(new Date()) +" Start testing!" ); for (int i = 0; i < TEST_NUM; i++) { ItemStrategyServiceTest ist = (ItemStrategyServiceTest) context.getBean("isTS"); ist.testIS(); If (I % 1000 == 0) {system.out.println ("1000 times "); } } DaoTimeAdvice ad = (DaoTimeAdvice) context.getBean("daoTimeAdvice"); ad.printInfo(); ItemStrategyServiceTest ist = (ItemStrategyServiceTest) context.getBean("isTS"); ist.testOther(); System.exit(1); }Copy the code

Test results:

defaultAutoCommit cycles Total elapsed time (NS) Mean time (NS)
true

40000

17831088316

445777

true

40000

17881589992

447039

false

40000

27280458229

682011

false

40000

27237413893

680935

DefaultAutoCommit takes nearly 1.5 times as long to execute as true when it is false, and does not reproduce the double time consumption, presumably because there are other costs associated with cat statistics or other AOP methods, thus widening the difference between False and true.

Loading process of MyBatis in Spring environment

According to the configuration file in section 1, the assembly of DAO beans in MyBatis should look like this:

  1. Start by assembling a datasource bean (Bean #1) called BasicDataSource dataSource.

The bean is simply instantiated and registered into the Spring context.

  1. use dataSourceTo create a sqlSessionFactory(Bean #2), this bean will scan MyBatis statement mapping file and parse when created.

In MyBatis, real database reads and writes are performed using an instance of SqlSession, which is managed by the SQLSessionFactory. The org. Mybatis. Spring. SqlSessionFactoryBean implements FactoryBean class (the class is more special, has nothing to do with the theme, go here). Spring will get the actual SQLSessionFactory instance from this bean, and the source code shows that the actual object returned is an instance of DefaultSqlSessionFactory.

  1. use sqlSessionFactoryThis factory class creates a mapper scanner (bean#4) and creates instances that contain DAO methods.

In order to allow the upper layer method to use DAO method through ordinary method call, it is necessary to register the corresponding bean in the Spring context. In the common use scenario of MyBatis, there is no implementation class of Mapper (specific SQL statement mapping is realized through annotations or XML files), only interface, In MyBatis these interfaces are implemented by dynamic proxy. Classes used here is org. Mybatis. Spring. Mapper. MapperScannerConfigurer, It implements the org. Springframework. Beans. Factory. Support. BeanDefinitionRegistryPostProcessor interface, so in the Spring “all the bean definitions of registration is completed, Call the method to register the Mapper implementation class (dynamically brokered objects) with the Spring context before instantiating. The specific code is as follows:

/ / code 5 @ Override public void postProcessBeanDefinitionRegistry (BeanDefinitionRegistry registry) {if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); / / set some attributes scanner. The scan (StringUtils. TokenizeToStringArray (enclosing 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); }Copy the code

In the source code as you can see, the real mapper implementation class is org. Mybatis. Spring. Mapper. MapperFactoryBean, Specific logic in the method of org. Mybatis. Spring. Mapper. ClassPathMapperScanner. ProcessBeanDefinitions (Set). Finally, the implementation of each method, and eventually fell into the org. Mybatis. Spring. A method of SqlSessionTemplate, and have been following the interceptors to intercept:

** * 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) {// omits some errors throw unwrapped; } finally { if (sqlSession ! = null) { closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); }}}}Copy the code

  1. MyBatis transaction management in Spring environment

From the source code to know the real SqlSessionFactory use org. Apache. The ibatis. Session. Defaults. DefaultSqlSessionFactory instance, at the same time, Transaction management using org. Mybatis. Spring. Transaction. SpringManagedTransactionFactory. The Transactional annotation ona Service method (or some other method that can be scanned) automatically creates transactions. So how does it work with MyBatis transactions?

Methods can see in the code 6 isSqlSessionTransactional (), it will return to the upper code if there is a Spring of affairs, if there is, will not be at the bottom of the perform the commit (). Reality is no Spring in my project, so it must be reached the following the commit (), the method finally fell on SpringManagedTransactionFactory the commit (), the code:

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(); }}Copy the code

If the DataSource's autoCommit is false, the result must be true, and the console will see a line of log: Research suggests the same case in your project (research case of JDBC Connection [XXXXXX]). This submission action requires interaction with the database, which is time-consuming.

Experimental verification

From the analysis in the previous section, the DAO method takes longer to execute because one more commit is performed, so if the upper layer method is managed by the Spring transaction manager (or if the data source defaultAutoCommit is true, which has been verified in the initial problem recurrence), The commit action of MyBatis will not be executed and the DAO method should take a shorter time accordingly. The Service method is annotated with the @Transactional annotation to test the true and false cases. Results:

You can see that the execution time is almost close, so it is almost certain that this is the cause. There are still a few points of doubt, especially the fact that there is no 2x time cost when the problem reappears, if you have other ideas, feel free to discuss them.