一、Mybatis缓存介绍

  在Mybatis中,它提供了一级缓存和二级缓存,默认的情况下只开启一级缓存,所以默认情况下是开启了缓存的,除非明确指定不开缓存功能。使用缓存的目的就是把数据保存在内存中,是应用能更快获取数据,避免与数据库频繁交互,特别是在查询比较多、命中率比较高的情况下,缓存就显得很重要。但是使用不得当,会产生脏数据。

二、目录

  • 一级缓存介绍及相关配置。
  • 一级缓存工作流程及源码分析。
  • 一级缓存总结。
  • 二级缓存介绍及相关配置。
  • 二级缓存源码分析。
  • 二级缓存总结。
  • 全文总结。

三、一级缓存介绍及相关配置

  一级缓存是有Session、Statement两种级别。Session级别,在同一个SqlSession中,执行相同的SQL查询,第一次从数据库里面找出来,然后写到缓存中,后续的查询就会从一级缓存中取出来。若是在一次数据库会话中发生了在修改操作后,再次执行的相同查询,会发现Myabtis查询了数据库,说明一级缓存失效;如果没有声明要刷新,而且缓存时间超时了,缓存会被清空。Statement级别,可以理解为支队当前执行的这个Statement有效。

  mybatis-config中关于缓存的部分配置:

1 <setting name="localCacheScope" value="SESSION"/>

四、一级缓存工作流程以及源码分析

执行流程大致为下图:

在上一篇文章中的BaseExecutor的query方法中有一断代码,根据算法生成的key,从缓存中查询是否存在需要查询的的对象,若为空,则从数据库中查询结果

 1     try {
2 queryStack++;
3 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;  //从缓存中查询
4 if (list != null) {
5 handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
6 } else {
7 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); //从数据库中查询
8 }
9 } finally {
10 queryStack--;
11   }

BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。如下代码所示:

1 public class PerpetualCache implements Cache {
2
3 private final String id;
4
5 private Map<Object, Object> cache = new HashMap<>();
6
7 ... ...
8 }

SQL语句执行过程在上一篇文章中已经详细解释,BaseExecutor的query方法执行到最后会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因

 1 if (queryStack == 0) {
2 for (DeferredLoad deferredLoad : deferredLoads) {
3 deferredLoad.load();
4 }
5 // issue #601
6 deferredLoads.clear();
7 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
8 // issue #482
9 clearLocalCache();
10 }
11 }

接下来看insert和delete方法,它们都是走update方法

 1   @Override
2 public int insert(String statement) {
3 return insert(statement, null);
4 }
5
6 @Override
7 public int insert(String statement, Object parameter) {
8 return update(statement, parameter);
9 }
10
11 @Override
12 public int delete(String statement) {
13 return update(statement, null);
14 }
15
16 @Override
17 public int delete(String statement, Object parameter) {
18 return update(statement, parameter);
19 }
 1   @Override
2 public int update(String statement, Object parameter) {
3 try {
4 dirty = true;
5 MappedStatement ms = configuration.getMappedStatement(statement);
6 return executor.update(ms, wrapCollection(parameter));
7 } catch (Exception e) {
8 throw ExceptionFactory.wrapException("Error updating database. Cause: " +
9 e, e);
10 } finally {
11 ErrorContext.instance().reset();
12 }
13 }

再转到BaseExecutor看update方法,最后把doUpdate交给了继承了BaseExecutor的Executor中

1 @Override
2 public int update(MappedStatement ms, Object parameter) throws SQLException {
3 ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
4 if (closed) {
5 throw new ExecutorException("Executor was closed.");
6 }
7 clearLocalCache();  //清空了缓存
8 return doUpdate(ms, parameter);
9 }

再看到SimpleExecutor(默认)源码,清空了缓存,然后往数据库中执行更新操作,所以一级缓存发生更新操作时,会清空缓存

 1 @Override
2 public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
3 Statement stmt = null;
4 try {
5 Configuration configuration = ms.getConfiguration();
6 StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
7 stmt = prepareStatement(handler, ms.getStatementLog());
8 return handler.update(stmt);
9 } finally {
10 closeStatement(stmt);
11 }
12 }

然后都是上一篇介绍SQL执行过程的内容了。

五、一级缓存的总结

  • MyBatis一级缓存的生命周期和SqlSession一致。

  • MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。

  • MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

六、二级缓存介绍及相关配置

  一级缓存中最大的共享范围是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要开启二级缓存。二级缓存开启的时候,调用Configuration的newExecutor方法,利用CachingExecutor装饰Executor,在进入一级缓存的查询前,先执行二级缓存的查询的流程。

mybatis-config中关于开启二级缓存的部分配置:

1 <setting name="cacheEnabled" value="true"/>

  在保证开启全局二级缓存的条件下,可以在Mapper.xml文件中配置二级缓存,在<mapper>节点内添加<cache/>节点(这里单单是默认的配置)

