Mybatis Principle--Caching Mechanism (Level 1 Caching)

Keywords: Mybatis Session Database JDBC

MyBatis designed a two-level structure for data caching, which is divided into a first-level cache and a second-level cache:
A first-level cache is a Session session-level cache: among SqlSession objects that represent a database session, also known as a local cache.Level 1 caching is a feature implemented internally by MyBatis that users cannot configure, automatically support caches by default, and users do not have the right to customize them (although this is not absolute; you can modify it by developing plug-ins);
A secondary cache is an application-level cache for an Application: it has a long life cycle, just like an Application's declaration cycle, that is, it serves the entire Application.

Since MyBatis uses the SqlSession object to represent a database session, in order to reduce resource waste, MyBatis creates a simple cache in the SqlSession object representing the session, caching the results of each query. When the next query is executed, if you decide that you have the same query before, the results will be taken out of the cache directly and returned.No more database queries are needed for the user.


Level 1 Cache

In fact, SqlSession is just an external interface to MyBatis, and SqlSession delegates its work to the Executor executor role, which is responsible for various operations on the database.When a SqlSession object is created, MyBatis creates a new Executor executor for the SqlSession object, and the cache information is maintained in the Executor executor, where MyBatis encapsulates the cache and cache-related operations into a Cache interface.
The implementation class of the Executor interface, PerpetualCache, which has a Cache interface in the BaseExecutor implementation class, maintains the cache using the PerpetualCache object for the BaseExecutor object
public class PerpetualCache implements Cache {
    private String id;
    private Map<Object, Object> cache = new HashMap();

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return this.cache.size();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }

    public void clear() {
        this.cache.clear();
    }

    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    public boolean equals(Object o) {
        if(this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else if(this == o) {
            return true;
        } else if(!(o instanceof Cache)) {
            return false;
        } else {
            Cache otherCache = (Cache)o;
            return this.getId().equals(otherCache.getId());
        }
    }

    public int hashCode() {
        if(this.getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        } else {
            return this.getId().hashCode();
        }
    }
}
Lifecycle of Level 1 Cache
  1. When MyBatis opens a database session, it creates a new SqlSession object, a new Executor object in the SqlSession object, and a new PerpetualCache object in the Executor object; when the session ends, the SqlSession object and its internal Executor object, as well as the PerpetualCache object, are also released.
  2. If SqlSession calls the close() method, the first-level cache PerpetualCache object will be released and the first-level cache will not be available.
  3. If clearCache() is called by SqlSession, the data in the PerpetualCache object is emptied, but the object can still be used;
    4. Any update operation (update(), delete(), insert () performed in SqlSession will empty the data of the PerpetualCache object, but the object can continue to be used;
Implementation of Level 1 Cache
  • For a query, a key value is constructed based on statementId,params,rowBounds, and the corresponding key value is used to cache the cached results stored in the Cache.
  • Determines whether the data taken from the Cache based on a specific key value is empty, that is, hit or not;
  • If hit, the cached result is returned directly;
  • If missed:
    1. Query the data in the database to get the query result;
    2. Keys and queried results are used as key s, and value pairs are stored in Cache.
    3. Return the query results;
  • End.
Design of Cache Interface and Definition of CacheKey
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        //The key value required to build the cache
        CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
        return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

//The key value required to build the cache
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if(this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());
            cacheKey.update(Integer.valueOf(rowBounds.getOffset()));
            cacheKey.update(Integer.valueOf(rowBounds.getLimit()));
            cacheKey.update(boundSql.getSql());
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();

            for(int i = 0; i < parameterMappings.size(); ++i) {
                ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                if(parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    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 = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }

                    cacheKey.update(value);
                }
            }

            return cacheKey;
        }
    }

Screenshot of debug source when calling the following method
//mapper interface method schoolCustomerDao.selectBySome (1l,'2017-09-17','120706049');


Debug Debug Debug Screenshot

Once the CacheKey is built, you can store the results of the query as follows:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

        List list;
        try {
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }
        //Cache the results of a query
        this.localCache.putObject(key, list);
        if(ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }

        return list;
    }

CacheKey is derived from the following conditions: statementId + rowBounds +SQL +passed to JDBC Parameter value passed to JDBC

Performance Analysis of First Level Cache

I'll discuss first-level cache performance issues for SqlSession from the perspective of two first-level cache features:

  • MyBatis has a simpler first-level cache design at the Session level by simply using HashMap for maintenance without restricting its capacity or size.
    Readers may feel inappropriate: If I keep querying data with a SqlSession object, will this cause a HashMap to be too large and cause a java.lang.OutOfMemoryError error?It makes sense for the reader to think about it this way, but MyBatis did.
    MyBatis has its own reasons for this design:
  1. Generally speaking, SqlSession has a short lifetime.Typically, there are not too many operations performed with a SqlSession object, and when executed, they will disappear.
  2. For a SqlSession object, as long as the update operation (update, insert, delete) is performed, the corresponding first-level cache in the SqlSession object will be emptied, so in general, the cache size will not be too large, affecting the memory space of the JVM;
  3. The cache in the SqlSession object can be freed manually.
  • Level 1 cache is a coarse-grained cache with no concept of update cache and cache expiration
    MyBatis's first level cache uses a simple HashMap. MyBatis is only responsible for storing the results of querying the database in the cache. It does not determine if the cache has been stored for too long or has expired, so there is no update to the cached results.

Based on the characteristics of the first level cache, I think you should pay attention to:

  1. When using SqlSession queries, we need to control the lifetime of SqlSession. The longer the SqlSession lasts, the older the cached data will be, which may cause errors with the real database. In this case, users can also manually empty SqlS in time.Cache in ession;
  2. The SqlSession object should not last too long for a SqlSession object that only performs a wide range of select operations and frequently performs them.

Posted by plasko on Tue, 21 May 2019 10:50:23 -0700