MyBatis缓存详解

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

MyBatis跟缓存相关的类都在cache包里面,其中有一个Cache接口,且只有一个实现类PerpetualCache,它是用HashMap实现缓存功能的。

除此之外,还有很多的装饰器,通过这些装饰器可以实现很多额外的功能:回收策略、日志记录、定时刷新等等。


在这里插入图片描述

但是无论怎么装饰,经过多少层装饰,最终使用的还是基本的实现类PerpetualCache。
所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。

缓存实现类 描述 作用 装饰条件
基本缓存 缓存基本实现类 默认是PerpetualCache,也可以自定义比如RedisCache等,具备基本功能的缓存类
LruCache LRU策略的缓存 当缓存达到上限时,删除最近最少使用的缓存 eviction="LRU" (默认)
FifoCache FIFO策略的缓存 当缓存达到上限时,删除最先入队的缓存 eviction="FIFO"
SoftCache/WeakCache 带清理策略的缓存 通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理掉这些缓存 eviction="SOFT"/eviction="WEAK"
LoggingCache 带日志功能的缓存 比如输出缓存命中率 基本
SynchronizedCache 同步缓存 基于Synchronized关键字实现,解决并发问题 基本
BlockingCache 阻塞缓存 通过在get/put方式中加锁,保证只有一个线程操作缓存,基于Java重入锁实现 blocking=true
SerializedCache 支持序列化的缓存 将对象序列化以后存到缓存中,取出是反序列化 readOnly=false(默认)
ScheduledCache 定时调度的缓存 在进行 get/put/remove/getSize 等操作前,判断 缓存时间是否超过了设置的最长缓存时间(默认是 一小时),如果是则清空缓存--即每隔一段时间清 空一次缓存 flushInterval不为空
TransactionalCache 事务缓存 在二级缓存中使用,可以一次存入多个缓存,删除多个缓存 在TransactionalCacheManager中用Map维护对应关系

了解了上面这些缓存对象,我们要思考一个问题,缓存对象什么时候被创建?什么时候被装饰?
我们要弄清楚这个问题,就必须要知道MyBatis一级缓存和二级缓存的工作位置和工作方式的区别。

一级缓存

一级缓存也叫本地缓存,MyBatis一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis的一级缓存是默认开启的,不需要任何配置。

首先我们必须弄清楚一个问题,在MyBatis执行流程里面,涉及到这么多的对象,那么缓存PerpetualCache应该放到哪里去维护?如果要在同一个会话里面共享一级缓存,那这个对象肯定是在SqlSession中创建的,作为SqlSession的一个属性。

DefaultSqlSession这个类里面只有两个属性,Configuration是全局的配置,cache不可能放在这里维护,所以缓存只可能在Executor里面维护。SimpleExecutor/ReuseExecutor/BatchExecutor的父类BaseExecutor的构造函数中持有了PerpetualCache。


在这里插入图片描述

在同一个会话中,多次执行相同的SQL语句,会直接从内存中取缓存的结果,不会再发送SQL到数据库。但是不同的会话中,即使执行的SQL一模一样,也不能使用到一级缓存。


在这里插入图片描述

接下来我们来验证一下,MyBatis的一级缓存到底是不是只能在一个会话里面共享,以及跨会话(不同session)操作相同的数据会产生什么问题。
判断是否命中缓存:如果再次发送SQL到数据库执行, 说明没有命中缓存;如果直接打印对象,说明从内存缓存中取到结果
  1. 在同一个Session中共享
BlogMapper mapper = session.getMapper(BlogMapper.class); 
System.out.println(mapper.selectBlog(1)); 
System.out.println(mapper.selectBlog(1)); 
  1. 不同Session不能共享
BlogMapper mapper = session.getMapper(BlogMapper.class); 
System.out.println(mapper.selectBlog(1)); 

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

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


在这里插入图片描述
  1. 同一个会话中,update(包括delete)会导致一级缓存被清空
