Mybatis一二级缓存实现原理与使用指南

Mybatis 与 Hibernate 一样,支持一二级缓存。一级缓存指的是 Session 级别的缓存,即在一个会话中多次执行同一条 SQL 语句并且参数相同,则后面的查询将不会发送到数据库,直接从 Session 缓存中获取。二级缓存,指的是 SessionFactory 级别的缓存,即不同的会话可以共享。

缓存,通常涉及到缓存的写、读、过期(更新缓存)等几个方面,请带着这些问题一起来探究Mybatis关于缓存的实现原理吧。

>提出问题:缓存的查询顺序,是先查一级缓存还是二级缓存?

本文以SQL查询与更新两个流程来揭开Mybatis缓存实现的细节。

>温馨提示,建议在阅读本文之前先阅读笔者的另外几篇文章:

1、源码分析Mybatis MapperProxy初始化之Mapper对象的扫描与构建

2、源码分析Mybatis MappedStatement的创建流程

3、源码分析Mybatis SQL执行流程

4、Mybatis执行SQL的4大基础组件详解

1、从 SQL 查询流程看一二级缓存

>温馨提示,本文不会详细介绍详细的 SQL 执行流程,如果对其感兴趣,可以查阅笔者的另外一篇文章:源码分析Mybatis SQL执行流程

1.1 创建Executor

Configuration#newExecutor

