mybatis caching mechanism

Keywords: Java Cache

mybatis cache mechanism I

mybatis provides cache support, which is divided into L1 cache and L2 cache

L1 cache

The first level cache is enabled by default in mybatis. It is a cache form with hashMap as the data structure.

How do I verify the L1 cache?

The following example queries the same data twice (or multiple times, twice is enough) to see whether the log has been queried twice.

//Validate L1 cache
@Test
public void FirstLevelCache() {
//Query User for the first time
User user1 = mapper.findUserById(1);

//Second query User
User user2 = mapper.findUserById(1);
System.out.println(user2 == user3);
sqlSession.close();
}

Log printing:

15:10:28,480 DEBUG PooledDataSource:406 - Created connection 903268937.
15:10:28,480 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@35d6ca49]
15:10:28,484 DEBUG findUserById:159 - ==>  Preparing: select * from user where id= ? 
15:10:28,524 DEBUG findUserById:159 - ==> Parameters: 1(Integer)
15:10:28,545 DEBUG findUserById:159 - <==      Total: 1
true

From the above log printing (debug mode), since the creation of the database connection, only the log printing is performed during the first query, and the data is directly fetched from the memory for the second time without database query. And the address value of the two objects after query is true, which shows that it is the same object in memory.

How do I refresh the L1 cache?
The following example intersperses an update operation between two queries.

//Validate L1 cache
@Test
public void FirstLevelCache() {

//Query User for the first time
User user1 = mapper.findUserById(1);

//Update user
User user = new User();
user.setUsername("zhansan");
user.setId(2);
mapper.update(user);
sqlSession.commit();

//Second query User
User user2 = mapper.findUserById(1);

//Query User for the third time
User user3 = mapper.findUserById(1);
System.out.println(user1 == user2);
System.out.println(user2 == user3);
sqlSession.close();
}

First query log
15:06:38,336 DEBUG PooledDataSource:406 - Created connection 424732838.
15:06:38,337 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1950e8a6]
15:06:38,340 DEBUG findUserById:159 - ==>  Preparing: select * from user where id= ? 
15:06:38,386 DEBUG findUserById:159 - ==> Parameters: 1(Integer)
15:06:38,417 DEBUG findUserById:159 - <==      Total: 1

Update operation log
15:06:38,419 DEBUG update:159 - ==>  Preparing: update user set username= ? where id = ? 
15:06:38,419 DEBUG update:159 - ==> Parameters: zhansan(String), 2(Integer)
15:06:38,421 DEBUG update:159 - <==    Updates: 1
15:06:38,422 DEBUG JdbcTransaction:70 - Committing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1950e8a6]

Second query log
15:06:38,424 DEBUG findUserById:159 - ==>  Preparing: select * from user where id= ? 
15:06:38,424 DEBUG findUserById:159 - ==> Parameters: 1(Integer)
15:06:38,426 DEBUG findUserById:159 - <==      Total: 1

Output results
false
true

It is found from the above logs that if there is an update operation between the two queries, the second query will query the database. It can be seen that the update operation flushes the cache. Here's a point to note: after the update operation, the transaction is committed. If only the update operation is performed without transaction submission, the cache will still be refreshed.

Note: of course, cache refresh is more than update. Operations such as insert, delete, rollback, transaction commit, closing sqlsession(close method), manual refresh (clearCache method) will refresh the first level cache.

What is the L1 cache? When was it created? What is the workflow of L1 cache
Let's start with the SqlSession interface to see which methods in this interface are related to the L1 cache

As can be seen from the above figure, only clearCache method in SqlSession is related to cache.
Follow this method to find its implementation class defaultsqlsession (only copy part of the code here):

private final Executor executor;

@Override
public void clearCache() {
    executor.clearLocalCache();
}

The method here is to call the method of the Executor and enter the implementation class BaseExecutor:

protected PerpetualCache localCache;

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

The loadalcache. Clear () method here calls the method of PerpetualCache. Let's take a look at the class of PerpetualCache:

private Map<Object, Object> cache = new HashMap<Object, Object>();

@Override
public void clear() {
    cache.clear();
}

From the perspective of PerpetualCache, mybatis encapsulates the first level cache into a PerpetualCache object. The data structure of HashMap is used in the PerpetualCache class to store data.

Therefore, from the tracking of the above source code, it can be concluded that the underlying level of the first-level cache of mybatis is a HashMap data structure.

Let's see how the L1 cache is created in the Executor,

As can be seen from the figure, there is a createCacheKey method, which obviously creates a cacheKey, that is, the value of the key in hashmap.

Let's take a look at the createCacheKey method:

In terms of method definition, there are four input parameters
MappedStatement ms: MappedStatement Object that encapsulates id,sql,Configuration And other information. 
Object parameterObject: implement sql Parameters passed in. 
RowBounds rowBounds: Paging object,Encapsulates information about paging
BoundSql boundSql: sql Objects, encapsulating sql Statements and parameters.

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    //A cachekey is created, which is the key value of the L1 cache.
    CacheKey cacheKey = new CacheKey();
    //The following code logic is an assignment to the cachekey
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

From the above source code, another important object is the Cache object, which is the first level Cache as the key value of hashmap. It can be seen from the code that the cacheKey.update method has been called many times. Let's take a look at this update method:

//Define a collection object, which is the key value of the so-called L1 cache.
private transient List<Object> updateList;

public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
//This collection is an ArrayList
this.updateList = new ArrayList<Object>();
}
public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;
    
    //The parameters passed in are added to the collection
    updateList.add(object);
}

From the above source code analysis, we can get the key value of the first level cache. Its bottom layer is an ArrayList, which is encapsulated and used through the CacheKey class.

After analyzing how the first level cache is created, let's analyze when the first level cache is created. In this article, the existence of the first level cache has been verified, so let's think about whether the data has been put into the cache before mybatis returns the query results after executing the query results. This idea is no problem, The curd operation of mybatis is performed in the Executor, so let's start with the query method of the Executor to see when the cache is created.
BaseExecutor class (extract only part of the analyzed code):

//L1 cache object
protected PerpetualCache localCache;

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //Get BoundSql object based on parameters
    BoundSql boundSql = ms.getBoundSql(parameter);
    //Create CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@SuppressWarnings("unchecked")
@Override
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();
}
//Result set
List<E> list;
try {
  queryStack++;
  //Get data from the localCache according to the key value. This step is to try to get data from the L1 cache.
  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
  } else {
  //If there is no data in the first level cache, query in the 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;
}


private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
  //Execute database query
  list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
  localCache.removeObject(key);
}
//Put the results of the query into the first level cache.
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
  localOutputParameterCache.putObject(key, parameter);
}
return list;
}

Obviously, the CacheKey object has been created when calling query, and the data will be fetched from the level-1 cache localCache in subsequent queries. If the data is not fetched, it will be queried in the database. After the query from the database is successful, the data will be cached in the level-1 cache localCache.

The above is a detailed analysis of the L1 cache mechanism, and then the detailed analysis of the L2 cache will be updated.
If there is any dispute about where to write, please give us more advice!

Posted by Wayniac on Wed, 10 Nov 2021 02:02:49 -0800