HuKai's Blog HuKai's Blog
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)

HuKai

梦想成为全栈的保安
首页
  • Java核心技术

    • Java基础
    • Java并发编程
    • JVM
    • Java新特性
  • Spring生态

    • Spring5
    • SpringMVC
    • SpringBoot
  • 开源框架

    • MyBatis
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 设计模式
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
  • 页面样式

    • HTML
    • CSS
  • JavaScript

    • JavaScript基础
    • ECMAScript6教程
    • TypeScript
  • 前端框架

    • Vue
    • Webpack
  • NIO
  • Netty
  • RabbitMQ
  • 技术文档

    • GitHub技巧
    • 博客搭建
    • 技术笔记
  • 优质文章

    • 小技巧
    • 解决方案
GitHub (opens new window)
  • Java基础

  • Java并发编程

  • JVM

  • Java新特性

  • Spring5

  • SpringMVC

  • SpringBoot

  • MyBatis

    • 应用篇

      • MyBatis快速入门
      • MyBatis配置文件深入
      • MyBatis复杂映射开发
      • MyBatis缓存机制
        • 1. MyBatis缓存概述
        • 2. 一级缓存
        • 3. 二级缓存
      • MyBatis插件机制
    • 源码篇

    • 扩展篇

  • Java
  • MyBatis
  • 应用篇
HuKai
2022-03-20
目录

MyBatis缓存机制

# 1. MyBatis缓存概述

缓存是一般的ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。两个缓存的关系如下图:

下面我们来详细介绍mybatis中的一级缓存和二级缓存。

# 2. 一级缓存

一级缓存又叫本地缓存(local cache),每当一个新 sqlSession 被创建,MyBatis就会创建一个与之相关联的本地缓存。任何在 sqlSession 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

# 2.1 验证一级缓存

在一个sqlSession中,对Emp表根据主键进行两次查询,查看他们发出sql语句的情况:

@Test
public void testLocalCache(){
    // 第一次查询
    Emp emp1 = empMapper.findById(7900);
    System.out.println(emp1);
    // 第二次查询
    Emp emp2 = empMapper.findById(7900);
    System.out.println(emp2);
}
1
2
3
4
5
6
7
8
9

可以看出,在第二次查询时直接打印了结果,没有发出查询的sql语句。

同样在一个sqlSession中,对Emp表根据主键进行两次查询,不同的是中间穿插一次更新操作:

