缓存简介

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

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

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

本文主要讨论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. 数据预处理 —— padding数据

    1. 论Conv2d()里的padding和Conv2d()前padding的区别及重要性.   小生建议,尽量少用Conv2d()里的填充方式,换成自定义填充方式(强烈建议).   小生为何这样建议 ...

  2. OpenWrt R2020.3.19 反追踪 抗污染 加速 PSW 无缝集成 UnPnP NAS

    固件说明 基于Lede OpenWrt R2020.3.19版本Lienol Feed及若干自行维护的软件包 结合家庭x86软路由场景需要定制 按照家庭应用场景对固件及软件进行测试,通过后发布 设计目 ...

  3. 【Scala】什么是隐式转换?它又能用来干嘛?该怎么用

    文章目录 定义 隐式参数 隐式转换 隐式值:给方法提供参数 隐式视图 将Int和Double类型转换为String 狗狗学技能(使用别的类中的方法) 使用规则 定义 隐式参数 隐式参数指在函数或者方法 ...

  4. 移动端网站开发要点-meta设置

    <!DOCTYPE html> <!-- 使用 HTML5 doctype,不区分大小写 --> <html lang="zh-cmn-Hans"&g ...

  5. HTML学习——day1

    HTML是一种用于创建网页的标准标记语 注意:对于中文网页需要使用<meta charset=''utf-8''>声明编码,否则会出现乱码. HTML标签 <标签>内容< ...

  6. python实现简易词频统计-源码

    需求:给瓦尔登湖文章统计单词出现的频率 思路:首先读取文件并以空格分割得到列表,然后利用for循环遍历列表中的元素并把去掉列表元素中的符号,第三步去掉相同的元素,将列表转换为一个字典,最后按照键值对升 ...

  7. 「雕爷学编程」Arduino动手做(13)——触摸开关模块

    37款传感器和模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器与模块,依照实践出真知(动手试试)的理念,以学习和交流为目的,这里准备 ...

  8. Java Mail 发送带有附件的邮件

    1.小编用的是163邮箱发送邮件,所以要先登录163邮箱开启POP3/SMTP/IMAP服务方法: 2.下载所需的java-mail 包 https://maven.java.net/content/ ...

  9. eclipse 整合mybatis的过程

    一.下载mybatis和数据库驱动的jar,我这里用到的数据库是pgAdmin III ,所以我下载的jar包分别为mybatis-3.0.2 jar和po's'tgresql-42.2.1.jar, ...

  10. Django操作session实例

    session项目文件: templates模板: login.html {% load static %} <!DOCTYPE html> <html lang="en& ...