MyBatis special cache explanation

Cache introduction

Generally, we use caching technology in the system to improve the efficiency of data query. When we find a batch of data from the database, we put it into the mixed storage (a block of memory area in simple understanding). The next time we query the same data, we can get the data directly from the cache. In this way, less interaction with database can improve the efficiency of query.

But a coin has two sides. While cache brings performance improvement, it also "stealthily" introduces many problems, such as cache synchronization, cache invalidation, cache avalanche and so on. Of course, these issues are not the focus of this paper.

This article mainly discusses the function of MyBatis cache. Although the caching function of mybatis is weak, in order to fully understand the mybatis framework, it is necessary to learn the caching function. Mybatis's cache is divided into first level cache and second level cache.

L1 cache

In the process of application running, it is possible for us to execute multiple SQL queries with the same query conditions in a database session. MyBatis provides the scenario of optimizing the first level cache. If the SQL statements are the same, the first level cache will be hit first, so as to avoid directly querying the database and improve the performance.

What is MyBatis L1 cache

The first level cache is the sqlSession level cache. The sqlSession object needs to be constructed when operating the database. There is a (memory area) data structure (HashMap) in the object to store the cached data. The cache data regions (hashmaps) between different sqlsessions do not affect each other.

In the process of application running, it is possible for us to execute multiple SQL queries with the same query conditions in a database session. MyBatis provides the scenario of optimizing the first level cache. If the SQL statements are the same, the first level cache will be hit first, so as to avoid directly querying the database and improve the performance.

How to open level 1 cache

The first level cache in MyBatis is turned on by default, and no additional operations are required.

If you need to turn off the first level cache, you can set the flushCache property to true in the Mapper mapping file, which will only work for a single SQL operation


<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap" flushCache="true">
    select 
    <include refid="Base_Column_List" />
    from cbondissuer
    where OBJECT_ID = #{objectId,jdbcType=VARCHAR}
  </select>
> Another way is to MyBatis In the main configuration file of, turn off all the first level cache
> ```xml
>   The default is SESSION´╝îThat is to say, level 1 cache is enabled
>   <setting name="localCacheScope" value="STATEMENT"/>
> ```

//Let's write code to verify the first level cache of MyBatis.

