Use and Configuration

  • See Caching and lazy loading in Mybatis for usage
  • The maximum scope of the level 2 cache isSqlSessionFactory, the minimum scope is a single mapper.xml file (namespace)
    • When usingcacheThe scope of the cache is a single XML file (namespace)
    • While the use ofcache-refYou can refer to other XML files (namespace), which implements cache sharing, but still cannot be crossedSqlSessionFactory

The key code

create

  • The creation of the second-level cache object is traced back to the Mappers tag that parses the configuration file, and is taken into the mapper.xml file when each mapper.xml file is parsedcacheorcache-refThe label
  • To resolve firstcache-refThe tag, if present, will be currentnamespaceSet the cache object referenced by the labelnamespaceCache object to achieve cache sharing
  • To resolvecacheThe tag, if present, will be currentnamespaceThe cache object is updated

CacheBuilder#build

public Cache build(a) {
    // Set the default implementation of cache, default decorator (only set, not assembled)
    setDefaultImplementations();
    // Create a default cache
    Cache cache = newBaseCacheInstance(implementation, id);
    // Set the cache properties
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    if (PerpetualCache.class.equals(cache.getClass())) {  // A cache implementation is PerpetualCache, i.e. not a user-defined cache implementation
        for (Class<? extends Cache> decorator : decorators) {
            // Nested custom decorators for cache level by level
            cache = newCacheDecoratorInstance(decorator, cache);
            // Set the properties for the decorator
            setCacheProperties(cache);
        }
        // Add a standard decorator to the cache
        cache = setStandardDecorators(cache);
    } else if(! LoggingCache.class.isAssignableFrom(cache.getClass())) {// Add a log decorator
        cache = new LoggingCache(cache);
    }
    // Returns the wrapped cache
    return cache;
}
Copy the code
  • When the cache object is built, the cache ID is set tocurrentNamespace
  • If the cache object isPerpetualCache, will add layers of decoration,BlockingCache->SynchronizedCache->LoggingCache->SerializedCache->ScheduledCache->LruCache->PerpetualCache
  • If the cache object is custom, it is only added for itLoggingCachedecoration
    • BlockingCache: Provides access blocking
    • SynchronizedCache: Synchronizes Cache
    • LoggingCache: Log function
    • SerializedCache: serialization function
    • ScheduledCache: indicates the scheduled cleaning function
    • LruCache: The Cache implementation of the Lru algorithm is used to remove the least recently used Key or Value
    • PerpetualCache: A basic implementation with a HashMap inside
  • Because the cache object is being createdSqlSessionFactoryObject, so the maximum scope isSqlSessionFactory

use

  • When level 2 caching is enabled, all BaseExecutor implementation classes are decorated with CachingExecutor

    // Determine whether caching is enabled according to the Settings node cacheEnabled configuration item in the configuration file
    if (cacheEnabled) { // If configured to enable the cache
        // Use CachingExecutor to decorate the actual executor
        executor = new CachingExecutor(executor);
    }
    Copy the code

