MyBatis源码分析(五):MyBatis Cache分析
一、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分析的更多相关文章
- Mybatis源码解析(一) —— mybatis与Spring是如何整合的?
Mybatis源码解析(一) -- mybatis与Spring是如何整合的? 从大学开始接触mybatis到现在差不多快3年了吧,最近寻思着使用3年了,我却还不清楚其内部实现细节,比如: 它是如 ...
- Mybatis源码学习第六天(核心流程分析)之Executor分析
今Executor这个类,Mybatis虽然表面是SqlSession做的增删改查,其实底层统一调用的是Executor这个接口 在这里贴一下Mybatis查询体系结构图 Executor组件分析 E ...
- 【MyBatis源码解析】MyBatis一二级缓存
MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相 ...
- 浩哥解析MyBatis源码(五)——DataSource数据源模块之非池型数据源
1 回顾 上一篇中我解说了数据源接口DataSource与数据源工厂接口DataSourceFactory,这二者是MyBatis数据源模块的基础,包括本文中的非池型非池型数据源(UnpooledDa ...
- mybatis源码解读(五)——sql语句的执行流程
还是以第一篇博客中给出的例子,根据代码实例来入手分析. static { InputStream inputStream = MybatisTest.class.getClassLoader().ge ...
- Tomcat8源码笔记(五)组件Container分析
Tomcat8源码笔记(四)Server和Service初始化 介绍过Tomcat中Service的初始化 最先初始化就是Container,而Container初始化过程是咋样的? 说到Contai ...
- mybatis源码学习--spring+mybatis注解方式为什么mybatis的dao接口不需要实现类
相信大家在刚开始学习mybatis注解方式,或者spring+mybatis注解方式的时候,一定会有一个疑问,为什么mybatis的dao接口只需要一个接口,不需要实现类,就可以正常使用,笔者最开始的 ...
- 【mybatis源码学习】mybatis和spring框架整合,我们依赖的mapper的接口真相
转载至:https://www.cnblogs.com/jpfss/p/7799806.html Mybatis MapperScannerConfigurer 自动扫描 将Mapper接口生成代理注 ...
- mybatis源码学习(二)--mybatis+spring源码学习
这篇笔记主要来就,mybatis是如何利用spring的扩展点来实现和spring的整合 1.mybatis和spring整合之后,我们就不需要使用sqlSession.selectOne()这种方式 ...
- 【mybatis源码学习】mybatis的插件功能
一.mybatis的插件功能可拦截的目标 org.apache.ibatis.executor.parameter.ParameterHandler org.apache.ibatis.executo ...
随机推荐
- TypeError: exchange_declare() got an unexpected keyword argument 'type'
在设置消息广播时:以下代码会报错channel.exchange_declare(exchange='direct_logs', type='direct')TypeError: exchange_d ...
- K8s 开始
Kubernetes 是用于自动部署,扩展和管理容器化应用程序的开源系统.本文将介绍如何快速开始 K8s 的使用. 了解 K8s Kubernetes / Overview 搭建 K8s 本地开发测试 ...
- TypeScript与面向对象
目录 1.引 2.类(class) 3.构造函数和this 4.继承 5.super 6.抽象类 7.接口 8.属性的封装 9.泛型 1.引 简而言之就是程序之中所有的操作都需要通过对象来完成.一切操 ...
- PHP的bz2压缩扩展工具
在日常的开发和电脑使用中,我们经常会接触到压缩和解压的一些工具,PHP 也为我们准备了很多相关的操作扩展包,都有直接可用的函数能够方便的操作一些压缩解压功能.今天,我们先学习一个比较简单但不太常用的压 ...
- final关键字在PHP中的使用
final关键字的使用非常简单,在PHP中的最主要作用是定义不可重写的方法.什么叫不可重写的方法呢?就是子类继承后也不能重新再定义这个同名的方法. class A { final function t ...
- Java面向对象系列(1)- 什么是面向对象
面向过程 & 面向对象 面向过程思想 步骤清晰清楚,第一步做什么,第二步做什么-- 面对过程适合处理一些较为简单的问题 面向对象思想 物以类聚,分类的思维模式,思考问题首先会解决问题需要哪些分 ...
- Shell系列(38)- 数组操作→取值、遍历、替换、删除
引言 在Linux平台上工作,我们经常需要使用shell来编写一些有用.有意义的脚本程序.有时,会经常使用shell数组.那么,shell中的数组是怎么表现的呢,又是怎么定义的呢?接下来逐一的进行讲解 ...
- 使用Jmeter执行接口自动化测试-如何初始化清空旧数据
需求分析: 每次执行完自动化测试,我们不会执行删除接口把数据删除,而需要留着手工测试,此时会导致下次执行测试有旧数据 我们手工可能也会新增数据,导致下次执行自动化测试有旧数据 下面介绍两种清空数据的方 ...
- lua文件修改为二进制文件
注意:lua编译跟luajit编译的二进制文件是不兼容,不能运行的 如果是使用luajit,请直接使用luajit直接编译二进制 第一种:luajit编译(以openresty为例,跟luac是相反的 ...
- bzoj#2407-探险【最短路,二进制分组】
正题 题目链接:https://darkbzoj.tk/problem/2407 题目大意 \(n\)个点的一张无向图(但是正反权值不同),求一个从\(1\)出发回到\(1\)且不经过重复边的最短路径 ...