```java
String id = "123";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//Mapper created by the same sqlSession
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//Mapper created by another sqlSession
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);

//The same Mapper, the same SQL query twice
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//Mapper created by the same sqlSession queries the same SQL again
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//Mapper created by different sqlSession queries the same SQL once
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);

System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));

sqlSession1.close();
sqlSession2.close();
System.out.println("end...");

There are four queries above, if you look at the logs. Only two database queries are found. Because the second and third queries both query the first level cache, what they find is actually the result in the cache. So the output is

cbondissuer10 equals cbondissuer101 :true
cbondissuer10 equals cbondissuer11 :true
cbondissuer10 equals cbondissuer21 :false

What factors invalidate the first level cache

The first level cache above gives us a sense of the existence of the first level cache in MyBatis. Now you may have a question. When will the cache fail?

  • When performing update operation through the same SqlSession, this update operation not only refers to update operation, but also refers to insert and delete operation;
  • The first level cache will be deleted when the transaction is submitted;
  • The first level cache will also be deleted when the transaction is rolled back;

Source code analysis of L1 cache

In fact, the essence of MyBatis level 1 cache is a Map like attribute of an Executor. The way to analyze the source code is to see where the cache is queried from the Map and where the cache is emptied.

1. Use cache analysis when querying

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  //The localCache variable is the first level cache variable
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;
  //. omit the following code
}

It is easy to find out where this variable is used in the global search code BaseExecutor.query Method uses this cache:

public abstract class BaseExecutor implements Executor {

// Omit other codes
 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //First query the results from the cache. If there are already results in the cache, use the cached results directly
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        //No results in cache queried from database
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
  //. omit the following code
}

The above code shows the process of using cache in BaseExecutor's query method. It should be noted that the cache is queried according to the cacheKey. We can make this key simple
Different sql statements can find out different caches. (note that different parameters in sql statements are also considered different sql statements.).

2. Code analysis leading to invalidation of L1 cache
Looking at the code of BaseExecutor, we can easily find that the following method empties the first level cache. (don't ask me how I found this code. I need to improve my code ability slowly.)

@Override
public void clearLocalCache() {
    if (!closed) {
        localCache.clear();
        localOutputParameterCache.clear();
    }
}

So we only need to look at where this method is called to know which conditions will cause the first level cache to fail. After tracking, it is found that the following three places will invalidate the first level cache

This method will be called for BaseExecutor's update method and MyBatis interface to add, delete and modify. This also confirms the above statement.

@Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

The commit method of BaseExecutor. Transaction commit will cause the first level cache to fail. If we use Spring, general transactions are submitted automatically, so it seems that the first level cache of MyBatis has not been considered

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

For the rollback method of BaseExecutor, transaction rollback will also cause the first level cache to fail.

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

Suggestions on the use of L1 cache

MyBatis is usually used in combination with spring. Generally, there is only one SqlSession implementation class in the whole spring container. Spring generally commits transactions actively, so the first level cache often fails.

In addition, we seldom execute the same SQL twice within the scope of a transaction. These reasons cause us to pay little attention to the existence of MyBatis level 1 cache in the development process.

It's not necessary to use it. As a qualified developer, you need to know these things clearly. You need to know the workflow of MyBatis L1 cache.

L2 cache

What is MyBatis L2 cache

The largest sharing scope of the first level cache of MyBatis is within a SqlSession. If multiple sqlsessions need to share the cache, they need to enable the second level cache. After the second level cache is enabled, the cacheingexector will be used to decorate the Executor,
Before entering the query process of the first level cache, first query the second level cache in the caching executor. The specific workflow is as follows:

When the second level Cache is enabled, all operation statements in the same namespace affect a common Cache (a Mapper mapping file corresponds to a Cache), that is, the second level Cache is shared by multiple sqlsessions, which is a global variable. When Cache is turned on, the process of data query execution is level 2 Cache - > Level 1 Cache - > database.

As can be seen from the above figure, there are many secondary cache implementations of MyBatis, such as MemCache, Ehcache, etc. It can also be Redis, etc., but additional Jar packages are needed.

How to open L2 cache

The second level cache is not enabled by default. It needs to be manually enabled. When implementing the second level cache, MyBatis requires that the returned POJO must be serializable. The conditions for enabling L2 cache are also relatively simple,

step1: through the

<settings>  
	<setting name = "cacheEnabled" value = "true" />
</settings>

Step 2: add tags to Mapper's xml configuration file

There are several options below the cache tab

  • eviction: cache recovery policy. The following strategies are supported

    • LRU - least recently recycled, remove objects that are not used for the longest time (the default is this policy)
    • FIFO - first in, first out, remove them in the order in which the cache enters
    • SOFT - SOFT reference to remove objects based on garbage collector state and SOFT reference rules
    • WEAK - WEAK reference, more actively remove objects based on garbage collector and WEAK reference rules
  • Flushing interval: the refresh interval of the cache. How often does the cache refresh? It is not cleared by default. Set a millisecond value;

  • readOnly: read only; true read only. MyBatis believes that all operations to get data from the cache are read-only operations and will not modify the data. In order to speed up data acquisition, MyBatis will directly give users the references of data in the cache. Not safe, fast. Read write (default): MyBatis thinks the data may be modified

  • size: how many elements are stored in the cache

  • type: specify the full class name of the custom Cache (just implement the Cache interface)

  • blocking: if the corresponding key cannot be found in the cache, will it be blocked until the corresponding data enters the cache.

Cache ref refers to the cache configuration of other namespaces. The operation of two namespaces uses the same cache.

What factors will invalidate the L2 cache

It can be seen from the above introduction that mybatis's secondary cache is mainly designed to share the cache between sqlsessions. But we usually use Spring to develop mybatis. Generally, there is only one SqlSession instance in Spring environment, so there are few opportunities to use L2 cache. So the following is a brief description of the second level cache of mybatis.

Or the columns above

String id = "{0003CCCA-AEA9-4A1E-A3CC-06D884BA3906}";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//Mapper created by the same sqlSession
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//Mapper created by another sqlSession
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class);

//The same Mapper, the same SQL query twice
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//Mapper created by the same sqlSession queries the same SQL again
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//You need to commit a transaction for the L2 cache to take effect
sqlSession1.commit();
//Mapper created by different sqlSession queries the same SQL once
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id);

System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));
  • The second level cache is based on the unit of namespace (Mapper), and the operations under different namespaces do not affect each other.
  • Insert, update and delete will clear all the caches in the namespace.
  • Multi table operations must not use the second level cache, because multi table operations will generate dirty data when updating.

Suggestions for using L2 cache

I don't think MyBatis's L2 cache is very practical. One reason is that there is only one sqlsession in the Spring environment, and there is no shared cache between sqlsessions; the other is that
MyBatis' cache can't be distributed, so the second level cache of MyBatis is mainly about understanding.

A brief summary

L1 cache

  • The essence of L1 cache is a Map like attribute of Executor;
  • The first level cache is on by default. You can turn off the first level cache by setting flushCache to true or global configuration localCacheScope to Statement;
  • When the first level cache is enabled, the query operation will first query the first level cache, and then query the database;
  • Adding, deleting, modifying and transaction commit rollback operations will lead to the invalidation of the first level cache;
  • Since transactions in Spring are submitted automatically, the MyBatis level 1 cache under Spring often fails. (but it doesn't mean it doesn't work unless you manually turn off the L1 cache.)
  • Distributed is not possible.

L2 cache

  • namesapce level cache (Mapper level or table level cache) is designed to realize the cache sharing between sqlsessions;
  • After the second level cache is enabled, the query logic is second level cache - > cached - > database;
  • Insert, update and delete will clear all the caches in the namespace;
  • Multi table queries must not use the secondary cache, because multi table operations may generate dirty data when updating.

In general, MyBatis has a weak caching capability. If you want to use the cache, it is recommended to use the spring cache and other frameworks.

reference resources

Tags: Java Mybatis SQL Database Spring

Posted on Wed, 03 Jun 2020 04:56:20 -0400 by lathifmca