一级缓存原理探究
一级缓存到底是什么?一级缓存的工作流程是怎样的?一级缓存何时消失?相信你现在应该会有这几个疑问,那么我们本节就来研究一下一级缓存的本质
嗯。。。。。。该从何处入手呢?
你可以这样想,上面我们一直提到一级缓存,那么提到一级缓存就绕不开 SqlSession,所以索性我们就直接从 SqlSession ,看看有没有创建缓存或者与缓存有关的属性或者方法
调研了一圈,发现上述所有方法中,好像只有 `clearCache()` 和缓存沾点关系,那么就直接从这个方法入手吧,分析源码时,**我们要看它(此类)是谁,它的父类和子类分别又是谁**,对如上关系了解了,你才会对这个类有更深的认识,分析了一圈,你可能会得到如下这个流程图
再深入分析,流程走到Perpetualcache 中的 clear() 方法之后,会调用其 cache.clear() 方法,那么这个cache 是什么东西呢? 点进去发现,cache 其实就是 private Map<Object, Object> cache = new HashMap<Object, Object>(); 也就是一个Map,所以说 cache.clear() 其实就是 map.clear() ,也就是说,缓存其实就是本地存放的一个 map 对象,每一个SqlSession 都会存放一个 map 对象的引用,那么这个 cache 是何时创建的呢?
你觉得最有可能创建缓存的地方是哪里呢? 我觉得是 Executor,为什么这么认为? 因为 Executor 是执行器,用来执行SQL请求,而且清除缓存的方法也在 Executor 中执行,所以很可能缓存的创建也很有可能在 Executor 中,看了一圈发现 Executor 中有一个 createCacheKey 方法,这个方法很像是创建缓存的方法啊,跟进去看看,你发现 createCacheKey 方法是由 `BaseExecutor` 执行的,代码如下
CacheKey cacheKey = new CacheKey(); //MappedStatement的id // id 就是Sql语句的所在位置 包名 + 类名 + SQL名称 cacheKey.update(ms.getId()); // offset 就是 0 cacheKey.update(rowBounds.getOffset()); // limit 就是 Integer.MAXVALUE cacheKey.update(rowBounds.getLimit()); // 具体的SQL语句 cacheKey.update(boundSql.getSql()); //后面是update了sql中带的参数 cacheKey.update(value); ... if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); }
创建缓存key会经过一系列的 update 方法,update 方法由一个 CacheKey 这个对象来执行的,这个 update 方法最终由 updateList 的 list 来把五个值存进去,对照上面的代码和下面的图示,你应该能理解这五个值都是什么了
这里需要注意一下最后一个值, configuration.getEnvironment().getId() 这是什么,这其实就是定义在 mybatis-config.xml 中的 <environment> 标签,见如下。
<environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> ></environments>
那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存吧,为什么叫查询缓存我们一会儿说。我们先来看一下这个缓存到底用在哪了,我们跟踪到 query 方法如下:
@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); } @SuppressWarnings("unchecked") @Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ... 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); } ... } // queryFromDatabase 方法 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; }
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。localcache 对象的put 方法最终交给 Map 进行存放
private Map<Object, Object> cache = new HashMap<Object, Object>(); @Override public void putObject(Object key, Object value) { cache.put(key, value); }
那么再说一下为什么一级缓存也叫做查询缓存呢?
我们先来看一下 update 更新方法,先来看一下 update 的源码
@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); }
由 BaseExecutor 在每次执行 update 方法的时候,都会先 clearLocalCache() ,所以更新方法并不会有缓存,这也就是说为什么一级缓存也叫做查询缓存了,这也就是为什么我们没有探究多次执行更新方法对一级缓存的影响了。
还有其他要补充的吗?
我们上面分析了一级缓存的执行流程,为什么一级缓存要叫查询缓存以及一级缓存组成条件
那么,你可能看到这感觉这些知识还是不够连贯,那么我就帮你把 `一级缓存的探究 `小结中的原理说一下吧,为什么一级缓存会失效
1. 探究更新对一级缓存失效的影响: 由上面的分析结论可知,我们每次执行 update 方法时,都会先刷新一级缓存,因为是同一个 SqlSession, 所以是由同一个 Map 进行存储的,所以此时一级缓存会失效
2. 探究不同的 SqlSession 对一级缓存的影响: 这个也就比较好理解了,因为不同的 SqlSession 会有不同的Map 存储一级缓存,然而 SqlSession 之间也不会共享,所以此时也就不存在相同的一级缓存
3. 同一个 SqlSession 使用不同的查询操作: 这个论点就需要从缓存的构成角度来讲了,我们通过 cacheKey 可知,一级缓存命中的必要条件是两个 cacheKey 相同,要使得 cacheKey 相同,就需要使 cacheKey 里面的值相同,也就是
看出差别了吗?第一个SQL 我们查询的是部门编号为1的值,而第二个SQL我们查询的是编号为5的值,两个缓存对象不相同,所以也就不存在缓存。
4. 手动清理缓存对一级缓存的影响: 由程序员自己去调用clearCache方法,这个方法就是清除缓存的方法,所以也就不存在缓存了。
总结
所以此文章到底写了点什么呢?抛给你几个问题了解一下
1. 什么是缓存?什么是 MyBatis 缓存?
2. 认识MyBatis缓存,MyBatis 一级缓存的失效方式
3. MyBatis 一级缓存的执行流程,MyBatis 一级缓存究竟是什么?