Home >Java >javaTutorial >[MyBatis source code analysis] MyBatis first and second level cache
MyBatis Cache
#We know that frequent database operations are very performance-consuming (mainly because for DB, data is persistent is stored in the disk, so the query operation needs to go through IO, and the IO operation speed is several orders of magnitude slower than the memory operation speed), especially for some identical query statements, the query results can be stored, and the next query will be the same When querying the content, you can directly obtain the data from the memory, which can greatly improve query efficiency in certain scenarios.
MyBatis’s cache is divided into two types:
First-level cache, the first-level cache isSqlSession level cache, for the same query, the results will be returned from the cache instead of querying the database
Level 2 cache, The second-level cache is a cache at the Mapper level. It is defined in the
Let’s take a detailed look at the first and second level cache of MyBatis.
MyBatis first-level cache workflow
Then let’s take a look at the MyBatis first-level cache work process. As mentioned earlier, the first-level cache of MyBatis is a SqlSession-level cache. When the openSession() method finishes running or the close method of SqlSession is actively called, the SqlSession is recycled, and the first-level cache is also recycled at the same time. . As mentioned in the previous article, in MyBatis, both the selectOne and selectList methods are eventually converted to the selectList method for execution, so take a look at the implementation of the selectList method of SqlSession:
1 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { 2 try { 3 MappedStatement ms = configuration.getMappedStatement(statement); 4 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 5 } catch (Exception e) { 6 throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); 7 } finally { 8 ErrorContext.instance().reset(); 9 }10 }
Continue to trace the code in line 4, to the query method of BaseExeccutor:
1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {2 BoundSql boundSql = ms.getBoundSql(parameter);3 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);4 return query(ms, parameter, rowBounds, resultHandler, key, boundSql);5 }
Line 3 builds the cache condition CacheKey, which involves how the conditions are equal to the above A query is a question of the same condition, because the same condition can return the previous result. This part of the code will be left for analysis in the next part.
Then look at the implementation of the query method in line 4. The code is located in CachingExecutor:
1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2 throws SQLException { 3 Cache cache = ms.getCache(); 4 if (cache != null) { 5 flushCacheIfRequired(ms); 6 if (ms.isUseCache() && resultHandler == null) { 7 ensureNoOutParams(ms, parameterObject, boundSql); 8 @SuppressWarnings("unchecked") 9 List<E> list = (List<E>) tcm.getObject(cache, key);10 if (list == null) {11 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);12 tcm.putObject(cache, key, list); // issue #578 and #11613 }14 return list;15 }16 }17 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);18 }
Line 3 ~Ignore the code on line 16 and continue with the query method on line 17. The code is located in BaseExecutor:
1 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 2 ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); 3 if (closed) { 4 throw new ExecutorException("Executor was closed."); 5 } 6 if (queryStack == 0 && ms.isFlushCacheRequired()) { 7 clearLocalCache(); 8 } 9 List<E> list;10 try {11 queryStack++;12 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;13 if (list != null) {14 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);15 } else {16 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);17 }18 } finally {19 queryStack--;20 }21 ...22 }
看12行,query的时候会尝试从localCache中去获取查询结果,如果获取到的查询结果为null,那么执行16行的代码从DB中捞数据,捞完之后会把CacheKey作为key,把查询结果作为value放到localCache中。
MyBatis一级缓存存储流程看完了,接着我们从这段代码中可以得到三个结论:
MyBatis的一级缓存是SqlSession级别的,但是它并不定义在SqlSessio接口的实现类DefaultSqlSession中,而是定义在DefaultSqlSession的成员变量Executor中,Executor是在openSession的时候被实例化出来的,它的默认实现为SimpleExecutor
MyBatis中的一级缓存,与有没有配置无关,只要SqlSession存在,MyBastis一级缓存就存在,localCache的类型是PerpetualCache,它其实很简单,一个id属性+一个HashMap属性而已,id是一个名为"localCache"的字符串,HashMap用于存储数据,Key为CacheKey,Value为查询结果
MyBatis的一级缓存查询的时候默认都是会先尝试从一级缓存中获取数据的,但是我们看第6行的代码做了一个判断,ms.isFlushCacheRequired(),即想每次查询都走DB也行,将,这意味着每次查询的时候都会清理一遍PerpetualCache,PerpetualCache中没数据,自然只能走DB
从MyBatis一级缓存来看,它以单纯的HashMap做缓存,没有容量控制,而一次SqlSession中通常来说并不会有大量的查询操作,因此只适用于一次SqlSession,如果用到二级缓存的Mapper级别的场景,有可能缓存数据不断碰到而导致内存溢出。
还有一点,差点忘了写了,
1 public int update(MappedStatement ms, Object parameter) throws SQLException {2 ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());3 if (closed) {4 throw new ExecutorException("Executor was closed.");5 }6 clearLocalCache();7 return doUpdate(ms, parameter);8 }
第6行clearLocalCache()方法,这意味着所有的增、删、改都会清空本地缓存,这和是否配置了flushCache=true是无关的。
这很好理解,因为增、删、改这三种操作都可能会导致查询出来的结果并不是原来的结果,如果增、删、改不清理缓存,那么可能导致读取出来的数据是脏数据。
一级缓存的CacheKey
接着我们看下一个问题:怎么样的查询条件算和上一次查询是一样的查询,从而返回同样的结果回去?这个问题,得从CacheKey说起。
我们先看一下CacheKey的数据结构:
1 public class CacheKey implements Cloneable, Serializable { 2 3 private static final long serialVersionUID = 1146682552656046210L; 4 5 public static final CacheKey NULL_CACHE_KEY = new NullCacheKey(); 6 7 private static final int DEFAULT_MULTIPLYER = 37; 8 private static final int DEFAULT_HASHCODE = 17; 9 10 private int multiplier;11 private int hashcode;12 private long checksum;13 private int count;14 private List<Object> updateList;15 ...16 }
其中最重要的是第14行的updateList这个两个属性,为什么这么说,因为HashMap的Key是CacheKey,而HashMap的get方法是先判断hashCode,在hashCode冲突的情况下再进行equals判断,因此最终无论如何都会进行一次equals的判断,看下equals方法的实现:
1 public boolean equals(Object object) { 2 if (this == object) { 3 return true; 4 } 5 if (!(object instanceof CacheKey)) { 6 return false; 7 } 8 9 final CacheKey cacheKey = (CacheKey) object;10 11 if (hashcode != cacheKey.hashcode) {12 return false;13 }14 if (checksum != cacheKey.checksum) {15 return false;16 }17 if (count != cacheKey.count) {18 return false;19 }20 21 for (int i = 0; i < updateList.size(); i++) {22 Object thisObject = updateList.get(i);23 Object thatObject = cacheKey.updateList.get(i);24 if (thisObject == null) {25 if (thatObject != null) {26 return false;27 }28 } else {29 if (!thisObject.equals(thatObject)) {30 return false;31 }32 }33 }34 return true;35 }
看到整个方法的流程都是围绕着updateList中的每个属性进行逐一比较,因此再进一步的,我们要看一下updateList中到底存储了什么。
关于updateList里面存储的数据我们可以看下哪里使用了updateList的add方法,然后一步一步反推回去即可。updateList中数据的添加是在doUpdate方法中:
1 private void doUpdate(Object object) { 2 int baseHashCode = object == null ? 1 : object.hashCode(); 3 4 count++; 5 checksum += baseHashCode; 6 baseHashCode *= count; 7 8 hashcode = multiplier * hashcode + baseHashCode; 9 10 updateList.add(object);11 }
它的调用方为update方法:
1 public void update(Object object) { 2 if (object != null && object.getClass().isArray()) { 3 int length = Array.getLength(object); 4 for (int i = 0; i < length; i++) { 5 Object element = Array.get(object, i); 6 doUpdate(element); 7 } 8 } else { 9 doUpdate(object);10 }11 }
这里主要是对输入参数是数组类型进行了一次判断,是数组就遍历逐一做doUpdate,否则就直接做doUpdate。再看update方法的调用方,其实update方法的调用方有挺多处,但是这里我们要看的是Executor中的,看一下BaseExecutor中的createCacheKey方法实现:
1 ...2 CacheKey cacheKey = new CacheKey();3 cacheKey.update(ms.getId());4 cacheKey.update(rowBounds.getOffset());5 cacheKey.update(rowBounds.getLimit());6 cacheKey.update(boundSql.getSql());7 ...
到了这里应当一目了然了,MyBastis从三组共四个条件判断两次查询是相同的:
RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE
即只要两次查询满足以上三个条件且没有定义flushCache="true",那么第二次查询会直接从MyBatis一级缓存PerpetualCache中返回数据,而不会走DB。
MyBatis二级缓存
上面说完了MyBatis,接着看一下MyBatis二级缓存,还是从二级缓存工作流程开始。还是从DefaultSqlSession的selectList方法进去:
1 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { 2 try { 3 MappedStatement ms = configuration.getMappedStatement(statement); 4 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 5 } catch (Exception e) { 6 throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); 7 } finally { 8 ErrorContext.instance().reset(); 9 }10 }
执行query方法,方法位于CachingExecutor中:
1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {2 BoundSql boundSql = ms.getBoundSql(parameterObject);3 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);4 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);5 }
继续跟第4行的query方法,同样位于CachingExecutor中:
1 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2 throws SQLException { 3 Cache cache = ms.getCache(); 4 if (cache != null) { 5 flushCacheIfRequired(ms); 6 if (ms.isUseCache() && resultHandler == null) { 7 ensureNoOutParams(ms, parameterObject, boundSql); 8 @SuppressWarnings("unchecked") 9 List<E> list = (List<E>) tcm.getObject(cache, key);10 if (list == null) {11 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);12 tcm.putObject(cache, key, list); // issue #578 and #11613 }14 return list;15 }16 }17 return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);18 }
从这里看到,执行第17行的BaseExecutor的query方法之前,会先拿Mybatis二级缓存,而BaseExecutor的query方法会优先读取MyBatis一级缓存,由此可以得出一个重要结论:假如定义了MyBatis二级缓存,那么MyBatis二级缓存读取优先级高于MyBatis一级缓存。
而第3行~第16行的逻辑:
第5行的方法很好理解,根据flushCache=true或者flushCache=false判断是否要清理二级缓存
第7行的方法是保证MyBatis二级缓存不会存储存储过程的结果
第9行的方法先尝试从tcm中获取查询结果,这个tcm解释一下,这又是一个装饰器模式(数数MyBatis用到了多少装饰器模式了),创建一个事物缓存TranactionalCache,持有Cache接口,Cache接口的实现类就是根据我们在Mapper文件中配置的
第10行~第12行,如果没有从MyBatis二级缓存中拿到数据,那么就会查一次数据库,然后放到MyBatis二级缓存中去
至于如何判定上次查询和这次查询是一次查询?由于这里的CacheKey和MyBatis一级缓存使用的是同一个CacheKey,因此它的判定条件和前文写过的MyBatis一级缓存三个维度的判定条件是一致的。
最后再来谈一点,"Cache cache = ms.getCache()"这句代码十分重要,这意味着Cache是从MappedStatement中获取到的,而MappedStatement又和每一个
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
因此MyBatis二级缓存的生命周期即整个应用的生命周期,应用不结束,定义的二级缓存都会存在在内存中。
从这个角度考虑,为了避免MyBatis二级缓存中数据量过大导致内存溢出,MyBatis在配置文件中给我们增加了很多配置例如size(缓存大小)、flushInterval(缓存清理时间间隔)、eviction(数据淘汰算法)来保证缓存中存储的数据不至于太过庞大。
MyBatis二级缓存实例化过程
接着看一下MyBatis二级缓存
1 private void cacheElement(XNode context) throws Exception { 2 if (context != null) { 3 String type = context.getStringAttribute("type", "PERPETUAL"); 4 Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); 5 String eviction = context.getStringAttribute("eviction", "LRU"); 6 Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); 7 Long flushInterval = context.getLongAttribute("flushInterval"); 8 Integer size = context.getIntAttribute("size"); 9 boolean readWrite = !context.getBooleanAttribute("readOnly", false);10 boolean blocking = context.getBooleanAttribute("blocking", false);11 Properties props = context.getChildrenAsProperties();12 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);13 }14 }
这里分别取
type表示缓存实现,默认是PERPETUAL,根据typeAliasRegistry中注册的,PERPETUAL实际对应PerpetualCache,这和MyBatis一级缓存是一致的
eviction表示淘汰算法,默认是LRU算法
第3行~第11行拿到了所有属性,那么调用12行的useNewCache方法创建缓存:
1 public Cache useNewCache(Class<? extends Cache> typeClass, 2 Class<? extends Cache> evictionClass, 3 Long flushInterval, 4 Integer size, 5 boolean readWrite, 6 boolean blocking, 7 Properties props) { 8 Cache cache = new CacheBuilder(currentNamespace) 9 .implementation(valueOrDefault(typeClass, PerpetualCache.class))10 .addDecorator(valueOrDefault(evictionClass, LruCache.class))11 .clearInterval(flushInterval)12 .size(size)13 .readWrite(readWrite)14 .blocking(blocking)15 .properties(props)16 .build();17 configuration.addCache(cache);18 currentCache = cache;19 return cache;20 }
这里又使用了建造者模式,跟一下第16行的build()方法,在此之前该传入的参数都已经传入了CacheBuilder:
1 public Cache build() { 2 setDefaultImplementations(); 3 Cache cache = newBaseCacheInstance(implementation, id); 4 setCacheProperties(cache); 5 // issue #352, do not apply decorators to custom caches 6 if (PerpetualCache.class.equals(cache.getClass())) { 7 for (Class<? extends Cache> decorator : decorators) { 8 cache = newCacheDecoratorInstance(decorator, cache); 9 setCacheProperties(cache);10 }11 cache = setStandardDecorators(cache);12 } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {13 cache = new LoggingCache(cache);14 }15 return cache;16 }
第3行的代码,构建基础的缓存,implementation指的是type配置的值,这里是默认的PerpetualCache。
第6行的代码,如果是PerpetualCache,那么继续装饰(又是装饰器模式,可以数数这几篇MyBatis源码解析的文章里面出现了多少次装饰器模式了),这里的装饰是根据eviction进行装饰,到这一步,给PerpetualCache加上了LRU的功能。
第11行的代码,继续装饰,这次MyBatis将它命名为标准装饰,setStandardDecorators方法实现为:
1 private Cache setStandardDecorators(Cache cache) { 2 try { 3 MetaObject metaCache = SystemMetaObject.forObject(cache); 4 if (size != null && metaCache.hasSetter("size")) { 5 metaCache.setValue("size", size); 6 } 7 if (clearInterval != null) { 8 cache = new ScheduledCache(cache); 9 ((ScheduledCache) cache).setClearInterval(clearInterval);10 }11 if (readWrite) {12 cache = new SerializedCache(cache);13 }14 cache = new LoggingCache(cache);15 cache = new SynchronizedCache(cache);16 if (blocking) {17 cache = new BlockingCache(cache);18 }19 return cache;20 } catch (Exception e) {21 throw new CacheException("Error building standard cache decorators. Cause: " + e, e);22 }23 }
这次是根据其它的配置参数来:
如果配置了flushInterval,那么继续装饰为ScheduledCache,这意味着在调用Cache的getSize、putObject、getObject、removeObject四个方法的时候都会进行一次时间判断,如果到了指定的清理缓存时间间隔,那么就会将当前缓存清空
如果readWrite=true,那么继续装饰为SerializedCache,这意味着缓存中所有存储的内存都必须实现Serializable接口
跟配置无关,将之前装饰好的Cache继续装饰为LoggingCache与SynchronizedCache,前者在getObject的时候会打印缓存命中率,后者将Cache接口中所有的方法都加了Synchronized关键字进行了同步处理
如果blocking=true,那么继续装饰为BlockingCache,这意味着针对同一个CacheKey,拿数据与放数据、删数据是互斥的,即拿数据的时候必须没有在放数据、删数据
Cache全部装饰完毕,返回,至此MyBatis二级缓存生成完毕。
最后说一下,MyBatis支持三种类型的二级缓存:
MyBatis默认的缓存,type为空,Cache为PerpetualCache
自定义缓存
第三方缓存
从build()方法来看,后两种场景的Cache,MyBatis只会将其装饰为LoggingCache,理由很简单,这些缓存的定期清除功能、淘汰过期数据功能开发者自己或者第三方缓存都已经实现好了,根本不需要依赖MyBatis本身的装饰。
MyBatis二级缓存带来的问题
补充一个内容,MyBatis二级缓存使用的在某些场景下会出问题,来看一下为什么这么说。
假设我有一条select语句(开启了二级缓存):
select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;
The operations for tableA and tableB are defined in two Mappers, called MapperA and MapperB respectively, that is, they belong to two namespaces. If caching is enabled at this time:
Execute the above sql statement in MapperA to query these 6 fields
tableB updated the two fields col1 and col2
MapperA executes the above sql statement again to query these 6 fields (provided that it is not executed After any insert, delete, update operations)
The problem arises at this time, even if tableB updates col1 in step (2) With the two fields col2 , in step (3), the 6 fields obtained by MapperA through the second-level cache are still the values of the original 6 fields, because we get the values from the CacheKey Look at the three sets of conditions:
## The Namespace of the Mapper where the
The offset and limit attributes of RowBounds. RowBounds is a class used by MyBatis to handle paging. The default offset is 0 and the limit defaults to Integer.MAX_VALUE
This problem is an unsolvable problem for MyBatis's second-level cache, so there is a prerequisite for using MyBatis's second-level cache:
Must ensure that all increases Delete, modify and check all in the same namespace..
The above is the detailed content of [MyBatis source code analysis] MyBatis first and second level cache. For more information, please follow other related articles on the PHP Chinese website!