专栏首页求道MyBatis为了解决二级缓存脏读问题,究竟做了那些骚操作!

MyBatis为了解决二级缓存脏读问题,究竟做了那些骚操作!

  • 朝闻道,夕死可矣!

一、存在即合理

MyBatis为了提高我们的查询性能,专门设计了一级缓存和二级缓存,众所周知,我们在开发环境中,使用的缓存的时候,也会遇到各种各样的挑战,比如缓存穿透缓存雪崩数据脏读等等各种各样的问题,MyBatis也同样,在设计二级缓存的时候,MyBatis也同样遇见了各种挑战;

我这几天在观看MyBatis对于二级缓存的设计的时候,突然发现,我们查询出来一个数据后并没有直接放置到二级缓存中,而是放到了另外一个存储空间,只有提交了之后才会被设置进二级缓存,我不仅疑惑,存在即合理,为什么MyBatis在设计二级缓存的时候,要“多此一举”呢?所以也就有了作者熬夜深入探究的过程!

二、测试代码

首先为了方便测试,我们先搞个能够命中二级缓存的实例代码:

@Test
public void sessionTest(){
    SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE, true);
    List<Object> objects = sqlSession.selectList("com.huangfu.TestMapper.selectUser","周六");
    List<Object> objects1 = sqlSession.selectList("com.huangfu.TestMapper.selectUser","周六");
    //哦吼  提交一哈
    sqlSession.commit();
    List<Object> objects2 = sqlSession.selectList("com.huangfu.TestMapper.selectUser","周六");
}

注:上面已经说到了,只有在提交之后才会将缓存刷新到二级缓存空间,原理后面会探究,此处属于作者嘚吧嘚!

这里会命中几次呢?你是不是猜的两次?如果你猜的两次,那么你肯定是不了解暂存区的概念,事实上,在第一次查询后,查询的结果并不会同步到二级缓存空间,只有在提交后,才会刷新进去,所以正确答案是只命中一次,命中率是 0.3333333333333333

至于这个原因嘛,听作者细细道来:

三、探究真理

首先大家要了解一个概念:暂存区,他是保存SqlSession在事务中需要向某个二级缓存提交的缓存数据,因为事务过程中的数据可能会回滚,所以不能直接把数据就提交二级缓存,而是暂存在TransactionalCache中,在事务提交后再将过程中存放在其中的数据提交到二级缓存,如果事务回滚,则将数据清除掉!

可以把暂存区理解为一个中间容器,它是为了保证一个事务原子性的容器,它存储这一个提交操作前的全部数据,待提交操作执行后,再将暂存区的内容一次性刷新到二级缓存空间内!

前几篇关于MyBatis的文章我说到过,有关二级缓存的逻辑被抽象到了CachingExecutor内部,既然我们开启了二级缓存,按照会话对象:执行器 = 1:1的说法,那么咱们示例代码的的执行器一定是CachingExecutor,看过我前面文章的人大概应该知道,查询方法会默认执行query方法,那么我们重点 debug的对象,应该是 query方法。

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
              ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    //获取该命名空间下的的二级缓存空间
    Cache cache = ms.getCache();
    if (cache != null) {
      //是否设置了刷新暂存区
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        //查询二级缓存空间里面的缓存数据
        List<E> list = (List<E>) tcm.getObject(cache, key);
        //如果二级缓存空间没有查到数据
        if (list == null) {
          //查询数据库
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          //将查询数据放置到暂存区
          tcm.putObject(cache, key, list);
        }
        return list;
      }
    }
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

可以看到,事实上,我们的插叙出来的数据并没有被放置到缓存区,而是被放置在了暂存区,至于原因,我们下面再谈!那么什么时候会从暂存区刷新到缓存区呢?是提交时的操作,我们看一下commit的基本逻辑!

一路源码追踪,会看到如下的逻辑

private void flushPendingEntries() {
    //遍历所有的暂存区数据,一个一个的放置到二级缓存空间
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    ..... 忽略讨论之外的代码....
  }

此时不仅恍然大悟,原来命中一次的原因是这样,只有提交了之后,才会被刷新进二级缓存区,所以提交后的查询才被命中缓存,那么话又说回来,用意何在?

其实仅仅是为了避免脏数据,试想一下,如果没有暂存区空间会有什么情况发生?

假设发生了一个写操作,执行完成后另外一个请求查询到了该数据直接放置到二级缓存区域,但是此时这条数据执行了回滚操作,那么此时就会造成一个脏读!

image-20200710135312612

