Mybatis source code analysis - what is the first level cache?
1. L1 cache
Let's conclude: MyBatis supports caching, but without configuration, it only enables L1 caching by default, which is only relative to the same SqlSession. Therefore, when the parameters are exactly the same as SQL, we use the same SqlSession object to call a Mapper method, and often execute SQL only once, because MyBatis will put it in the cache after the first query using SelSession. When querying later, if there is no statement to refresh and the cache does not timeout, SqlSession will fetch the currently cached data and will not send SQL to the database again.
Ask questions:
1. How long is the lifecycle of L1 cache?
a. When mybatis starts a database session, it will create a new SqlSession object, and there will be a new Executor object in the SqlSession object. Hold a new perpetual cache 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.
b. If SqlSession calls the close() method, the L1 cache object will be released and the L1 cache will not be available.
c. If clearCache() is called by SqlSession, the data in the PerpetualCache object will be emptied, but the object can still be used.
d. Any update operation (update(), delete(), insert()) in sqlsession will clear the data of the perpetual cache object, but the object can continue to be used
2. How to judge whether two queries are identical?
mybatis believes that for two queries, if the following conditions are exactly the same, they are considered to be the same two queries.
a. statementId passed in
b. The result range in the required result set when querying
c. The Sql statement string (boundSql.getSql()) generated by this query is finally passed to JDBC java.sql.Preparedstatement
d. Pass the parameter value to be set to java.sql.Statement
The following analysis is combined with the source code
1. How long is the lifecycle of L1 cache?
a. When I execute opsession
final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit);
An executor object will be created, and then the type of the current executor will be judged. The default is simpleexecution
executor = new SimpleExecutor(this, transaction);
Simpleexecution inherits the baseExecutor class. The constructor of baseExecutor is called
protected BaseExecutor(Configuration configuration, Transaction transaction) { this.transaction = transaction; this.deferredLoads = new ConcurrentLinkedQueue<>(); this.localCache = new PerpetualCache("LocalCache"); this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache"); this.closed = false; this.configuration = configuration; this.wrapper = this; }
Make the current executor object hold the perpetual cache object,
this.localCache = new PerpetualCache("LocalCache");
This object is used to manage the L1 Cache. It implements the Cache interface, which is essentially a HashMap collection
When the reply ends, the close method of the Executor is called
@Override public void close(boolean forceRollback) { try { try { rollback(forceRollback); } finally { if (transaction != null) { transaction.close(); } } } catch (SQLException e) { // Ignore. There's nothing that can be done at this point. log.warn("Unexpected exception on closing transaction. Cause: " + e); } finally { transaction = null; deferredLoads = null; localCache = null; localOutputParameterCache = null; closed = true; } }
Directly reduce the localCache to null, and the object is unavailable
b. As shown in the figure above, after calling the close method, the cache will be emptied and unavailable
c. If sqlSession.clearCache() is executed; Method actually calls the clear method of cache, and cache is a map collection. Therefore, the object itself can be used only to empty the data in chche
d. When we execute any one (update|insert|delete, actually the updete method is called),
@Override public 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."); } clearLocalCache(); return doUpdate(ms, parameter); }
Will call the clearLocalCache() method to empty the cache
2. How to judge whether two queries are identical?
When we initialize mybatis and query, the API interface of selectList is called in the end
In fact, the query method of the executor is called. We enter this method
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, handler); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
CacheKey is generated here. We can see how mybatis confirms whether there are two consistent queries
CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId());//statementId cacheKey.update(rowBounds.getOffset());//Paging parameters cacheKey.update(rowBounds.getLimit());//Paging parameters cacheKey.update(boundSql.getSql());//sql statement ...Omit some codes cacheKey.update(value);//Requested parameters ...Omit some codes cacheKey.update(configuration.getEnvironment().getId());//Id of the environment tag of the core configuration file
There are six parameters in total
After creating the cache, how is the cache used? Let's continue the analysis
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
After creating the CacheKey, the query method is executed. Let's continue to take a look
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } if (queryStack == 0) { for (DeferredLoad deferredLoad : deferredLoads) { deferredLoad.load(); } // issue #601 deferredLoads.clear(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
First, query whether there is data in the cache,
If there is data, it is fetched from the cache
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
If not, query from the database and store it in the cache
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; localCache.putObject(key, EXECUTION_PLACEHOLDER); try { list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { localCache.removeObject(key); } localCache.putObject(key, list); if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
When the current sqlsession is closed, the cache is completely invalidated. When the current sqlsession is refreshed, the cache object can still be used, but the data is empty
2. L2 cache
1. The principle of L2 cache is the same as that of L1 cache. The first query will put the data into the cache, and then the second query will directly fetch it from the cache.
2. Level cache is based on the namespace of mapper file.
3. L2 cache needs to be opened manually
The following describes how to enable the L2 cache of mybatis
There are two ways:
Method 1: Based on annotation, it is not introduced here
Mode 2: xml based mode
The xml based approach is described below
First, configure cacheEnabled in the core configuration file of mybatis. The default here is true
<settings> <setting name="logImpl" value="STDOUT_LOGGING" /> <setting name="cacheEnabled" value="true"/> </settings>
Then configure it in mapper.xml
<cache/>
There are also many attributes, which will be described later
Finally, the entity class corresponding to the result should be serialized
Order implements Serializable
You can start the L2 cache
Let's analyze the source code to see how the L2 cache works
First from
<cache/>
Start analysis:
First, locate the location where the mapper XML file is parsed
public void parse() { if (!configuration.isResourceLoaded(resource)) { configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
Enter configurationElement(parser.evalNode("/mapper"));
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); 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); } }
Enter cacheElement(context.evalNode("cache");
private void cacheElement(XNode context) { if (context != null) { String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); Long flushInterval = context.getLongAttribute("flushInterval"); Integer size = context.getIntAttribute("size"); boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
Here are the attributes in the tag
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
Build Cache
public Cache useNewCache(Class<? extends Cache> typeClass, Class<? extends Cache> evictionClass, Long flushInterval, Integer size, boolean readWrite, boolean blocking, Properties props) { Cache cache = new CacheBuilder(currentNamespace) .implementation(valueOrDefault(typeClass, PerpetualCache.class)) .addDecorator(valueOrDefault(evictionClass, LruCache.class)) .clearInterval(flushInterval) .size(size) .readWrite(readWrite) .blocking(blocking) .properties(props) .build(); configuration.addCache(cache); currentCache = cache; return cache; }
We can see that a mapper.xml only constructs a cache and puts it into the configuration,
And assign the cache to MapperBuilderAssistant.currentCache
Go back to xmlmaperbuilder. After building the cache, start building the Statement,
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
Enter statementParser.parseStatementNode(); method
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
adopt
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache);
Save the previous currentCache into MappendStatement, and the label will be parsed here
Next, let's look at how the L2 cache is used in queries