public void testCacheInvalid() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            System.out.println(mapper.selectBlogById(1));

            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("2019年1月6日14:39:58");
            mapper.updateByPrimaryKey(blog);
            session.commit();

            // 相同会话执行了更新操作,缓存是否被清空?
            System.out.println("在执行更新操作之后,是否命中缓存?");
            System.out.println(mapper.selectBlogById(1));

        } finally {
            session.close();
        }
    }

一级缓存是在 BaseExecutor 中的 update()方法中调用 clearLocalCache()清空的(无条件),query中会判断。


在这里插入图片描述
  1. 其他会话更新了数据,会导致读取到脏数据(一级缓存不能跨会话共享)
public void testDirtyRead() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));

            // 会话2更新了数据,会话2的一级缓存更新
            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("after modified 112233445566");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            mapper2.updateByPrimaryKey(blog);
            session2.commit();

            // 其他会话更新了数据,本会话的一级缓存还在么?
            System.out.println("会话1查到最新的数据了吗?");
            System.out.println(mapper1.selectBlogById(1));
        } finally {
            session1.close();
            session2.close();
        }
    }

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

二级缓存

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

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

作为一个作用范围更广的缓存,它肯定是在SqlSession的外层,否则不可能被多个SqlSession共享。而一级缓存是在SqlSession内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下,才会到一个会话中取一级缓存。

第二个问题,二级缓存存放在哪个对象中维护呢?要跨会话共享的话,SqlSession本身和它里面的BaseExecutor已经满足不了需求了,那我们应该在BaseExecutor之外创建一个对象。
实际上MyBatis用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis在创建Executor的时候会对Executor进行装饰。

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

在这里插入图片描述

一级缓存是默认开启的,那怎么开启二级缓存呢?
第一步,再mybatis-config.xml中添加配置(可以不添加,默认是开启的)

<!-- 控制全局缓存(二级缓存)-->
<setting name="cacheEnabled" value="true"/>

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

<!-- 声明这个namespace使用二级缓存 -->
<cache/>

<!--        <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
               size="1024"
               eviction="LRU"
               flushInterval="120000"
               readOnly="false"/>-->

cache标签属性详解:

属性 含义 取值
type 缓存实现类 需要实现Cache接口,默认是PerpetualCache
size 最多缓存对象个数 默认是1024
eviction 回收策略 LRU - 最近最少使用的:移除最长时间不被使用的对象(默认)
FIFO - 先进先出:按对象进入缓存的顺序移除他们
SOFT - 软引用:移除基于垃圾回收器状态和软引用规则的对象
WEAK - 弱引用:更积极地移除基于垃圾回收器状态和弱引用规则的对象
flushInterval 定时自动清空缓存间隔 自动刷新时间,单位ms,未配置时只有调用时刷新
readOnly 是否只读 true: 只读缓存;会给所有对象的调用者返回对象的相同实例,因此这些对象不能被修改。这提供了很中要的性能优势
false: 读写缓存,会返回缓存对象的copy(通过序列化),不会共享,性能会慢一些,但是安全,因此默认为false,此时缓存的对象必须实现序列化接口。
blocking 是否使用可重入锁实现缓存的并发控制 true: 会使用BlockingCache对Cache进行装饰。默认为false

Mapper.xml配置了<cache/>之后,select()会被缓存,update()、insert()、delete()会刷新缓存。
思考:如果只配置了cacheEnabled=true,mapper.xml中没有配置<cache/>,还有二级缓存吗?还会出现CachingExecutor包装对象吗?
只要配置了cacheEnabled=true,基本执行器就会被装饰,mapper中有没有配置<cache/>,决定了在启动的时候会不会创建这个mapper的cache对象,最终会影响到CachingExecutor中query方法中的判断:


在这里插入图片描述

如果某些查询方法对数据的实时性要求很高,不需要二级缓存怎么办?
我们可以在单个statement上显示的关闭二级缓存(默认是true)

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

