Mybatis 与 Hibernate 同样,支持一二级缓存。一级缓存指的是 Session 级别的缓存,即在一个会话中屡次执行同一条 SQL 语句而且参数相同,则后面的查询将不会发送到数据库,直接从 Session 缓存中获取。二级缓存,指的是 SessionFactory 级别的缓存,即不一样的会话能够共享。java
缓存,一般涉及到缓存的写、读、过时(更新缓存)等几个方面,请带着这些问题一块儿来探究Mybatis关于缓存的实现原理吧。算法
提出问题:缓存的查询顺序,是先查一级缓存仍是二级缓存?数据库
本文以 SQL 查询与更新两个流程来揭开 Mybatis 缓存实现的细节。缓存
舒适提示,建议在阅读本文以前先阅读笔者的另外几篇文章:
1)源码分析Mybatis MapperProxy初始化【图文并茂】
2)源码分析Mybatis MappedStatement的建立流程
3)【图文并茂】源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解
4)【图文并茂】Mybatis执行SQL的4大基础组件详解微信
舒适提示,本文不会详细介绍详细的 SQL 执行流程,若是对其感兴趣,能够查阅笔者的另一篇文章:【图文并茂】源码解析MyBatis Sharding-Jdbc SQL语句执行流程详解mybatis
具体实现由 Configuration 的 newExecutor 方法实现。app
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) { // @1 executor = new CachingExecutor(executor); // @2 } executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
代码@1:若是 cacheEnabled 为 true,表示开启缓存机制,缓存的实现类为 CachingExecutor,这里使用了经典的装饰模式,处理了缓存的相关逻辑后,委托给的具体的 Executor 执行。框架
cacheEnable 在实际的使用中经过在 mybatis-config.xml 文件中指定,例如:ide
<configuration> <settings> <setting name="cacheEnabled" value="true"> </settings> </configuration>
该值默认为true。源码分析
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameterObject); // @1 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // @2 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // @3 }
代码@1:根据参数生成SQL语句。
代码@2:根据 MappedStatement、参数、分页参数、SQL 生成缓存 Key。
代码@3:调用6个参数的 query 方法。
缓存 Key 的建立比较简单,本文就只贴出代码,你们一目了然,你们重点关注组成缓存Key的要素。
BaseExecute#createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (closed) { throw new ExecutorException("Executor was closed."); } CacheKey cacheKey = new CacheKey(); cacheKey.update(ms.getId()); cacheKey.update(rowBounds.getOffset()); cacheKey.update(rowBounds.getLimit()); cacheKey.update(boundSql.getSql()); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); // mimic DefaultParameterHandler logic for (ParameterMapping parameterMapping : parameterMappings) { if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } cacheKey.update(value); } } if (configuration.getEnvironment() != null) { // issue #176 cacheKey.update(configuration.getEnvironment().getId()); } return cacheKey; }
接下来重点看CachingExecutor的另一个query方法。
CachingExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); // @1 if (cache != null) { flushCacheIfRequired(ms); // @2 if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); // @3 if (list == null) { // @4 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@5 tcm.putObject(cache, key, list); // issue #578 and #116 // @6 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@7 }
代码@1:获取 MappedStatement 中的 Cache cache 属性。
代码@2:若是不为空,则尝试从缓存中获取,不然直接委托给具体的执行器执行,例如 SimpleExecutor (@7)。
代码@3:尝试从缓存中根据缓存 Key 查找。
代码@4:若是从缓存中获取的值不为空,则直接返回缓存中的值,不然先从数据库查询@5,将查询结果更新到缓存中。
这里的缓存即 MappedStatement 中的 Cache 对象是一级缓存仍是二级缓存?一般在 ORM 类框架中,Session 级别的缓存为一级缓存,即会话结束后就会失效,显然这里不会随着 Session 的失效而失效,由于 Cache 对象是存储在于 MappedStatement 对象中的,每个 MappedStatement 对象表明一个 Dao(Mapper) 中的一个方法,即表明一条对应的 SQL 语句,是一个全局的概念。
相信你们也会以为,想继续深刻了解 CachingExecutor 中使用的 Cache 是一级缓存仍是二级缓存,了解 Cache 对象的建立相当重要。关于 MappedStatement 的建立流程,建议查阅笔者的另一篇博文:源码分析Mybatis MappedStatement的建立流程。
本文只会关注 MappedStatement 对象流程中关于缓存相关的部分。
接下来将按照先二级缓存,再一级缓存的思路进行讲解。
从上面看,若是 cacheEnable 为 true 而且 MappedStatement 对象的 cache 属性不为空,则能使用二级缓存。
咱们能够看到 MappedStatement 对象的 cache 属性赋值的地方为:MapperBuilderAssistant 的 addMappedStatement 方法,从该方法的调用链能够得知是在解析 Mapper 定义的时候就会建立。
在这里插入图片描述
使用的 cache 属性为 MapperBuilderAssistant 的 currentCache,咱们跟踪一下该属性的赋值方法:
public Cache useCacheRef(String namespace)
其调用链以下:
在这里插入图片描述
能够看出是在解析 cacheRef 标签,即在解析 Mapper.xml 文件中的 cacheRef 标签时,即二级缓存的使用和 cacheRef 标签离不开关系,而且特别注意一点,其参数为 namespace,即每个 namespace 对应一个 Cache 对象,在 Mybatis 的方法中,一般namespace 对一个 Mapper.java 对象,对应对数据库一张表的更新、新增操做。
public Cache useNewCache
其调用链以下图所示:
在这里插入图片描述
在解析 Mapper.xml 文件中的 cache 标签时被调用。
接下来咱们根据 cache 标签简单看一下 cache 标签的解析,下面以 xml 配置方式为例展开,基于注解的解析,其原理相似,其代码 XMLMapperBuilder 的 cacheElement 方法。
private void cacheElement(XNode context) throws Exception { 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); } }
从上面 cache 标签的核心属性以下:
在这里插入图片描述
cacheRef 只有一个属性,就是 namespace,就是引用其余 namespace 中的 cache。
Cache 的建立流程就讲解到这里,同一个 Namespace 只会定义一个 Cache。二级缓存的建立是在 *Mapper.xml 文件中使用了< cache/>、< cacheRef/>标签时建立,而且会按 NameSpace 为维度,为各个 MapperStatement 传入它所属的 Namespace 的二级缓存对象。
二级缓存的查询逻辑就介绍到这里了,咱们再次回看 CacheingExecutor 的查询方法:
CachingExecutor#query
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { Cache cache = ms.getCache(); // @1 if (cache != null) { flushCacheIfRequired(ms); // @2 if (ms.isUseCache() && resultHandler == null) { ensureNoOutParams(ms, boundSql); @SuppressWarnings("unchecked") List<E> list = (List<E>) tcm.getObject(cache, key); // @3 if (list == null) { // @4 list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@5 tcm.putObject(cache, key, list); // issue #578 and #116 // @6 } return list; } } return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@7 }
若是 MappedStatement 的 cache 属性为空,则直接调用内部的 Executor 的查询方法。也就是若是在 *.Mapper.xm l文件中未定义< cache/>或< cacheRef/>,则 cache 属性会为空。
Mybatis 根据 SQL 的类型共有以下3种 Executor类型,分别是 SIMPLE, REUSE, BATCH,本文将以 SimpleExecutor为 例来对一级缓存的介绍。
BaseExecutor#query
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()) { // @1 clearLocalCache(); } List<E> list; try { queryStack++; list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; // @2 if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // @3 } } 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; }
代码@1:queryStack:查询栈,每次查询以前,加一,查询返回结果后减一,若是为1,表示整个会会话中没有执行的查询语句,并根据 MappedStatement 是否须要执行清除缓存,若是是查询类的请求,无需清除缓存,若是是更新类操做的MappedStatemt,每次执行以前都须要清除缓存。
代码@2:若是缓存中存在,直接返回缓存中的数据。
代码@3:若是缓存未命中,则调用 queryFromDatabase 从数据中查询。
咱们顺便看一下 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); // @2 } finally { localCache.removeObject(key); // @3 } localCache.putObject(key, list); // @4 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list; }
代码@1:先往本地缓存存入一个特定值,表示正在执行中。
代码@2:从数据中查询数据。
代码@3:先移除正在执行中的标记。
代码@4:将数据库中的值存储到一级缓存中。
能够看出一级缓存的属性为 localCache,为 Executor 的属性。若是你们看过笔者发布的这个 Mybatis 系列就能轻易得出一个结论,每个 SQL 会话对应一个 SqlSession 对象,每个 SqlSession 会对应一个 Executor 对象,故 Executor 级别的缓存即为Session 级别的缓存,即为 Mybatis 的一级缓存。
上面已经介绍了一二级缓存的查找与添加,在查询的时候,首先查询缓存,若是缓存未命中,则查询数据库,而后将查询到的结果存入缓存中。
下面咱们来简单看看缓存的更新。
从更新的角度,更加的是关注缓存的更新,即当数据发生变化后,若是清除对应的缓存。
CachingExecutor#update
public int update(MappedStatement ms, Object parameterObject) throws SQLException { flushCacheIfRequired(ms); // @1 return delegate.update(ms, parameterObject); // @2 }
代码@1:若是有必要则刷新缓存。
代码@2:调用内部的 Executor,例如 SimpleExecutor。
接下来重点看一下 flushCacheIfRequired 方法。
private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } } TransactionalCacheManager#clear public void clear(Cache cache) { getTransactionalCache(cache).clear(); }
TransactionalCacheManager 事务缓存管理器,其实就是对 MappedStatement 的 cache 属性进行装饰,最终调用的仍是MappedStatement 的 getCache 方法获得其缓存对象而后调用 clear 方法,清空全部的缓存,即缓存的更新策略是只要namespace 的任何一条插入或更新语句执行,整个 namespace 的缓存数据将所有清空。
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); }
其更新策略与二级缓存维护的同样。
一二级缓存的的新增、查询、更新就介绍到这里了,接下来对其进行一个总结。
Mybatis 一二级缓存时序图以下:
在这里插入图片描述
3.2 如何使用二级缓存
一、在mybatis-config.xml中将cacheEnable设置为true。例如:
<configuration> <settings> <setting name="cacheEnabled" value="true"> </settings> </configuration>
不过该值默认为true。
二、在须要缓存的表操做,对应的 Dao 的配置文件中,例如 *Mapper.xml 文件中使用 cache、或 cacheRef 标签来定义缓存。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.winterchen.dao.UserDao" > <insert id="insert" parameterType="com.winterchen.model.UserDomain"> //省略 </insert> <select id="selectUsers" resultType="com.winterchen.model.UserDomain"> //省略 </select> <cache type="lru" readOnly="true" flushInterval="3600000"></cache> </mapper>
这样就定义了一个 Cache,其 namespace 为 com.winterchen.dao.UserDao。其中 flushInterval 定义该 cache 定时清除的时间间隔,单位为 ms。
若是一个表的更新操做、新增操做位于不一样的 Mapper.xml 文件中,若是对一个表的操做的 Cache 定义在不一样的文件,则缓存数据则会出现不一致的状况,由于 Cache 的更新逻辑是,在一个 Namespace 中,若是有更新、插入语句的执行,则会清除该 namespace 对应的 cache 里面的全部缓存。那怎么来处理这种场景呢?cacheRef 闪亮登场。
若是一个 Mapper.xml 文件须要引入定义在别的 Mapper.xml 文件中定义的 cache,则使用 cacheRef,示例以下:
<cacheRef "namespace" = "com.winterchen.dao.UserDao"/>
一级缓存默认是开启的,也没法关闭。
缓存的介绍就介绍到这里。若是本文对您有所帮助,麻烦点一下赞,谢谢。
更多文章请关注微信公众号: