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.
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.