了解了二级缓存的工作位置和开启关闭的方法之后,我们也来验证一下二级缓存。

  1. 事务不提交,二级缓存不存在
public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            // 事务不提交的情况下,二级缓存会写入吗?
            //session1.commit();

            System.out.println("第二次查询");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));
        } finally {
            session1.close();
        }
    }

思考:为什么事务不提交,二级缓存不生效?
因为二级缓存使用 TransactionalCacheManager(TCM)来管理,最后又调用了TransactionalCache的getObject()、 putObject和commit()方法, TransactionalCache里面又持有了真正的Cache对象,比如是经过层层装饰的PerpetualCache。在 putObject 的时候,只是添加到了entriesToAddOnCommit 里面,只有它的commit()方法被调用的时候才会调用 flushPendingEntries()真正写入缓存。它就是在DefaultSqlSession调用commit()的时候被调用的


在这里插入图片描述

在这里插入图片描述
  1. 使用不同的session和mapper,验证二级缓存可以跨session存在
public void testCache() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            // 事务不提交的情况下,二级缓存会写入吗?
            session1.commit();

            System.out.println("第二次查询");
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            System.out.println(mapper2.selectBlogById(1));
        } finally {
            session1.close();
        }
    }
  1. 在其他的session中执行增删改操作,验证缓存会被刷新
public void testCacheInvalid() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        SqlSession session1 = sqlSessionFactory.openSession();
        SqlSession session2 = sqlSessionFactory.openSession();
        SqlSession session3 = sqlSessionFactory.openSession();
        try {
            BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
            BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
            BlogMapper mapper3 = session3.getMapper(BlogMapper.class);
            System.out.println(mapper1.selectBlogById(1));
            session1.commit();

            // 是否命中二级缓存
            System.out.println("是否命中二级缓存?");
            System.out.println(mapper2.selectBlogById(1));

            Blog blog = new Blog();
            blog.setBid(1);
            blog.setName("2019年1月6日15:03:38");
            mapper3.updateByPrimaryKey(blog);
            session3.commit();

            System.out.println("更新后再次查询,是否命中二级缓存?");
            // 在其他会话中执行了更新操作,二级缓存是否被清空?
            System.out.println(mapper2.selectBlogById(1));

        } finally {
            session1.close();
            session2.close();
            session3.close();
        }
    }

思考: 为什么增删改操作会清空缓存?
在 CachingExecutor 的 update()方法里面会调用 flushCacheIfRequired(ms),isFlushCacheRequired 就是从标签里面渠道的 flushCache 的值。而增删改操作的flushCache属性默认为true

一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?

  1. 因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。
  2. 如果多个namespace中有针对于同一个表的操作,比如Blog 表,如果在一个namespace中刷新了缓存,另一个namespace中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper里面只操作单表的情况使用。

思考:如果要让多个namespace共享一个二级缓存,应该怎么做?
跨namespace的缓存共享的问题,可以使用<cache-ref>来解决:

<cache-ref namespace="com.gupaoedu.crud.dao.DepartmentMapper" /> 

cache-ref 代表引用别的命名空间的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。
注意:在这种情况下,多个Mapper的操作都会引起缓存刷新,缓存的意义已经不大了。

使用第三方插件做二级缓存

除了MyBatis自带的二级缓存之外,我们也可以通过实现Cache接口来自定义二级缓存。
MyBatis官方提供了一些第三方缓存集成方式,比如ehcache和redis:
https://github.com/mybatis/redis-cache
pom文件引入依赖:

<dependency> 
    <groupId>org.mybatis.caches</groupId> 
    <artifactId>mybatis-redis</artifactId> 
    <version>1.0.0-beta2</version> 
</dependency

Mapper.xml配置,type使用RedisCache:

<cache type="org.mybatis.caches.redis.RedisCache" eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> 

redis.properties配置:

host=localhost 
port=6379 
connectionTimeout=5000 
soTimeout=5000 
database=0

当然,我们也可以使用独立的缓存服务,不使用MyBatis自带的二级缓存。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342