@Test
public void testLocalCache(){
    // 第一次查询
    Emp emp1 = empMapper.findById(7900);
    System.out.println(emp1);
    // 更新操作
    Emp emp = new Emp();
    emp.setEmpno(7788);
    emp.setEname("SCOTT");
    empMapper.update(emp);
    sqlSession.commit();
    System.out.println("update complete...");
    // 第二次查询
    Emp emp2 = empMapper.findById(7900);
    System.out.println(emp2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行完更新操作后,第二次查询又发出了sql查询语句。

总结:

  1. 第一次发起查询员工编码为7900的员工信息,先去找本地缓存中是否有员工编码为7900的员工信息,如果没有,从数据库查询员工信息。得到员工信息,将员工信息存储到一级缓存中。
  2. 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级
    缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读
    。
  3. 第二次发起查询员工编码为7900的员工信息,先去找缓存中是否有员工编码为7900的员工信息,缓存中有,直接从缓存中获取员工信息,如果没有,则从数据库查询。

# 2.2 一级缓存原理探究与源码分析

想要了解一级缓存的工作流程,我们可以从源码入手,通过分析源码来了解。怎么分析一级缓存的源码呢?

提到一级缓存就绕不开SqlSession,所以索性我们就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法。

从方法名上看好像只有clearCache()和缓存沾点关系,那么就直接从这个方法入手。我们找到SqlSession的默认实现类DefaultSqlSession:

private final Executor executor;

@Override
public void clearCache() {
    executor.clearLocalCache();
}
1
2
3
4
5
6

调用了Executor接口中的clearLocalCache()方法,有过自定义持久层框架的基础我们知道这个类的作用是封装JDBC操作,我们找到它的默认实现类BaseExecutor继续跟进:

protected PerpetualCache localCache;

@Override
public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
}
1
2
3
4
5
6
7
8
9

调用了PerpetualCache类中的clear()方法,继续跟进:

private Map<Object, Object> cache = new HashMap();

public void clear() {
    this.cache.clear();
}
1
2
3
4
5

点进去发现,cache其实就是private Map<Object, Object> cache = new HashMap();也就是一个Map,所以说cache.clear()其实就是map.clear(),也就是说,一级缓存其实就是本地存放的一个map对象,每一个SqISession都会存放一个map对象的引用,那么这个cache是何时创建的呢?

你觉得最有可能创建缓存的地方是哪里呢?我觉得是Executor,为什么这么认为?因为Executor是执行器,用来执行SQL请求,而且清除缓存的方法也在Executor中执行,所以很可能缓存的创建也很有可能在Executor中。

Executor中有个createCacheKey()方法,看方法名我们知道这是一个创建缓存的Key值方法,具体代码如下:

public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    ...
    CacheKey cacheKey = new CacheKey();
    //MappedStatement 的 id
    // id就是Sql语句的所在位置包名+类名+ SQL名称
    cacheKey.update(ms.getId());
    // offset 就是 0
    cacheKey.update(rowBounds.getOffset());
    // limit 就是 Integer.MAXVALUE
    cacheKey.update(rowBounds.getLimit());
    // 具体的SQL语句
    cacheKey.update(boundSql.getSql());
    ...
    // 后面是update 了 sql中带的参数
    cacheKey.update(value);
    ...
    if (this.configuration.getEnvironment() != null) {
        // 环境ID
        cacheKey.update(this.configuration.getEnvironment().getId());
    }

    return cacheKey;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

创建缓存key会经过一系列的update方法,udate方法由一个CacheKey这个对象来执行的,这个
update方法最终由updateList的list来把五个值存进去。

通过debug发现,最终生成的key值为:

-954155565:-259018827:com.hukai.demo.mapper.EmpMapper.findById:0:2147483647:select * from emp where empno = ?:7900:development

创建缓存的Key值这个方法是在哪调用呢?通过搜索发现在BaseExecutor中的query()方法中调用了:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = this.createCacheKey(ms, parameter, rowBounds, boundSql);
    return this.query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ...
    List list;
    try {
        ++this.queryStack;
        list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
        if (list != null) {
            this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        --this.queryStack;
    }
    ...
}

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List list;
        try {
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }

        this.localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }

        return list;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。 localcache对
象的put方法最终交给Map进行存放。

# 2.3 一级缓存总结

通过源码分析我们了解到,MyBatis 跟缓存相关的类都在cache 包里面,其中有一个Cache 接口,只有一个默认的实现类PerpetualCache,它是用HashMap 实现的。

一级缓存的工作流程:

  1. 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。
  2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中。
  3. 如果命中,则直接将缓存结果返回。
  4. 如果没命中,去数据库中查询数据,得到查询结果。将key和查询到的结果分别作为key,value对存储到Cache中,然后将查询结果返回。

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:

  • session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中
  • statement 级别的缓存,避坑: 为了避免这个问题,可以将一级缓存的级别设为 statement 级别的,这样每次查询结束都会清掉一级缓存

# 3. 二级缓存

二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。

如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。

# 3.1 开启二级缓存

和一级缓存默认开启不一样,二级缓存需要我们手动开启。
首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:

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

然后在Mapper.xml 中配置标签

<!--开启二级缓存-->
<cache></cache>
1
2

如果是注解的方式开发,可以在相应的Mapper类中添加@CacheNamespace(blocking = true)来开启二级缓存:

@CacheNamespace(blocking = true)
public interface EmpMapper {
    ...
}
1
2
3
4

开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操 作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取 这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口。

# 3.2 验证二级缓存

# 3.2.1 验证二级缓存sqlSession无关性

开启二级缓存后,我们来验证二级缓存和sqlSession无关,上测试代码:

public void testSecondLevelCache(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    EmpMapper empMapper1 = sqlSession1.getMapper(EmpMapper.class);
    EmpMapper empMapper2 = sqlSession2.getMapper(EmpMapper.class);

    //第一次查询,发出sql语句,并将查询的结果放入缓存中
    Emp emp1 = empMapper1.findById(7788);
    System.out.println(emp1);
    sqlSession1.close();

    //第二次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句
    Emp emp2 = empMapper2.findById(7788);
    System.out.println(emp2);
    sqlSession2.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

可以看出,第二次查询即使sqlSession1已经关闭了,这次查询依然不发出sql语句,证明二级缓存已经成功开启了。

# 3.2.2 验证更新对二级缓存影响

与一级缓存一样,更新操作很可能对二级缓存造成影响,下面用三个 SqlSession来进行模拟,第一个 SqlSession 只是单纯的提交,第二个 SqlSession 用于检验二级缓存所产生的影响,第三个 SqlSession 用于执行更新操作,测试如下:

public void testSecondLevelCache(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();
    SqlSession sqlSession4 = sqlSessionFactory.openSession();

    EmpMapper empMapper1 = sqlSession1.getMapper(EmpMapper.class);
    EmpMapper empMapper2 = sqlSession2.getMapper(EmpMapper.class);
    EmpMapper empMapper3 = sqlSession3.getMapper(EmpMapper.class);
    EmpMapper empMapper4 = sqlSession4.getMapper(EmpMapper.class);

    //第一次查询,发出sql语句,并将查询的结果放入缓存中
    Emp emp1 = empMapper1.findById(7788);
    System.out.println(emp1);
    sqlSession1.close();

    //第二次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句
    Emp emp2 = empMapper2.findById(7788);
    System.out.println(emp2);
    sqlSession2.close();

    //第三个sqlSession执行更新操作
    Emp emp = new Emp();
    emp.setEmpno(7369);
    emp.setEname("SMITH");
    empMapper3.update(emp);
    sqlSession3.commit();
    sqlSession3.close();

    // 第三次查询
    Emp emp3 = empMapper4.findById(7788);
    System.out.println(emp3);
    sqlSession4.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35


# 3.2.3 useCache和flushCache

mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存

<select id="selectBlog" resultMap="BaseResultMap" useCache="false">
1

如果某些查询方法对数据的实时性要求很高,不需要二级缓存,可以设置为false。

在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。

设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。

一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可

对于注解开发方式,我们可以使用@Options注解来显式
关闭二级缓存:

@Options(useCache = false,flushCache = true)

# 3.3 二级缓存原理

作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession共享。而一级缓存是在SqlSession内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢?要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。

实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor对象的时候会对Executor进行装饰。CachingExecutor对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor实现类,比如SimpleExecutor来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

分析CachingExecutor的源码,不难看出二级缓存和一级缓存工作机制大体是类似的。在更新时,会刷新缓存:

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

# 3.4 二级缓存整合redis

上面我们介绍了mybatis自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。为了实现这一需求,我们使用第三方缓存框架,将缓存都放在这个第三方框架中,然后无论有多少台服务器,我们都能从缓存中获取数据。

这里我们介绍mybatis与redis的整合。

刚刚提到过,mybatis提供了一个eache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。

redis分布式缓存就可以,mybatis提供了一个针对cache接口的redis实现类,该类存在mybatis-redis包中。

pom文件:

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-redis</artifactId>
    <version>1.0.0-beta2</version>
</dependency>
1
2
3
4
5

Mapper.xml配置文件:

<cache type="org.mybatis.caches.redis.RedisCache" />
1

redis.properties:

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=
1
2
3
4
5
编辑 (opens new window)
#MyBatis
上次更新: 2022/03/20, 16:39:15
MyBatis复杂映射开发
MyBatis插件机制

← MyBatis复杂映射开发 MyBatis插件机制→

最近更新
01
MyBatisPlus
03-20
02
MyBatis源码剖析-延迟加载
03-20
03
MyBatis源码剖析-二级缓存
03-20
更多文章>
Theme by Vdoing | Copyright © 2021-2022 HuKai | 赣ICP备17016768号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式