缓存简介

一般我们在系统中使用缓存技术是为了提升数据查询的效率。当我们从数据库中查询到一批数据后将其放入到混存中(简单理解就是一块内存区域),下次再查询相同数据的时候就直接从缓存中获取数据就行了。

这样少了一步和数据库的交互,可以提升查询的效率。

但是一个硬币都具有两面性,缓存在带来性能提升的同时也“悄悄”引入了很多问题,比如缓存同步、缓存失效、缓存雪崩等等。当然这些问题不是本文讨论的重点。

本文主要讨论MyBatis缓存这个比较鸡肋的功能。虽然说MyBatis的缓存功能比较鸡肋,但是为了全面了解MyBatis这个框架,学习下缓存这个功能还是挺有必要的。MyBatis的缓存分为一级缓存和二级缓存,

下面就分别来介绍下这两个特性。

一级缓存

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。

什么是MyBatis一级缓存

一级缓存是 SqlSession级别 的缓存。在操作数据库时需要构造 sqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互相不影响的。

在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。

怎么开启一级缓存

MyBatis中一级缓存默认是开启的,不需要我们做额外的操作。

如果你需要关闭一级缓存的话,可以在Mapper映射文件中将flushCache属性设置为true,这种做法只会针对单个SQL操作生效

<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap" flushCache="true">
select
<include refid="Base_Column_List" />
from cbondissuer
where OBJECT_ID = #{objectId,jdbcType=VARCHAR}
</select>
> 还有一种做法是在MyBatis的主配置文件中,关闭所有的一级缓存
> ```xml
> 默认是SESSION,也就是开启一级缓存
> <setting name="localCacheScope" value="STATEMENT"/>
> ``` 下面我们来写代码验证下MyBatis的一级缓存。 ```java
String id = "123";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class); //同一个Mapper,同样的SQL查了两次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一个sqlSession创建的Mapper,又查询了一次同样的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//不一样的sqlSession创建的Mapper查询了一次同样的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id); System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20)); sqlSession1.close();
sqlSession2.close();
System.out.println("end...");

上面进行了四次查询,如果你观察日志的话。会发现只进行了两个数据库查询。因为第二和第三次的查询都查询了一级缓存,查出的其实是缓存中的结果。所以输出的结果是

cbondissuer10 equals cbondissuer101 :true
cbondissuer10 equals cbondissuer11 :true
cbondissuer10 equals cbondissuer21 :false

哪些因素会使一级缓存失效

上面的一级缓存初探让我们感受到了 MyBatis 中一级缓存的存在,那么现在你或许就会有疑问了,那么什么时候缓存失效呢?

  • 通过同一个SqlSession执行更新操作时,这个更新操作不仅仅指代update操作,还指插入和删除操作;
  • 事务提交时会删除一级缓存;
  • 事务回滚时也会删除一级缓存;

一级缓存源码解析

其实MyBatis一级缓存的实质就是一个Executor的一个类似Map的属性,分析源码的方法就是看在哪些地方从这个Map中查询了缓存,又是在哪些清空了这些缓存。

1. 查询时使用缓存分析

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
protected Executor wrapper; protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
//这个localCache变量就是一级缓存变量
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
//..省略下面代码
}

全局搜索代码中哪些地方使用了这个变量,很容易找到BaseExecutor.query方法使用了这个缓存:

public abstract class BaseExecutor implements Executor {

// 省略其他代码
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()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
//先从缓存中查询结果,如果缓存中已经存在结果直接使用缓存的结果
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);
}
} 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;
}
//..省略下面代码
}

上面的代码展示了,BaseExecutor的query方法使用缓存的过程。需要注意的是查询缓存时是根据cacheKey进行查询的,我们可以将这个key简单的

理解为sql语句,不同的sql语句能查出不同的缓存。(注意sql语句中的参数不同也会被认为是不同的sql语句)。

2. 导致一级缓存失效的代码分析

查看BaseExecutor的代码,我们很容易发现是下面的方法清空了一级缓存。(不要问我是怎么发现这个代码的,看代码能力需要自己慢慢提升)

@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}

那么我们只要查看哪些地方调用了这个方法就知道哪些情况下会导致一级缓存失效了。跟踪下来,最后发现下面三处地方会使得一级缓存失效

BaseExecutor的update方法,使用MyBatis的接口进行增、删、改操作都会调用到这个方法,这个也印证了上面的说法。

@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的commit方法,事务提交会导致一级缓存失败。如果我们使用Spring的话,一般事务都是自动提交的,所以好像MyBatis的一级缓存一直没怎么被考虑过

@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}

BaseExecutor的rollback方法,事务回滚也会导致一级缓存失效。

@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}

一级缓存使用建议

平时使用MyBatis时都是和Spring结合使用的,在整个Spring容器中一般只有一个SqlSession实现类。而Spring一般都是主动提交事务的,所以说一级缓存经常失效。

还有就是我们也很少在一个事务范围内执行同一个SQL两遍,上面的这些原因导致我们在开发过程中很少注意到MyBatis一级缓存的存在。

不怎么用并不是说不用,作为一个合格的开发者需要对这些心知肚明,要清楚的知道MyBatis一级缓存的工作流程。

二级缓存

什么是MyBatis二级缓存

MyBatis 一级缓存最大的共享范围就是一个SqlSession内部,那么如果多个 SqlSession 需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用 CachingExecutor 装饰 Executor,

进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示:

当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个 共同的 cache(一个Mapper映射文件对应一个Cache),也就是二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

从上面的图可以看出,MyBatis的二级缓存实现可以有很多种,可以是MemCache、Ehcache等。也可以是Redis等,但是需要额外的Jar包。

怎么开启二级缓存

二级缓存默认是不开启的,需要手动开启二级缓存,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。开启二级缓存的条件也是比较简单,

step1:通过直接在 MyBatis 配置文件中通过

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

step2: 在 Mapper 的xml 配置文件中加入 标签

cache标签下面有下面几种可选项

  • eviction: 缓存回收策略,支持的策略有下面几种

    • LRU - 最近最少回收,移除最长时间不被使用的对象(默认是这个策略)
    • FIFO - 先进先出,按照缓存进入的顺序来移除它们
    • SOFT - 软引用,移除基于垃圾回收器状态和软引用规则的对象
    • WEAK - 弱引用,更积极的移除基于垃圾收集器和弱引用规则的对象
  • flushinterval:缓存刷新间隔,缓存多长时间刷新一次,默认不清空,设置一个毫秒值;

  • readOnly: 是否只读;true 只读 ,MyBatis 认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。MyBatis 为了加快获取数据,直接就会将数据在缓存中的引用交给用户。不安全,速度快。读写(默认):MyBatis 觉得数据可能会被修改

  • size : 缓存存放多少个元素

  • type: 指定自定义缓存的全类名(实现Cache 接口即可)

  • blocking:若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。

哪些因素会使二级缓存失效

从上面的介绍可以知道MyBatis的二级缓存主要是为了SqlSession之间共享缓存设计的。但是我们平时开发过程中都是结合Spring来进行MyBatis的开发。在Spring环境下一般也只有一个SqlSession实例,所以二级缓存使用到的机会不多。所以下面就简单描述下Mybatis的二级缓存。

还是以上面的列子为列

String id = "{0003CCCA-AEA9-4A1E-A3CC-06D884BA3906}";
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
//同一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper10 = sqlSession1.getMapper(CbondissuerMapper.class);
CbondissuerMapper cbondissuerMapper11 = sqlSession1.getMapper(CbondissuerMapper.class);
//另外一个sqlSession创建的Mapper
CbondissuerMapper cbondissuerMapper20 = sqlSession2.getMapper(CbondissuerMapper.class); //同一个Mapper,同样的SQL查了两次
Cbondissuer cbondissuer10 = cbondissuerMapper10.selectByPrimaryKey(id);
Cbondissuer cbondissuer101 = cbondissuerMapper10.selectByPrimaryKey(id);
//同一个sqlSession创建的Mapper,又查询了一次同样的SQL
Cbondissuer cbondissuer11 = cbondissuerMapper11.selectByPrimaryKey(id);
//这边需要提交事务才能让二级缓存生效
sqlSession1.commit();
//不一样的sqlSession创建的Mapper查询了一次同样的SQL
Cbondissuer cbondissuer20 = cbondissuerMapper20.selectByPrimaryKey(id); System.out.println("cbondissuer10 equals cbondissuer101 :"+(cbondissuer10==cbondissuer101));
System.out.println("cbondissuer10 equals cbondissuer11 :"+(cbondissuer10==cbondissuer11));
System.out.println("cbondissuer10 equals cbondissuer21 :"+(cbondissuer10==cbondissuer20));
  • 二级缓存是以namespace(Mapper)为单位的,不同namespace下的操作互不影响。
  • insert,update,delete操作会清空所在namespace下的全部缓存。
  • 多表操作一定不要使用二级缓存,因为多表操作进行更新操作,一定会产生脏数据。

二级缓存使用建议

个人觉得MyBatis的二级缓存实用性不是很大。一个原因就是Spring环境下,一本只有一个SqlSession,不存在sqlSession之间共享缓存;还有就是

MyBatis的缓存都不能做到分布式,所以对于MyBatis的二级缓存以了解为主。

简单总结

一级缓存

  • 一级缓存的本质是Executor的一个类似Map的属性;
  • 一级缓存默认开启,将flushCache设置成true或者将全局配置localCacheScope设置成Statement可以关闭一级缓存;
  • 在一级缓存开启的情况下,查询操作会先查询一级缓存,再查询数据库;
  • 增删改操作和事务提交回滚操作会导致一级缓存失效;
  • 由于Spring中事务是自动提交的,因此Spring下的MyBatis一级缓存经常失效。(但是并不表示不生效,除非你手动关闭一级缓存)
  • 不能实现分布式。

二级缓存

  • namesapce级别的缓存(Mapper级别或者叫做表级别的缓存),设计的主要目的是实现sqlSession之间的缓存共享;
  • 开启二级缓存后,查询的逻辑是二级缓存->已经缓存->数据库;
  • insert,update,delete操作会清空所在namespace下的全部缓存;
  • 多表查询一定不要使用二级缓存,因为多表操作进行更新操作,可能会产生脏数据。

总体来说,MyBatis的缓存功能比较鸡肋。想要使用缓存的话还是建议使用spring-cache等框架。

参考

MyBatis特性详解的更多相关文章

  1. C#中的 特性 详解(转载)

    本篇幅转载于:http://www.cnblogs.com/rohelm/archive/2012/04/19/2456088.html C#中特性详解 特性提供了功能强大的方法,用于将元数据或声明信 ...

  2. iOS开发——高级特性&Runtime运行时特性详解

    Runtime运行时特性详解 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的动态特性,使这门古老的语言焕发生机.主要内容如下: 引言 ...

  3. ES6,ES2105核心功能一览,js新特性详解

    ES6,ES2105核心功能一览,js新特性详解 过去几年 JavaScript 发生了很大的变化.ES6(ECMAScript 6.ES2105)是 JavaScript 语言的新标准,2015 年 ...

  4. 《Android群英传》读书笔记 (5) 第十一章 搭建云端服务器 + 第十二章 Android 5.X新特性详解 + 第十三章 Android实例提高

    第十一章 搭建云端服务器 该章主要介绍了移动后端服务的概念以及Bmob的使用,比较简单,所以略过不总结. 第十三章 Android实例提高 该章主要介绍了拼图游戏和2048的小项目实例,主要是代码,所 ...

  5. C#各个版本中的新增特性详解

    序言 自从2000年初期发布以来,c#编程语言不断的得到改进,使我们能够更加清晰的编写代码,也更加容易维护我们的代码,增强的功能已经从1.0搞到啦7.0甚至7.1,每一次改过都伴随着.NET Fram ...

  6. ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解

    ASP.NET Core Web服务器 Kestrel和Http.sys 特性详解 1.1. 名词解释 1.2. Kestrel基本工作原理 1.2.1. Kestrel的基本架构 1.2.2. Ke ...

  7. Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验

    Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高 ...

  8. 单元测试系列之十一:Jmockit之mock特性详解

    本文是Jmockit学习过程中,根据官网所列的工具特性进行解读. 1.调用次数约束(Invocation count constraints) 可以通过调用计数约束来指定预期和/或允许匹配给定期望的调 ...

  9. Java9 新特性 详解

    作者:木九天   <   Java9 新特性 详解  > Java9 新特性 详解 摘要: 1.目录结构 2.repl工具 jShell命令 3.模块化 4.多版本兼容jar包 5.接口方 ...

随机推荐

  1. 201771010113 李婷华 《面向对象程序设计(Java)》第十七周总结

    一.理论知识部分 Java 的线程调度采用优先级策略:优先级高的先执行,优先级低的后执行:多线程系统会自动为每个线程分配一个优先级,缺省时,继承其父类的优先级: 任务紧急的线程,其优先级较高: 同优先 ...

  2. Apache Hudi又双叕被国内顶级云服务提供商集成了!

    是的,最近国内云服务提供商腾讯云在其EMR-V2.2.0版本中优先集成了Hudi 0.5.1版本作为其云上的数据湖解决方案对外提供服务 Apache Hudi 在 HDFS 的数据集上提供了插入更新和 ...

  3. VS Code的安装与配置

    VS Code的安装与配置 Visual Studio Code(VS Code)是微软旗下的一个开源文本编辑器,支持Windows.macOS.Linux操作系统.数量众多.种类多样的插件极大提高了 ...

  4. 基于情感词典的python情感分析

    近期老师给我们安排了一个大作业,要求根据情感词典对微博语料进行情感分析.于是在网上狂找资料,看相关书籍,终于搞出了这个任务.现在做做笔记,总结一下本次的任务,同时也给遇到有同样需求的人,提供一点帮助. ...

  5. JAVA知识总结(三):继承和访问修饰符

    今天乘着还有一些时间,把上次拖欠的面向对象编程三大特性中遗留的继承和多态给简单说明一下.这一部分还是非常重要的,需要仔细思考. 继承 继承:它是一种类与类之间的关系,通过使用已存在的类作为基础来建立新 ...

  6. Spark SQL源码解析(四)Optimization和Physical Planning阶段解析

    Spark SQL原理解析前言: Spark SQL源码剖析(一)SQL解析框架Catalyst流程概述 Spark SQL源码解析(二)Antlr4解析Sql并生成树 Spark SQL源码解析(三 ...

  7. 配置centos7 java环境

    一.环境 centos7 jdk-8u231-linux-x64.tar.gz 二.安装jdk 使用ftp或者 WinScp软件把下载在win10电脑上的jdk安装包上传到linux 解压到/opt/ ...

  8. C/C++数组和指针详解

    /****************************************************************/ /*            学习是合作和分享式的! /* Auth ...

  9. JS中各种变量类型在条件判断为false的情况

    var a = undefined; ->false var a = 0; ->false var a = 0.0; ->false var a = NaN; ->false ...

  10. throttle和debounce

    遇到的问题 在开发过程中会遇到频率很高的事件或者连续的事件,如果不进行性能的优化,就可能会出现页面卡顿的现象,比如: 鼠标事件:mousemove(拖曳)/mouseover(划过)/mouseWhe ...