mybatis framework: first and second level cache

The previous article mentioned that the cache will be used when querying. Its built-in two-level cache is as follows:

// The L1 cache is bound to sqlsession in the executor
// org.apache.ibatis.executor.BaseExecutor#localCache
// Point to org apache. ibatis. cache. impl. PerpetualCache#cache
private Map<Object, Object> cache = new HashMap<>();

// L2 cache, in MappedStatement (corresponding to a crud method in mapper.xml), the cycle is consistent with SqlSessionFactory
org.apache.ibatis.mapping.MappedStatement#cache
// Finally, it also points to org apache. ibatis. cache. impl. PerpetualCache#cache
private Map<Object, Object> cache = new HashMap<>();
  • 1, The L2 cache is a query cache. select writes, insert, update and delete are cleared
  • 1, The L2 cache points to org apache. ibatis. cache. impl. Perpetual cache #cache is essentially a HashMap
  • 1, The calculation methods of L2 cache keys are the same, pointing to org apache. ibatis. executor. Baseexecutor #createcachekey, the essence of Key: ID of statement + offset + limit + SQL + param parameter
  • The L1 cache life cycle is consistent with SqlSession and is enabled by default; The L2 cache declaration cycle is consistent with SqlSessionFactory and needs to be opened manually
  • The same namespace uses the same L2 cache; The L2 cache is associated with the transaction, the transaction commit data will be written to the cache, and the transaction rollback data will not be written to the cache

Next, take a look at the source code.

L1 cache

The life cycle of L1 cache is sqlSession; In the same sqlSession, use the same sql and query criteria to query the DB multiple times, and the non first query will hit the L1 cache.

The L1 cache is enabled by default. If you want to close it, you need to add configuration

// ==If it is not set, the default is SESSION (which will be covered in the subsequent source code analysis)
<setting name="localCacheScope" value="STATEMENT"/>

Take the query method as the entry

org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)
org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // ==Calculate CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // ==Use cache in query
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

CacheKey calculation

org.apache.ibatis.executor.BaseExecutor#createCacheKey
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    CacheKey cacheKey = new CacheKey();
    // ==Call the update method to modify the cache
    cacheKey.update(ms.getId());
    cacheKey.update(rowBounds.getOffset());
    cacheKey.update(rowBounds.getLimit());
    cacheKey.update(boundSql.getSql());
    // value is a parameter
    cacheKey.update(value);
    return cacheKey;
}

From here, we can guess that the CacheKey is related to the id, offset, limit, sql and param parameters of the statement.

Enter the CacheKey to verify this guess:

### CacheKey class ###

// Default 37
private final int multiplier;
// Default 17
private int hashcode;
private long checksum;
private int count;

private List<Object> updateList;

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
    
    // --Modify several attribute values
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;
    // --updateList new object
    updateList.add(object);
}

public boolean equals(Object object) {
    // --Compare several attribute values
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    if (checksum != cacheKey.checksum) {
      return false;
    }
    if (count != cacheKey.count) {
      return false;
    }
    // --Compare the objects in updateList one by one
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
}

Use cache in query

org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    try {
      queryStack++;
      // == 1. Get data from localCache first
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
          handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } 
      // == 2. No data in cache, query from database
      else {
          list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    }

    // ##If scope is set to state type, the L1 cache will be cleaned up
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // Clean cache
      clearLocalCache();
    }
    return list;
}

Continue to observe code 2 Location:

List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  // Cache occupancy, indicating execution in progress
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    // ==Query DB logic
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key);
  }
  // ==Put the execution results into the L1 cache
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

To sum up, the query results will be stored in the localCache during the query process.
However, when the scope is set to state, the cache will be emptied every time -- this is the secret of L1 cache invalidation.

Add, delete, modify and clean up the cache

Both insert and delete methods execute update:

public int insert(String statement) {
    return insert(statement, null);
}

public int delete(String statement) {
    return update(statement, null);
}

So observe update:

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.");
    }
    // ==Clean up L1 cache
    clearLocalCache();
    return doUpdate(ms, parameter);
}

L2 cache

L2 cache needs to be turned on:

  • Step 1
<setting name="cacheEnabled" value="STATEMENT"/>
  • Step two

At the same time, in mapper Add tags to XML

<cache/> 

By default, the key of L2 Cache is namespace. If you want to reference the Cache configuration of other namespaces, you can use the following label:

<cache-ref namespace="xxx"/>

CachingExecutor