1public Executor newExecutor(Transaction transaction, ExecutorType executorType) { 2 executorType = executorType == null ? defaultExecutorType : executorType; 3 executorType = executorType == null ? ExecutorType.SIMPLE : executorType; 4 Executor executor; 5 if (ExecutorType.BATCH == executorType) { 6 executor = new BatchExecutor(this, transaction); 7 } else if (ExecutorType.REUSE == executorType) { 8 executor = new ReuseExecutor(this, transaction); 9 } else { 10 executor = new SimpleExecutor(this, transaction); 11 } 12 if (cacheEnabled) { // [@1](https://my.oschina.net/u/1198) 13 executor = new CachingExecutor(executor); // @2 14 } 15 executor = (Executor) interceptorChain.pluginAll(executor); 16 return executor; 17}

代码@1:如果 cacheEnabled 为 true,表示开启缓存机制,缓存的实现类为 CachingExecutor,这里使用了经典的装饰模式,处理了缓存的相关逻辑后,委托给的具体的 Executor 执行。

cacheEnable 在实际的使用中通过在 mybatis-config.xml 文件中指定,例如:

1<configuration> 2 <settings> 3 <setting name="cacheEnabled" value="true"> 4 </setting></settings> 5</configuration>

该值默认为true。

1.2 CachingExecutor#query

1public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { 2 BoundSql boundSql = ms.getBoundSql(parameterObject); // [@1](https://my.oschina.net/u/1198) 3 CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // @2 4 return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // @3 5}

代码@1:根据参数生成SQL语句。

代码@2:根据 MappedStatement、参数、分页参数、SQL 生成缓存 Key。

代码@3:调用6个参数的 query 方法。

缓存 Key 的创建比较简单,本文就只贴出代码,大家一目了然,大家重点关注组成缓存Key的要素。 BaseExecute#createCacheKey

1public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { 2 if (closed) { 3 throw new ExecutorException("Executor was closed."); 4 } 5 CacheKey cacheKey = new CacheKey(); 6 cacheKey.update(ms.getId()); 7 cacheKey.update(rowBounds.getOffset()); 8 cacheKey.update(rowBounds.getLimit()); 9 cacheKey.update(boundSql.getSql()); 10 List<parametermapping> parameterMappings = boundSql.getParameterMappings(); 11 TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry(); 12 // mimic DefaultParameterHandler logic 13 for (ParameterMapping parameterMapping : parameterMappings) { 14 if (parameterMapping.getMode() != ParameterMode.OUT) { 15 Object value; 16 String propertyName = parameterMapping.getProperty(); 17 if (boundSql.hasAdditionalParameter(propertyName)) { 18 value = boundSql.getAdditionalParameter(propertyName); 19 } else if (parameterObject == null) { 20 value = null; 21 } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { 22 value = parameterObject; 23 } else { 24 MetaObject metaObject = configuration.newMetaObject(parameterObject); 25 value = metaObject.getValue(propertyName); 26 } 27 cacheKey.update(value); 28 } 29 } 30 if (configuration.getEnvironment() != null) { 31 // issue #176 32 cacheKey.update(configuration.getEnvironment().getId()); 33 } 34 return cacheKey; 35}

接下来重点看CachingExecutor的另外一个query方法。

CachingExecutor#query

1public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2 throws SQLException { 3 Cache cache = ms.getCache(); // @1 4 if (cache != null) { 5 flushCacheIfRequired(ms); // @2 6 if (ms.isUseCache() &amp;&amp; resultHandler == null) { 7 ensureNoOutParams(ms, boundSql); 8 @SuppressWarnings("unchecked") 9 List<e> list = (List<e>) tcm.getObject(cache, key); // @3 10 if (list == null) { // @4 11 list = delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@5 12 tcm.putObject(cache, key, list); // issue #578 and #116 // @6 13 } 14 return list; 15 } 16 } 17 return delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@7 18}

代码@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 对象流程中关于于缓存相关的部分。

接下来将按照先二级缓存,再一级缓存的思路进行讲解。

1.2.1 二级缓存

1.2.1.1 MappedStatement#cache属性创建机制

从上面看,如果 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 标签时被调用。

1.2.1.2 cache标签解析

接下来我们根据 cache 标签简单看一下 cache 标签的解析,下面以 xml 配置方式为例展开,基于注解的解析,其原理类似,其代码 XMLMapperBuilder 的 cacheElement 方法。

1private 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 }

从上面 cache 标签的核心属性如下:

  • type 缓存实现类,可选择值:PERPETUAL、LRU 等,Mybatis 中所有的缓存实现类如下: 在这里插入图片描述
  • eviction 移除算法,默认为 LRU。
  • flushInterval 缓存过期时间。
  • size 缓存在内存中的缓存个数。
  • readOnly 是否是只读。
  • blocking 是否阻塞,具体实现请看 BlockingCache。
1.2.1.3 cacheRef

在这里插入图片描述

cacheRef 只有一个属性,就是 namespace,就是引用其他 namespace 中的 cache。

Cache 的创建流程就讲解到这里,同一个 Namespace 只会定义一个 Cache。二级缓存的创建是在 *Mapper.xml 文件中使用了< cache/>、< cacheRef/>标签时创建,并且会按 NameSpace 为维度,为各个 MapperStatement 传入它所属的 Namespace 的二级缓存对象。

二级缓存的查询逻辑就介绍到这里了,我们再次回成 CacheingExecutor 的查询方法: CachingExecutor#query

1public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2 throws SQLException { 3 Cache cache = ms.getCache(); // @1 4 if (cache != null) { 5 flushCacheIfRequired(ms); // @2 6 if (ms.isUseCache() &amp;&amp; resultHandler == null) { 7 ensureNoOutParams(ms, boundSql); 8 @SuppressWarnings("unchecked") 9 List<e> list = (List<e>) tcm.getObject(cache, key); // @3 10 if (list == null) { // @4 11 list = delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@5 12 tcm.putObject(cache, key, list); // issue #578 and #116 // @6 13 } 14 return list; 15 } 16 } 17 return delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); //@7 18}

如果 MappedStatement 的 cache 属性为空,则直接调用内部的 Executor 的查询方法。也就时如果在 *.Mapper.xm l文件中未定义< cache/>或< cacheRef/>,则 cache 属性会为空。

1.2.2 一级缓存

Mybatis 根据 SQL 的类型共有如下3种 Executor类型,分别是 SIMPLE, REUSE, BATCH,本文将以 SimpleExecutor为 例来对一级缓存的介绍。

BaseExecutor#query

1public <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 &amp;&amp; ms.isFlushCacheRequired()) { // @1 7 clearLocalCache(); 8 } 9 List<e> list; 10 try { 11 queryStack++; 12 list = resultHandler == null ? (List<e>) localCache.getObject(key) : null; // @2 13 if (list != null) { 14 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); 15 } else { 16 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); // @3 17 } 18 } finally { 19 queryStack--; 20 } 21 if (queryStack == 0) { 22 for (DeferredLoad deferredLoad : deferredLoads) { 23 deferredLoad.load(); 24 } 25 // issue #601 26 deferredLoads.clear(); 27 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { 28 // issue #482 29 clearLocalCache(); 30 } 31 } 32 return list; 33 }

代码@1:queryStack:查询栈,每次查询之前,加一,查询返回结果后减一,如果为1,表示整个会会话中没有执行的查询语句,并根据 MappedStatement 是否需要执行清除缓存,如果是查询类的请求,无需清除缓存,如果是更新类操作的MappedStatemt,每次执行之前都需要清除缓存。

代码@2:如果缓存中存在,直接返回缓存中的数据。

代码@3:如果缓存未命中,则调用 queryFromDatabase 从数据中查询。

我们顺便看一下 queryFromDatabase 方法,再来看一下一级缓存的实现类。

1 private <e> List<e> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 2 List<e> list; 3 localCache.putObject(key, EXECUTION_PLACEHOLDER); //@! 4 try { 5 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); // @2 6 } finally { 7 localCache.removeObject(key); // @3 8 } 9 localCache.putObject(key, list); // @4 10 if (ms.getStatementType() == StatementType.CALLABLE) { 11 localOutputParameterCache.putObject(key, parameter); 12 } 13 return list; 14 }

代码@1:先往本地遍历存储一个厂里,表示正在执行中。

代码@2:从数据中查询数据。

代码@3:先移除正在执行中的标记。

代码@4:将数据库中的值存储到一级缓存中。

可以看出一级缓存的属性为 localCache,为 Executor 的属性。如果大家看过笔者发布的这个 Mybatis 系列就能轻易得出一个结论,每一个 SQL 会话对应一个 SqlSession 对象,每一个 SqlSession 会对应一个 Executor 对象,故 Executor 级别的缓存即为Session 级别的缓存,即为 Mybatis 的一级缓存。

上面已经介绍了一二级缓存的查找与添加,在查询的时候,首先查询缓存,如果缓存未命中,则查询数据库,然后将查询到的结果存入缓存中。

下面我们来简单看看缓存的更新。

2、从SQL更新流程看一二级缓存

从更新的角度,更加的是关注缓存的更新,即当数据发生变化后,如果清除对应的缓存。

2.1 二级缓存

CachingExecutor#update

1public int update(MappedStatement ms, Object parameterObject) throws SQLException { 2 flushCacheIfRequired(ms); // @1 3 return delegate.update(ms, parameterObject); // @2 4}

代码@1:如果有必要则刷新缓存。 代码@2:调用内部的 Executor,例如 SimpleExecutor。

接下来重点看一下 flushCacheIfRequired 方法。

1private void flushCacheIfRequired(MappedStatement ms) { 2 Cache cache = ms.getCache(); 3 if (cache != null &amp;&amp; ms.isFlushCacheRequired()) { 4 tcm.clear(cache); 5 } 6} 7 8TransactionalCacheManager#clear 9public void clear(Cache cache) { 10 getTransactionalCache(cache).clear(); 11}

TransactionalCacheManager 事务缓存管理器,其实就是对 MappedStatement 的 cache 属性进行装饰,最终调用的还是MappedStatement 的 getCache 方法得到其缓存对象然后调用 clear 方法,清空所有的缓存,即缓存的更新策略是只要namespace 的任何一条插入或更新语句执行,整个 namespace 的缓存数据将全部清空。

2.2 一级缓存的更新

1public 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}

其更新策略与二级缓存维护的一样。

一二级缓存的的新增、查询、更新就介绍到这里了,接下来对其进行一个总结。

3、总结

3.1 一二级缓存作用序列图

Mybatis 一二级缓存时序图如下: 在这里插入图片描述

3.2 如何使用二级缓存

1、在mybatis-config.xml中将cacheEnable设置为true。例如:

1<configuration> 2 <settings> 3 <setting name="cacheEnabled" value="true"> 4 </setting></settings> 5</configuration>

不过该值默认为true。

2、在需要缓存的表操作,对应的 Dao 的配置文件中,例如 *Mapper.xml 文件中使用 cache、或 cacheRef 标签来定义缓存。

1<!--?xml version="1.0" encoding="UTF-8" ?--> 2 3<mapper namespace="com.winterchen.dao.UserDao"> 4 <insert id="insert" parametertype="com.winterchen.model.UserDomain"> 5 //省略 6 </insert> 7 <select id="selectUsers" resulttype="com.winterchen.model.UserDomain"> 8 //省略 9 </select> 10 <cache type="lru" readonly="true" flushinterval="3600000"></cache> 11</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" />

一级缓存默认是开启的,也无法关闭。

缓存的介绍就介绍到这里。如果本文对您有所帮助,麻烦点一下赞,谢谢。


作者介绍:丁威,《RocketMQ技术内幕》作者,RocketMQ 社区布道师,公众号:中间件兴趣圈 维护者,目前已陆续发表源码分析Java集合、Java 并发包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等源码专栏。如果希望第一时间获取最新的干货文章可关注微信公众号,如果期望加入高质量的技术交流圈,可以加入知识兴趣:一个高质量交流社群</e></e></e></e></e></e></e></e></e></e></e></e></e></e></e></e></e></e></e></parametermapping></e></e>

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )