怒肝一页,搞定这篇MyBatis 缓存结构(下)
二级缓存 主要内容: 二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis 首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存。与一级缓存不同,二级缓存和具体的命名空间绑定,一级缓存则是和 SqlSession 绑定。 在按照 MyBatis 规范使用 SqlSession 的情况下,一级缓存不存在并发问题。二级缓存则不然,二级缓存可在多个命名空间间共享。这种情况下,会存在并发问题,因此需要针对性去处理。除了并发问题,二级缓存还存在事务问题。 二级缓存如何开启? 配置项 <configuration> <settings> <setting name="cacheEnabled" value="true|false" /> </settings> </configuration> cacheEnabled=true表示二级缓存可用,但是要开启话,需要在Mapper.xml内配置。 <cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> 或者 简单方式 <cache/> 对配置项属性说明: flushInterval="60000",间隔60秒清空缓存,这个间隔60秒,是被动触发的,而不是定时器轮询的。 size=512,表示队列最大512个长度,大于则移除队列最前面的元素,这里的长度指的是CacheKey的个数,默认为1024。 readOnly="true",表示任何获取对象的操作,都将返回同一实例对象。如果readOnly="false",则每次返回该对象的拷贝对象,简单说就是序列化复制一份返回。 eviction:缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。FIFO:First In First Out先进先出队列。 在Configuration类的newExecutor方法中是否开启二级缓存 public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } //是否开启二级缓存 if (cacheEnabled) { executor = new CachingExecutor(executor); } executor = (Executor) interceptorChain.pluginAll(executor); return executor; } 二级缓存通过CachingExecutor来实现的,原理是缓存里存在,就返回,不存在就调用Executor ,如果一级缓存未关闭,则先查一级缓存,不存在,再到数据库中查询。 下面使用一张图来表示: 下面是源码: @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { // 获得 BoundSql 对象 BoundSql boundSql = ms.getBoundSql(parameterObject); // 创建 CacheKey 对象 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // 查询 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } @Override public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // 调用 MappedStatement#getCache() 方法,获得 Cache 对象, //即当前 MappedStatement 对象的二级缓存。 Cache cache = ms.getCache(); if (cache != null) { // <2> // 如果需要清空缓存,则进行清空 flushCacheIfRequired(ms); //当 MappedStatement#isUseCache() 方法,返回 true 时,才使用二级缓存。默认开启。 //可通过@Options(useCache = false) 或 <select useCache="false"> 方法,关闭。 if (ms.isUseCache() && resultHandler == null) { // <2.2> // 暂时忽略,存储过程相关 ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") //从二级缓存中,获取结果 List<E> list = (List<E>) tcm.getObject(cache, key); if (list == null) { // 如果不存在,则从数据库中查询 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 缓存结果到二级缓存中 tcm.putObject(cache, key, list); // issue #578 and #116 } // 如果存在,则直接返回结果 return list; } } // 不使用缓存,则从数据库中查询 return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } 二级缓存key是如何生成的? 也是使用的是BaseExecutor类中的createCacheKey方法生成的,所以二级缓存key和一级缓存生成规则是一样的。 二级缓存范围 二级缓存有一个非常重要的空间划分策略: namespace="com.tian.mybatis.mappers.UserMapper" namespace="com.tian.mybatis.mappers.RoleMapper" 即,按照namespace划分,同一个namespace,同一个Cache空间,不同的namespace,不同的Cache空间。 比如: 在这个namespace下的二级缓存是同一个。 二级缓存什么时候会被清空? 每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。 事务不提交,二级缓存不生效? SqlSession sqlSession = sqlSessionFactory.openSession(); System.out.println("第一次查询"); User user = sqlSession.selectOne("com.tian.mybatis.mapper.UserMapper.selectById", 1); System.out.println(user); //sqlSession.commit(); SqlSession sqlSession1 = sqlSessionFactory.openSession(); System.out.println("第二次查询"); User user2 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectById", 1); System.out.println(user2); 因为二级缓存使用的是TransactionalCaheManager(tcm)来管理的,最后又调用了TranscatinalCache的getObject()、putObject()、commit方法。 TransactionalCache里面又持有真正的Cache对象,比如:经过层层装饰的PrepetualCache。 在putObject的时候,只是添加到entriesToAddOnCommit里面。 //TransactionalCache类中 @Override public void putObject(Object key, Object object) { // 暂存 KV 到 entriesToAddOnCommit 中 entriesToAddOnCommit.put(key, object); } 只有conmit方法被调用的时候,才会调用flushPendingEntries方法,真正写入到缓存里。DefaultSqlSession调用commit方法的时候就会调到这个commit方法。 //TransactionalCache类中 public void commit() { //如果 clearOnCommit 为 true ,则清空 delegate 缓存 if (clearOnCommit) { delegate.clear(); } // 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中 flushPendingEntries(); // 重置 reset(); } private void flushPendingEntries() { // 将 entriesToAddOnCommit 刷入 delegate 中 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) { delegate.putObject(entry.getKey(), entry.getValue()); } // 将 entriesMissedInCache 刷入 delegate 中 for (Object entry : entriesMissedInCache) { if (!entriesToAddOnCommit.containsKey(entry)) { delegate.putObject(entry, null); } } } private void reset() { // 重置 clearOnCommit 为 false clearOnCommit = false; // 清空 entriesToAddOnCommit、entriesMissedInCache entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } 为什么增删该操作会清空二级缓存呢? 因为在CachingExecutor的update方法中 @Override public int update(MappedStatement ms, Object parameterObject) throws SQLException { flushCacheIfRequired(ms); return delegate.update(ms, parameterObject); } private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); // 是否需要清空缓存 //通过 @Options(flushCache = Options.FlushCachePolicy.TRUE) 或 <select flushCache="true"> 方式, //开启需要清空缓存。 if (cache != null && ms.isFlushCacheRequired()) { //调用 TransactionalCache#clear() 方法,清空缓存。 //注意,此时清空的仅仅,当前事务中查询数据产生的缓存。 //而真正的清空,在事务的提交时。这是为什么呢? //还是因为二级缓存是跨 Session 共享缓存,在事务尚未结束时,不能对二级缓存做任何修改。 tcm.clear(cache); } } 如何实现多个namespace的缓存共享? 关于多个namespace的缓存共享的问题,可以使用来解决。 比如: <cache-ref namespace="com.tian.mybatis.mapper.RoleMapper" cache-ref代表引用别名的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少或者按照业务可以对表进行分组的时候可以使用。 「注意」:在这种情况下,多个mapper的操作都会引起缓存刷新,所以这里的缓存的意义已经不是很大了。 如果将第三方缓存作为二级缓存? Mybatis除了自带的二级换以外,我们还可以通过是想Cache接口来自定义二级缓存。 添加依赖 <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-redis</artifactId> <version>1.0.0-beta2</version> </dependency> redis基础配置项 host=127.0.0.1 port=6379 connectionTimeOut=5000 soTimeout=5000 datebase=0 在我们的UserMapper.xml中添加 <cache type="org.mybatis.caches.redis.RedisCache" eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> RedisCache类图,Cache就是Mybatis中缓存的顶层接口。 二级缓存应用场景 对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。 缓存查询顺序 先查二级缓存,不存在则坚持一级缓存是否关闭,没关闭,则再查一级缓存,还不存在,最后查询数据库。 二级缓存总结 二级缓存开启方式有两步: 第一步:在全局配置中添加配置 <settings> <setting name="cacheEnabled" value="true"/> </settings> 第二步,在Mapper中添加配置 <cache type="org.mybatis.caches.redis.RedisCache" eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> 二级换是默认开启的,但是针对每一个Mapper的二级缓存是需要手动开启的。 二级缓存的key和一级缓存的key是一样的。 每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。 我们可以继承第三方缓存来作为Mybatis的二级缓存。 总结 本文先从整体分析了Mybatis的缓存体系结构,然后就是对每个缓存实现类的源码进行分析,有详细的讲述一级缓存和二级缓存,如何开启关闭,缓存的范围的说明,缓存key是如何生成的,对应缓存是什么时候会被清空,先走二级缓存在走以及缓存,二级缓存使用第三方缓存。