The entry of L2 cache is created in the executor:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      // Simpleexecution is created by default
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      // ==When the L2 cache is enabled, the decorator mode is used, and the caching executor is used to package one layer
      executor = new CachingExecutor(executor);
    }
    return executor;
}

Observe what is done in the constructor

// Attributes are assigned to each other
public CachingExecutor(Executor delegate) {
  this.delegate = delegate;
  delegate.setExecutorWrapper(this);
}

After assignment, the relationship between cacheingexecution and simpleexecution is as follows

After knowing this, let's look at the query method of cacheingexecution:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // ==Call the createCacheKey method of delegate (analyzed earlier)
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // ==Query of L2 cache
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

Observe the implementation of query method

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // ##A. get cache through MappedStatement
    Cache cache = ms.getCache();
    if (cache != null) {
      // Cache refresh
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // -- 1. Get query results through tcm
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // -- 2. No result in TCM, query through the original executor (L1 cache + jdbc logic)
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // -- 3. The query results are finally put into tcm
          tcm.putObject(cache, key, list); 
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

// ##B.tcm points here
TransactionalCacheManager tcm = new TransactionalCacheManager();

The logic is simple, but there are two problems bothering me

  1. When was the L2 cache (code A location) obtained through MappedStatement initialized?
  2. What is the relationship between L2 cache and tcm (transactional cache manager)?

L2 cache initialization

Push backward along the cache, which can be traced back to Mapper parsing.

The complete call chain is as follows (as a review):

// Create SqlSessionFactory
org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties)
org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
// configuration parsing
org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration
// Parse mapper
org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement{
    // ==Configuration reference of L2 cache (execute namespace)
    cacheRefElement(context.evalNode("cache-ref"));
    // ==Enable L2 cache
    cacheElement(context.evalNode("cache"));
}

org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement
org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache{
    // ==L2 cache creation
    Cache cache = new CacheBuilder(currentNamespace)
        // --The Cache implementation is a perpetual Cache
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        // --The wrapper uses LruCache
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
}

Look at the whole decoration chain of L2 cache (steal picture)

SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache.

Relationship between L2 cache and TransactionalCacheManager

TransactionalCacheManager class:

// ##Maintain a map, where key is Cache and value is TransactionalCache
Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

public Object getObject(Cache cache, CacheKey key) {
    
           // ## 1. This method establishes a k-v relationship in transactionalCaches
    return getTransactionalCache(cache)
                ⬇⬇⬇⬇⬇⬇
                transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
            
            // == 2. Get from L2 cache
            .getObject(key);
}

Then look at TransactionalCache

// ==L2 cache
private final Cache delegate;
// ==L2 cache cleanup flag
private boolean clearOnCommit;

// ####The following two sets can be understood as being used to store temporary data####
// ==Objects that need to be added to the L2 cache when a transaction is committed
private final Map<Object, Object> entriesToAddOnCommit;
// ==Object key that does not exist in L2 cache
private final Set<Object> entriesMissedInCache;

public void putObject(Object key, Object object) {
    // Object is recorded in entriesToAddOnCommit
    entriesToAddOnCommit.put(key, object);
}

public Object getObject(Object key) {
    // Get from L2 cache
    Object object = delegate.getObject(key);
    if (object == null) {
      // The key does not exist in the L2 cache and is recorded in entriesMissedInCache
      entriesMissedInCache.add(key);
    }
}

It can be seen here that there is a deep entanglement between L2 cache and Transaction.
So what's the problem?

  • Transaction commit

Observe the commit method of transaction manager:

org.apache.ibatis.cache.TransactionalCacheManager#commit
org.apache.ibatis.cache.decorators.TransactionalCache#commit
public void commit() {
    // ==Refresh object
    flushPendingEntries();
}

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      // ==Object is flushed from entriesToAddOnCommit to L2 cache
      delegate.putObject(entry.getKey(), entry.getValue());
    }
}

It can be proved here that when the transaction is committed, the object is flushed from a temporary collection entriesToAddOnCommit to the L2 cache.

  • Transaction rollback

Then observe the rollback method

org.apache.ibatis.cache.decorators.TransactionalCache#rollback
public void rollback() {
    unlockMissedEntries();
    // ==Reset to clean up the temporary collection data
    reset();
}

private void reset() {
  clearOnCommit = false;
  entriesToAddOnCommit.clear();
  entriesMissedInCache.clear();
}

appendix

P6-P7 knowledge collection

Keywords: Java Mybatis source code analysis

Added by ceemac on Tue, 01 Mar 2022 13:04:40 +0200