4. Analysis of L2 cache source code

1, L2 cache configuration

brief introduction

The L2 cache is built on the L1 cache. When receiving a query request, MyBatis will first query the L2 cache. If the L2 cache is not alive

In, query the first level cache again. If the first level cache does not exist, query the database again.

graph LR A (L2 cache) - > b (L1 cache) - > C (database)

Unlike the L1 Cache, the L2 Cache is bound to a specific namespace. A Mapper has a Cache, mappedstatements in the same Mapper share a Cache, and the L1 Cache is bound to SqlSession

How to enable L2 caching

1. Enable global L2 cache configuration:

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

2. Configure the label in the Mapper configuration file that needs to use L2 cache

<cache></cache>

3. Configure useCache=true on the specific CURD label

 <select id="findById" resultType="com.wuzx.pojo.User" useCache="true">
    select * from user where id = #{id}
</select>

Source code analysis

Parsing of tag < cache / >

In fact, this tag is configured in each mapper.xml file, so it is parsed together in the mapper file every time to get the source code

    // Resolve ` < mapper / > ` nodes
    private void configurationElement(XNode context) {
        try {
            // Get namespace attribute
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            // Set the namespace property
            builderAssistant.setCurrentNamespace(namespace);
            // Resolve < cache ref / > nodes
            cacheRefElement(context.evalNode("cache-ref"));
            // Resolve < cache / > nodes
            cacheElement(context.evalNode("cache"));
            // Abandoned! Old style parameter mapping. Inline parameter is preferred. This element may be removed in the future and will not be recorded here.
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            // Parse < resultmap / > nodes
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            // Parse < SQL / > nodes
            sqlElement(context.evalNodes("/mapper/sql"));
            // Parse < select / > < insert / > < update / > < delete / > nodes
            // Here, the generated Cache will be wrapped into the corresponding MappedStatement
            buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
    }
    
        // Resolve < cache / > tags
    private void cacheElement(XNode context) throws Exception {
        if (context != null) {
            //Resolve the type attribute of the < cache / > tag. Here we can customize the implementation class of cache, such as redisCache. If there is no customization, the same personal as the L1 cache is used here
            String type = context.getStringAttribute("type", "PERPETUAL");
            Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
            // Get the Cache implementation class responsible for expiration
            String eviction = context.getStringAttribute("eviction", "LRU");
            Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
            // How often the cache is emptied. 0 means not empty
            Long flushInterval = context.getLongAttribute("flushInterval");
            // Cache container size
            Integer size = context.getIntAttribute("size");
            // Serialize
            boolean readWrite = !context.getBooleanAttribute("readOnly", false);
            // Is it blocked
            boolean blocking = context.getBooleanAttribute("blocking", false);
            // Get Properties property
            Properties props = context.getChildrenAsProperties();
            // Create Cache object
            builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
    }
    
    
       /**
     * Create Cache object
     *
     * @param typeClass Cache implementation class responsible for storage
     * @param evictionClass Cache implementation class responsible for expiration
     * @param flushInterval How often the cache is emptied. 0 means not empty
     * @param size Cache container size
     * @param readWrite Serialize
     * @param blocking Is it blocked
     * @param props Properties object
     * @return Cache object
     */
    public Cache useNewCache(Class<? extends Cache> typeClass,
                             Class<? extends Cache> evictionClass,
                             Long flushInterval,
                             Integer size,
                             boolean readWrite,
                             boolean blocking,
                             Properties props) {

        // 1. Generate Cache object
        Cache cache = new CacheBuilder(currentNamespace)
                //Here, if we define the type in < Cache / >, we will use the custom Cache. Otherwise, we will use the same PerpetualCache as the L1 Cache
                .implementation(valueOrDefault(typeClass, PerpetualCache.class))
                .addDecorator(valueOrDefault(evictionClass, LruCache.class))
                .clearInterval(flushInterval)
                .size(size)
                .readWrite(readWrite)
                .blocking(blocking)
                .properties(props)
                .build();
        // 2. Add to Configuration
        configuration.addCache(cache);
        // 3. Assign the cache to MapperBuilderAssistant.currentCache
        currentCache = cache;
        return cache;
    }

From the source code, we can see that the id is the only one configured by the namespace tag, and then the cache object will be added to the caches collection of the configuration object, and the cache will be assigned to MapperBuilderAssistant.currentCache

    /**
     * Cache Object collection
     *
     * KEY: Namespace namespace
     */
    protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");

2, Analysis of query call cache source code

Cacheingexecution (implementation class of Executor supporting L2 cache)

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

    public Object getObject(Object key) {
        // When querying, you query directly from delegate, that is, from the real cache object
        Object object = delegate.getObject(key);
        // If it does not exist, it is added to entriesMissedInCache
        if (object == null) {
            // If the cache misses, the key is stored in entriesMissedInCache
            entriesMissedInCache.add(key);
        }
        // issue #146
        // If clearOnCommit is true, it indicates that it is in the continuous emptying state, and null is returned
        if (clearOnCommit) {
            return null;
        // Return value
        } else {
            return object;
        }
    }

    public void putObject(Object key, Object object) {
        // Store the key value pairs in the entriesToAddOnCommit Map instead of the real cache object delegate
        entriesToAddOnCommit.put(key, object);
    }	

The L2 cache objects are stored in the TransactionalCache.entriesToAddOnCommit map, but each query is directly queried from TransactionalCache.delegate. Therefore, after the L2 cache queries the database, setting the cache value does not take effect immediately, mainly because saving directly to the delegate will lead to dirty data problems

3, Why only after SqlSession is committed or closed?

Let's take a look at what the SqlSession.commit() method does

SqlSession

    public void commit(boolean force) {
        try {
            // Commit transaction
            executor.commit(isCommitOrRollbackRequired(force));
            // Mark dirty as false
            dirty = false;
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }
    public void commit(boolean required) throws SQLException {
        // Execute the method corresponding to delegate
        delegate.commit(required);
        // Submit TransactionalCacheManager
        tcm.commit();
    }
    /**
     * Commit all transactionalcaches
     */
    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();
        }
    }

Refresh of L2 cache

    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        // If you need to empty the cache, empty it
        flushCacheIfRequired(ms);
        // Execute the method corresponding to delegate
        return delegate.update(ms, parameterObject);
    }

    /**
     * If you need to empty the cache, empty it
     *
     * @param ms MappedStatement object
     */
    private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) { // Do you want to empty the cache
            tcm.clear(cache);
        }
    }

MyBatis L2 cache is only applicable to infrequently added, deleted and modified data, such as street data of national administrative regions, provinces, cities and towns. Once the data changes, MyBatis will empty the cache. Therefore, L2 cache is not suitable for data that is frequently updated.

4, Summary

In the design of L2 Cache, MyBatis makes a lot of use of decorator mode, such as cacheingexecution and decorators of various Cache interfaces

  • The L2 cache realizes the cache data sharing between sqlsessions, which belongs to the namespace level
  • L2 cache has rich cache strategies.
  • The L2 cache can be composed of multiple decorators combined with the underlying cache
  • The L2 cache is completed by a cache decoration executor, cacheingexecution, and a transactional cache.

Keywords: Mybatis

Added by pyrodude on Sat, 30 Oct 2021 20:25:07 +0300