默认的二级缓存有以下效果:

  • 映射语句文件中的所有 SELECT 语句将会被缓存
  • 映射语句文件中的所有 SELECT、UPDATE、DELETE 语句会刷新缓存,缓存会使用 Least Recently Used(LRU,最近最少使用)的算法来收回
  • 根据时间表(如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新 缓存会存储集合或对象(无论查询方法返回什么类型的值)的 10 个引用。
  • 缓存会被视为 ad/writ (可读/可写)的 意味着对象检索不是共享的,而且可以安全 地被调用者修改,而不干扰其 调用者或线程所做的潜在修改

所有的这些属性都可以通过修改<cache/>

七、二级缓存源码分析

  二级缓存开启,在一个SqlSession查询了对象,用另外一个SqlSession中查同样的数据,首先CachingExecutor先从二级缓存中查询

  在CachingExecutor的query方法里,首先从MappedStatement中获取配置初始化时赋予的Cache,本质上是装饰器模式的使用,具体的装饰链是:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。

 1 @Override
2 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
3 throws SQLException {
4 Cache cache = ms.getCache(); //获取配置初始化时的cache 5 if (cache != null) {
6 flushCacheIfRequired(ms); //判断是否刷新
7 if (ms.isUseCache() && resultHandler == null) {
8 ensureNoOutParams(ms, boundSql); //主要是用来处理存储过程的
9 @SuppressWarnings("unchecked")
10 List<E> list = (List<E>) tcm.getObject(cache, key); //从tcm中获取缓存的列表,把获取值的职责一路传递,最终到perpetualCache
11 if (list == null) {
12 list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
13 tcm.putObject(cache, key, list); //如果查询到数据,则调用tcm.putObject方法,往缓存中放入值;不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中
14 }
15 return list;
16 }
17 }
18
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
19 }

以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。

  • SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
  • LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
  • SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
  • LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
  • PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

看到Lru算法,再来说说缓存的收回策略 eviction :

  • LRU(最近最少使用的):移除最长时间不被使用的对象,这是默认值
  • FIFO(先进先出): 按对象进入缓存的顺序来移除它们
  • SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
  • WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象
1 <cache
2 eviction = "FIFO"
3 flushlnterval = "60000"
4 size = "512"
5 readOnly = "true" \ />

flushinterval (刷新间隔):可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新。

size (引用数目):可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是 1024。

readOnly (只读):属性可以被设置为 true false 。只读的缓存会给所有调用者 返回缓存对象的相同实例,因此这些对象不能被修改 这提供了很重要的性能优势。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但是安全。因此默认是 false。

MyBatis的CachingExecutor持有了TransactionalCacheManager,上面的tcm,它里面有一个Map,这个Map保存了Cache和TransactionalCache包装后的clear方法

1 private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。TransactionalCache里面有一个clear方法

1 @Override
2 public void clear() {
3 clearOnCommit = true;
4 entriesToAddOnCommit.clear();
5 }

CachingExecutor里面的commit方法

1 @Override
2 public void commit(boolean required) throws SQLException {
3 delegate.commit(required);
4 tcm.commit();
5 }

TransactionalCachingManager的commit方法

1 public void commit() {
2 for (TransactionalCache txCache : transactionalCaches.values()) {
3 txCache.commit();
4 }
5 }

TransactionalCache的commit方法

1 public void commit() {
2 if (clearOnCommit) {
3 delegate.clear();
4 }
5 flushPendingEntries();
6 reset();
7 }

TransactionalCache的flushPendingEntries方法,在flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。

1 private void flushPendingEntries() {
2 for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
3 delegate.putObject(entry.getKey(), entry.getValue());
4 }
5 ......
6 }

后续的查询操作会重复执行这套流程。如果是 insert | update | delete 的话,会统一进入CachingExecutor的update方法

1 private void flushCacheIfRequired(MappedStatement ms) 

二级缓存执行后会执行一级缓存操作。

八、二级缓存总结

  • MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。

  • MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。

  • 在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

九、总结

本文对介绍了MyBatis一二级缓存的基本概念,并从应用及源码的角度对MyBatis的缓存机制进行了分析。最后对MyBatis缓存机制做了一定的总结,个人建议MyBatis缓存特性在生产环境中进行关闭,单纯作为一个ORM框架使用可能更为合适。

参考:聊聊MyBatis缓存机制

