Use and Configuration
- See Caching and lazy loading in Mybatis for usage
- The maximum scope of the level 2 cache is
SqlSessionFactory
, the minimum scope is a single mapper.xml file (namespace
)- When using
cache
The scope of the cache is a single XML file (namespace
) - While the use of
cache-ref
You can refer to other XML files (namespace
), which implements cache sharing, but still cannot be crossedSqlSessionFactory
- When using
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 parsed
cache
orcache-ref
The label - To resolve first
cache-ref
The tag, if present, will be currentnamespace
Set the cache object referenced by the labelnamespace
Cache object to achieve cache sharing - To resolve
cache
The tag, if present, will be currentnamespace
The 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 to
currentNamespace
- If the cache object is
PerpetualCache
, will add layers of decoration,BlockingCache->SynchronizedCache->LoggingCache->SerializedCache->ScheduledCache->LruCache->PerpetualCache
- If the cache object is custom, it is only added for it
LoggingCache
decoration- 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 created
SqlSessionFactory
Object, 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
- in
CachingExecutor
Holding theTransactionalCacheManager
Object that is used when operating with a level 2 cache objectTransactionalCache
Wrap the cache object as shown above - this
TransactionalCacheManager
Object that holds oneHashMap
Can be managed differentlynamespace
Cache 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 execution
flushCacheIfRequired
Method 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.