CachingExecutor#query

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
    // Get the cache corresponding to MappedStatement. Possible results are caches for that namespace, caches for other shared namespaces, and no caches
    Cache cache = ms.getCache();
    // If the mapping file is not set to 
      
        or 
       
        , the cache variable is null
       
      
    if(cache ! =null) { // Cache exists
        // Determine whether to clear level 2 cache before statement execution, and if so, clear level 2 cache
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) { // This statement uses caching and has no result handler
            // Level 2 cache does not support CALLABLE statements with output parameters
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            // Read the result from the cache
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) { // No results in the cache
                // Execute to the wrapped actuator
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // Cache the result returned by the wrapped executor
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            returnlist; }}// Execute by the wrapped actual actuator
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Copy the code
  • Gets the cache object in the current namespace from MappedStatement

  • FlushCache flushCache flushCache flushCache flushCache flushCache flushCache

    private void flushCacheIfRequired(MappedStatement ms) {
        // Get the cache corresponding to MappedStatement
        Cache cache = ms.getCache();
        if(cache ! =null && ms.isFlushCacheRequired()) { // Cache exists and the operation statement requires that the cache be cleared before execution
            // Clear the cache in the transactiontcm.clear(cache); }}Copy the code
  • Check whether cache is used for the current query (based on the useCache attribute value, this query is used by default, and other default values are invalid). If the ResultHandler value is null, level-2 cache is not used

  • With a level 2 cache, the results are fetched from the cache first and returned if they are retrieved

  • If there is no result in the cache, it is given to the wrapped executor to execute the query, the returned result is cached, and finally returned

TransactionalCacheManager#getTransactionalCache

private TransactionalCache getTransactionalCache(Cache cache) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}
Copy the code
  • inCachingExecutorHolding theTransactionalCacheManagerObject that is used when operating with a level 2 cache objectTransactionalCacheWrap the cache object as shown above
  • thisTransactionalCacheManagerObject that holds oneHashMapCan be managed differentlynamespaceCache object of

TransactionalCache#clear

  • Set clear caching at transaction commit time
  • Clear data that has not actually been written to the cache (temporary container)
  • In other words, in executionflushCacheIfRequiredMethod without actually clearing data from the cache
@Override
public void clear(a) {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
}
Copy the code

TransactionalCache#getObject

  • Read cached data from the wrapped class
  • If the cache is not hit, the key is recorded in the cache container to calculate the cache hit ratio
  • If clear cache is set, null is returned, otherwise it is returned normally
@Override
public Object getObject(Object key) {
    // issue #116
    // Read the corresponding data from the cache
    Object object = delegate.getObject(key);
    if (object == null) { // Cache failed to hit
        // Records that the cache is not hit
        entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) { // If clear immediately upon commit is set, null is returned
        return null;
    } else {
        // Returns the result of the query
        returnobject; }}Copy the code

TransactionalCache#putObject

  • When the query is complete and data is saved to the cache, it is not written directly to the cache, but stored in a temporary container
@Override
public void putObject(Object key, Object object) {
    // Add it to the entriesToAddOnCommit list
    entriesToAddOnCommit.put(key, object);
}
Copy the code

What happens when the transaction commits?

Sequence diagram

sequenceDiagram
participant A as DefaultSqlSession
participant B as CachingExecutor
participant C as TransactionalCacheManager
participant D as TransactionalCache

A ->> B : commit
B ->> C : commit
C ->> D : commit

TransactionalCache#commit

public void commit(a) {
    if (clearOnCommit) { // Clear the cache after a transaction commits
        // Clear the cache
        delegate.clear();
    }
    // Write operations not written to the cache to the cache
    flushPendingEntries();
    // Clean the environment
    reset();
}
Copy the code
  • If clearing cache after commit transaction is set, the cache is cleared

  • Write the data cached in the temporary container to the cache (missing data in the cache container will be written to the cache together as null values)

    private void flushPendingEntries(a) {
        // Write the data in entriesToAddOnCommit to the cache
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        // Write data from entriesMissedInCache to the cache
        for (Object entry : entriesMissedInCache) {
            if(! entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry,null); }}}Copy the code
  • Reset the clearOnCommit flag to empty the temporary container

    private void reset(a) {
        clearOnCommit = false;
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }
    Copy the code

Use a level 2 cache across SqlSessionFactory

The idea is simple: just define a cache container whose lifetime does not depend on the creation of the SqlSessionFactory.

  • Custom MyCache class, this kind of need to implement org. Apache. Ibatis. Cache. The cache interface

    public class MyCache implements Cache {
    
        private final String id;
    
        static Map<Object, Object> cache = null;
    
        static {
            cache = new HashMap<>();
        }
    
        public MyCache(String id) {
            this.id = id;
        }
    
        @Override
        public String getId(a) {
            return id;
        }
    
        @Override
        public void putObject(Object key, Object value) {
            cache.put(key, value);
        }
    
        @Override
        public Object getObject(Object key) {
            return cache.get(key);
        }
    
        @Override
        public Object removeObject(Object key) {
            cache.remove(key);
            return null;
        }
    
        @Override
        public void clear(a) {
            cache.clear();
        }
    
        @Override
        public int getSize(a) {
            return Integer.valueOf(cache.size()+"");
        }
    
        @Override
        public ReadWriteLock getReadWriteLock(a) {
            return null; }}Copy the code
  • Since MyCache is instantiated every time it is created at the SqlSessionFactory, it is instantiated by calling MyCache’s parameterized constructor

    / / CacheBuilder class
    
    private Cache newBaseCacheInstance(Class<? extends Cache> cacheClass, String id) {
        Constructor<? extends Cache> cacheConstructor = getBaseCacheConstructor(cacheClass);
        try {
            // Create a cache class instance with the argument constructor
            return cacheConstructor.newInstance(id);
        } catch (Exception e) {
            throw new CacheException("Could not instantiate cache implementation (" + cacheClass + "). Cause: "+ e, e); }}private Constructor<? extends Cache> getBaseCacheConstructor(Class<? extends Cache> cacheClass) {
            try {
                // Get the parameterized constructor for the cache implementation class
                return cacheClass.getConstructor(String.class);
            } catch (Exception e) {
                throw new CacheException("Invalid base cache implementation (" + cacheClass + ")."
                        + "Base cache implementations must have a constructor that takes a String id as a parameter. Cause: "+ e, e); }}Copy the code
  • Therefore, the MyCache class cannot be set as a singleton, only the internal Map can be set as a singleton. In this case, static code blocks are used to initialize the Map, so that each time the SqlSessionFactory is created, the MyCache object is different, but its internal Map is the same. This enables the use of a level 2 cache across SqlSessionFactory.

    static Map<Object, Object> cache = null;
    
    static {
        cache = new HashMap<>();
    }
    Copy the code
  • Finally, configure MyCache to be used in the cache tag.

    <! -- Enable level 2 cache -->
    <cache type="org.apache.ibatis.z_run.util.MyCache"
           size="1024"
           eviction="LRU"
           flushInterval="120000"
           readOnly="true"/>
    Copy the code

Map is used here for demonstration purposes, not for other performance optimizations, but for your own research if necessary.