MyBatis源码分析(五):MyBatis Cache分析的更多相关文章

  1. Mybatis源码解析(一) —— mybatis与Spring是如何整合的?

    Mybatis源码解析(一) -- mybatis与Spring是如何整合的?   从大学开始接触mybatis到现在差不多快3年了吧,最近寻思着使用3年了,我却还不清楚其内部实现细节,比如: 它是如 ...

  2. Mybatis源码学习第六天(核心流程分析)之Executor分析

    今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...

  3. 【MyBatis源码解析】MyBatis一二级缓存

    MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...

  4. 浩哥解析MyBatis源码(五)——DataSource数据源模块之非池型数据源

    1 回顾 上一篇中我解说了数据源接口DataSource与数据源工厂接口DataSourceFactory,这二者是MyBatis数据源模块的基础,包括本文中的非池型非池型数据源(UnpooledDa ...

  5. mybatis源码解读(五)——sql语句的执行流程

    还是以第一篇博客中给出的例子,根据代码实例来入手分析. static { InputStream inputStream = MybatisTest.class.getClassLoader().ge ...

  6. Tomcat8源码笔记(五)组件Container分析

    Tomcat8源码笔记(四)Server和Service初始化 介绍过Tomcat中Service的初始化 最先初始化就是Container,而Container初始化过程是咋样的? 说到Contai ...

  7. mybatis源码学习--spring+mybatis注解方式为什么mybatis的dao接口不需要实现类

    相信大家在刚开始学习mybatis注解方式,或者spring+mybatis注解方式的时候,一定会有一个疑问,为什么mybatis的dao接口只需要一个接口,不需要实现类,就可以正常使用,笔者最开始的 ...

  8. 【mybatis源码学习】mybatis和spring框架整合,我们依赖的mapper的接口真相

    转载至:https://www.cnblogs.com/jpfss/p/7799806.html Mybatis MapperScannerConfigurer 自动扫描 将Mapper接口生成代理注 ...

  9. mybatis源码学习(二)--mybatis+spring源码学习

    这篇笔记主要来就,mybatis是如何利用spring的扩展点来实现和spring的整合 1.mybatis和spring整合之后,我们就不需要使用sqlSession.selectOne()这种方式 ...

  10. 【mybatis源码学习】mybatis的插件功能

    一.mybatis的插件功能可拦截的目标 org.apache.ibatis.executor.parameter.ParameterHandler org.apache.ibatis.executo ...

随机推荐

  1. 从线上日志统计接口访问量QPS

    这一阵子在面试,连续遇到好几家(大小厂都有)问我的项目线上qps的情况了,说实话,我作为一个大头兵,本来没关注过这个数据,只能含混地给个"大概.也许"的回答. 回来之后,我决定对业 ...

  2. CodeForce-801C Voltage Keepsake(二分)

    题目大意:有n个装备,每个设备耗能为每单位时间耗能ai,初始能量为bi;你有一个充电宝,每单位时间可以冲p能量,你可以在任意时间任意拔冲. 如果可以所有设备都可以一直工作下去,输出-1:否则,输出所有 ...

  3. CodeForce-797C Minimal string(贪心模拟)

    Minimal string CodeForces - 797C Petya 收到一个长度不超过 105 的字符串 s.他拿了两个额外的空字符串 t 和 u 并决定玩一个游戏.这个游戏有两种合法操作: ...

  4. Jmeter系列(8)- 参数化:JSON提取器、全局参数化、CSV文件导入

    JSON提取器 同一个响应结果需要提取多个参数进行参数化,下方输入项用分号(;)进行间隔.请求引用时${变量名} 全局参数化 此处全局参数化用的是用户自定义的变量 CSV文件导入

  5. Django边学边记--状态保持(cookie和session)

    Cookie 概念: Cookie,也叫Cookies,指某些网站为了辨别用户身份.进行session跟踪而储存在用户本地终端上的数据(通常经过加密),好比会员卡或餐票. 特点: Cookie是由服务 ...

  6. Jmeter监控技术实战

    性能测试中监控的意义 为性能分析提供依据 监控方案 serverAgent jmeter的插件,监控颗粒度不高,界面简陋 服务器中启动 jmeter中添加插件 Nmon Grafana 优秀监控方案所 ...

  7. 【原创】linux mint 17.3 kvm 安装windows7虚拟机

    一.安装windows7虚拟机 linux mint 17.3是一个不错的桌面发行版本,我下载了 linux mint 17.3 for xfce 桌面版本,运行速度没得说,而且安装设置都挺简单,非常 ...

  8. P5956-[POI2017]Podzielno【数学】

    正题 题目链接:https://www.luogu.com.cn/problem/P5956 题目大意 \(B\)进制下,给出序列\(a\),\(a_i\)表示数字\(i\)有多少个.求一个最大的\( ...

  9. Dapr + .NET Core实战(十一)单机Dapr集群负载均衡

    如何单机部署Dapr集群 第十篇讲过了K8S集群下如何使用Dapr运行程序,但是很多人一直在问如何单机下进行Dapr的负载,这节课我们来聊聊如何单机进行Dapr的负载. 首先要说的是单机下,通过 da ...

  10. java 从零开始手写 RPC (04) -序列化

    序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...