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);
}
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行完更新操作后,第二次查询又发出了sql查询语句。
总结:
- 第一次发起查询员工编码为7900的员工信息,先去找本地缓存中是否有员工编码为7900的员工信息,如果没有,从数据库查询员工信息。得到员工信息,将员工信息存储到一级缓存中。
- 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级
缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。 - 第二次发起查询员工编码为7900的员工信息,先去找缓存中是否有员工编码为7900的员工信息,缓存中有,直接从缓存中获取员工信息,如果没有,则从数据库查询。
# 2.2 一级缓存原理探究与源码分析
想要了解一级缓存的工作流程,我们可以从源码入手,通过分析源码来了解。怎么分析一级缓存的源码呢?
提到一级缓存就绕不开SqlSession,所以索性我们就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法。

从方法名上看好像只有clearCache()和缓存沾点关系,那么就直接从这个方法入手。我们找到SqlSession的默认实现类DefaultSqlSession:
private final Executor executor;
@Override
public void clearCache() {
executor.clearLocalCache();
}
2
3
4
5
6
调用了Executor接口中的clearLocalCache()方法,有过自定义持久层框架的基础我们知道这个类的作用是封装JDBC操作,我们找到它的默认实现类BaseExecutor继续跟进:
protected PerpetualCache localCache;
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
2
3
4
5
6
7
8
9
调用了PerpetualCache类中的clear()方法,继续跟进:
private Map<Object, Object> cache = new HashMap();
public void clear() {
this.cache.clear();
}
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;
}
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;
}
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 实现的。

一级缓存的工作流程:
- 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果。
- 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中。
- 如果命中,则直接将缓存结果返回。
- 如果没命中,去数据库中查询数据,得到查询结果。将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>
2
3
然后在Mapper.xml 中配置标签
<!--开启二级缓存-->
<cache></cache>
2
如果是注解的方式开发,可以在相应的Mapper类中添加@CacheNamespace(blocking = true)来开启二级缓存:
@CacheNamespace(blocking = true)
public interface EmpMapper {
...
}
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();
}
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();
}
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">
如果某些查询方法对数据的实时性要求很高,不需要二级缓存,可以设置为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);
}
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>
2
3
4
5
Mapper.xml配置文件:
<cache type="org.mybatis.caches.redis.RedisCache" />
redis.properties:
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=
2
3
4
5