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
- When was the L2 cache (code A location) obtained through MappedStatement initialized?
- 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(); }