基于上图反之,我们在进行修改操作的时候,依旧不能够直接清空二级缓存空间,而是伪清除(留存一个清除标记),待提交操作的时候,才真正的执行删除操作!

所以在修改方法里面有这样一段代码:

public void clear(Cache cache) {
    //clear方法调用如下
  getTransactionalCache(cache).clear();
}

public void clear() {
    //设置清除标记
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

可以看到,修改方法事实上并不会去去清空二级缓存区域,而是设置了一个提交标识,那么这个提交标识有什么用处呢?

public void commit() {
    //当设置清除标记的时候删除二级缓存
    if (clearOnCommit) {
        delegate.clear();
    }
    //刷新暂存区到缓存区
    flushPendingEntries();
    //恢复个数值位置 比如 提交标记重置为false
    reset();
}

为啥又要多此一步?

一个修改操作,修改完数据后,将二级缓存清空,但是此时数据异常,发生回滚!事实上,数据没有修改成功,我们是不应该去清空二级缓存的,这是不应该的!所以在没有提交前,是不能清空缓存区的!

经过以上的分析,我们总结出大概流程如下:

一个暂存区,就能够避免部分数据脏读问题,不得不感叹MyBatis设计的精妙之处!但是这真的能够解决脏读问题吗?事实上并不是如此!下面扩展一些因为一些特殊原因引起的脏读问题!

四、扩展知识

因为MyBatis数据二级缓存的设计对于不同的命名空间是隔离的(一个Mapper 用一个二级缓存),所以,在特定情况下依旧会出现脏读的数据!

这个出现的原因是因为不同的Mapper查询隔离分别使用不同的存储空间,那么当两个Mapper操作同一张表时就出现脏读的问题,如何解决呢?

想一下,出现这个问题的原因是什么?是因为没有公用一个缓存区,那么我们使用同一个缓存区就能够解决了吧!如何使用呢?

只需要在对应的Mapper文件中,将该Mapper的命名空间引用另外一个Mapper的命名空间就可以使两个Mapper共用一个缓存空间!
<cache-ref namespace="xxx.xxx.xxx.UserMapper2"></cache-ref>

当然还有其他的解决方案,比如注解级别的,作者就不一一赘述了!其实,这两天我看网上的一些资料,作者应该是第一个专门介绍暂存区的人,如果文章中有理解有问题,欢迎各位指正!

本文分享自微信公众号 - JAVA程序狗(javacxg),作者:皇甫嗷嗷叫

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 当一个http请求来临时,SpringMVC究竟偷偷帮你做了什么?请求映射器篇

    Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进...

    止术
  • 当一个http请求来临时,SpringMVC究竟偷偷帮你做了什么?

    Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进...

    止术
  • 面试官问我Volatile的原理?从操作系统层面的设计怼回去!

    在多线程并发编程中,synchronized和volatile都扮演着及其重要的角色;可以这么说,Volatile是轻量级的synchronized!volat...

    止术
  • 从100到1000万高并发的架构演进之路

    本文以设计淘宝网的后台架构为例,介绍从一百个并发到千万级并发情况下服务端的架构的14次演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一...

    bigsai
  • JavaScript获取cookie的方法

    之前都是使用 php 对 cookie 进行操作,今天有个需求,需要用 js 获取网站的 cookie 。下面开始:

    德顺
  • 教育如何应对人工智能等挑战

    作者 | 王元丰 授权发布 编辑 | GeoTalks 我们处在新的工业革命时代,颠覆性新技术不断涌现,对社会产生深刻影响。人工智能、机器人等技术创新太...

    企鹅号小编
  • 高通骁龙855强势上线:7nm工艺,性能较845提升至少3倍,全面支持5G

    12月5日凌晨,高通于夏威夷举办骁龙技术峰会,强势发布全球首款5G商用AI芯片——骁龙855。

    镁客网
  • 向FT学到的第一些东西@365

    早上原本打算组队去听YT告知的无人机讲座,结果昨晚临时要求开会,收获还是挺多的,除了开会之外也有另外一点重要的收获。到了大四最后悔的也许就是只是结识了那...

    钱塘小甲子
  • 万字长文!资深大牛谈游戏程序员的个人修炼

    程序你好
  • 机器学习竞赛分享:NFL大数据碗(上篇)

    一年一度的NFL大数据碗,今年的预测目标是通过两队球员的静态数据,预测该次进攻推进的码数,并转换为该概率分布;

    HoLoong

扫码关注云+社区

领取腾讯云代金券