Mybatis的缓存

一 Mybatis缓存体系图

Mybatis缓存的基础实现是perpetualCache,但是mybatis利用装饰者模式对基础cache提供了许多的增强功能,比如上图,BlockingCache里利用concurrentHashMap封装了一些可重入锁Reetranlock实现了并发问题的解决

/**
 * Simple blocking decorator
 *
 * Simple and inefficient version of EhCache's BlockingCache decorator.
 * It sets a lock over a cache key when the element is not found in cache.
 * This way, other threads will wait until this element is filled instead of hitting the database.
 *
 * @author Eduardo Macarron
 *
 */
*当在缓存中找不到元素时,它设置对缓存键的锁定。
*这样,其他线程将等待此元素被填充,而不是命中数据库。
*锁acquire和release详情请看源码.

一 . 一级缓存的工作位置和维护对象

一级缓存的作用域是sqlsession,而且根据下图,查看一下sqlsession的实现类可以发现configuration是我们加载xml文件的全局变量,肯定不是sqlsess的工作位置,那么只有executor了

而且作为Executor 我们这里有simple reuser batch Executor三种,他们都继承了BaseExecutor
看下baseExecutor果然发现里面有个perpetualCache作为一级缓存,所以我们也称一级缓存为本地缓存,因为我们每连接一次数据库就会创建一个会话,每创建一个会员就会创建一个执行器,每个执行器里就有一个一级缓存.

我们用户去查询数据时候会先到一级缓存中尝试获取数据,如果有数据会直接返回不在查库,如果没找到数据会先返回应用再写入缓存,如下.
测试一级缓存

1、在同一个 session 中共享

BlogMapper mapper = session.getMapper(BlogMapper.class); 
System.out.println(mapper.selectBlog(1)); 
System.out.println(mapper.selectBlog(1));

2、不同 session 不能共享

SqlSession session1 = sqlSessionFactory.openSession(); 
BlogMapper mapper1 = session1.getMapper(BlogMapper.class); 
System.out.println(mapper.selectBlog(1));

PS:一级缓存在 BaseExecutor 的 query()——queryFromDatabase()中存入。在 queryFromDatabase()之前会 get()。


3、同一个会话中,update(包括 delete)增删改会导致一级缓存被清空

测试代码.
mapper.updateByPrimaryKey(blog); 
session.commit();
System.out.println(mapper.selectBlogById(1));

一级缓存是在 BaseExecutor 中的 update()方法中调用 clearLocalCache()清空的 (无条件),query 中会判断。
为什么呢?
如下图所示,我们的mapper元素属性中有个flushCache,在增删改里他是开启的true,在查询select里它是关闭的.这个会刷新该会话的缓存


4、其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)

//会话 2 更新了数据,会话 2 的一级缓存更新
BlogMapper mapper2 = session2.getMapper(BlogMapper.class); 
mapper2.updateByPrimaryKey(blog); 
session2.commit();
// 会话 1 读取到脏数据,因为一级缓存不能跨会话共享 System.out.println(mapper1.selectBlog(1));

一级缓存的不足

使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据 可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要 解决这个问题,就要用到二级缓存。

如何关闭一级缓存呢?

方法:在配置文件的setings中更改localCacheScope属性值为STATEMENT

原理:
原理解释来自mybatis3官方文档

二 . 二级缓存

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

如果开启了二级缓存,二级缓存应该是工作在一级缓存之前,还是 在一级缓存之后呢?二级缓存是在哪里维护的呢?

二级缓存应该是工作在一级缓存之前.(如果二级缓存中有就会直接返回,如果二级缓存没有,会去一级缓存中查,一级缓存也没有会去datasource中查,并依次存储,详情可以看后面有个流程图)

要跨会话共享的话,SqlSession 本 身和它里面的 BaseExecutor 已经满足不了需求了,那我们应该在 BaseExecutor 之外创建一个对象。实际上我们的二级缓存还是利用的装饰者模式做了一个包装类cachingExecutor对一级缓存做了增强,如果启用了 二级缓存,MyBatis 在创建 Executor 对象的时候会对 Executor 进行装饰。

TransactionalCacheManager

CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接 返回,如果没有委派交给真正的查询器 Executor 实现类,比如 SimpleExecutor 来执行 查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户.

开启二级缓存的方法

第一步:在 mybatis-config.xml 中配置了(可以不配置,默认是 true):

 

只要没有显式地设置cacheEnabled=false,都会用 CachingExecutor 装饰基本的 执行器
第二步:在 Mapper.xml 中配置标签:

 

eviction="LRU"  
flushInterval="120000"  
readOnly="false"/> 

第三步:确保要使用缓存的select语句没有关闭缓存
我们mapper.xml文件里面的元素有个属性 usecache="",默认=ture,如果你添加了usecache="false",那么他就不会走缓存了
cache 属性详解:


Mapper.xml 配置了之后,select()会被缓存。update()、delete()、insert() 会刷新缓存。


我们配置二级缓存后,内部会通过一个CacheingExecutor对原来的Executor进行一个装饰,这样如果我们二级缓存中有数据就会直接返回,如果


二级缓存工作流程以及原理

思考:如果 cacheEnabled=true,Mapper.xml 没有配置标签,还有二级缓存吗? 还会出现 CachingExecutor 包装对象吗?
会.

只要 cacheEnabled=true 基本执行器就会被装饰。有没有配置,决定了在 启动的时候会不会创建这个 mapper 的 Cache 对象,最终会影响到 CachingExecutor query 方法里面的判断:
if (cache != null) { }

思考:如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?

我们可以在单个 Statement ID 上显式关闭二级缓存(默认是 true)