• 3. 一级缓存解析



    在上一篇基础上,我们继续看下一级缓存相关内容
    mybatis中存在同时存在一级缓存和二级缓存,两者区别如下:

    • 一级缓存:也叫做会话级缓存,生命周期仅存在于当前会话,不可以直接关关闭。但可以通过flushCache和localCacheScope对其做相应控制(下面会涉及如何进行控制)。
    • 二级缓存:也叫应用级性缓存,缓存对象存在于整个应用周期,而且可以跨线程使用。

    本节内容主要分析一级缓存,首先看一下一级缓存命中的条件有哪些

    1. 一级缓存命中条件

    命中一级缓存需同时满足下面四个条件:

    1. SQL与参数相同;
    2. 同一个会话;
    3. 相同的MapperStatement ID;
    4. RowBounds分页的offset和limit要相等。

    1.1 SQL与参数相同

    正例:如要命中一级缓存需同时满足上述4个条件方可,如下面所示可以命中一级缓存。

        public void test1(){
            SqlSession sqlSession = mybatisUtil.getSqlSession();
            UserDao userDao = sqlSession.getMapper(UserDao.class);
            User user1 = userDao.selectOne(2);
            User user2 = userDao.selectOne(2);
            System.out.println(user1 == user2);     //输出true
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    反例:尽管SQL相同,但参数不同不可命中缓存,如下案例所示

    public void test1(){
        SqlSession sqlSession = mybatisUtil.getSqlSession();
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user1 = userDao.selectOne(2);
        User user2 = userDao.selectOne(3);
        System.out.println(user1 == user2);     //输出false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.2 同一个会话

    如果不是同一个会话同样不可以命中一级缓存,入戏所示构建两个sqlsession

    public void test1(){
        SqlSession sqlSession = mybatisUtil.getSqlSession();
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user1 = userDao.selectOne(2);
        SqlSession sqlSession2 = mybatisUtil.getFactory().openSession(true);
        UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
        User user2 = userDao2.selectOne(2);
        System.out.println(user1 == user2);     //输出false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    1.3 相同的MapperStatement ID

    尽管SQL和参数相同,如果MapperStatement ID不同也不可以命中缓存,MapperStatement ID为书写mapper文件对应的每个SQL前的id。

    public void test1(){
        SqlSession sqlSession = mybatisUtil.getSqlSession();
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user1 = userDao.selectOne(2);
        User user2 = userDao.selectById(2);
        System.out.println(user1 == user2);     //输出false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    <select id="selectOne" parameterType="int" resultType="com.lzj.bean.User">
        select * from user where id=#{id}
    </select>
    
    <select id="selectById" parameterType="int" resultType="com.lzj.bean.User">
        select * from user where id=#{id}
    </select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    1.4 RowBounds分页的offset和limit要相等

    如果分页条件不同也不可以命中一级缓存,比如下面案例,第一次未设置分页条件即默认RowBounds.DEFAULT,第二次分页0,2,从第0条开始,获取2条数据,显然分页条件不一样,因此不可以命中缓存。

    public void test1(){
        SqlSession sqlSession = mybatisUtil.getSqlSession();
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user1 = userDao.selectOne(2);
        List<Object> users2 = sqlSession.selectList("com.lzj.dao.UserDao.selectOne", 2, new RowBounds(0, 2));
        System.out.println(user1 == users2.get(0));     //输出false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    正案例,sqlSession.getMapper底层也是调用的RowBounds.DEFAULT。

    public void test1(){
        SqlSession sqlSession = mybatisUtil.getSqlSession();
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        User user1 = userDao.selectOne(2);
        List<Object> users2 = sqlSession.selectList("com.lzj.dao.UserDao.selectOne", 2, RowBounds.DEFAULT);
        System.out.println(user1 == users2.get(0));     //输出true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2. 一级缓存源码解析

    分析源码最好根据一个案例进行代码跟踪,下面以一个简单案例为例

        public void test1(){
            SqlSession sqlSession = mybatisUtil.getSqlSession();
            List<Object> users2 = sqlSession.selectList("com.lzj.dao.UserDao.selectOne", 2, RowBounds.DEFAULT);
        }
    
    • 1
    • 2
    • 3
    • 4

    在分析一级缓存源码时主要分析一级缓存部分,下面会过滤掉二级缓存部分以及嵌套查询部分。
    首先这个地方的sqlSession指的默认的DefalultSqlSession,因此就可以断点到DefaultSqlSession中的selectList方法,如下所示
    在这里插入图片描述
    通过断点可以看到首先执行的是二级缓存CachingExecutor,然后把断点打到二级缓存中的query方法中,如下所示当list为null时表示未命中二级缓存,再继续去执行delegate中的query方法。在前面一章节介绍到了CachingExecutor二级缓存执行器实际是对简单执行器、复用执行器、批量执行器的装饰,所以内部的delegeta还是指的这三种执行器,如没有特别指定,默认就是对SimpleExecutor执行器的装饰。
    在这里插入图片描述
    下面把断点放到BaseExecutor中的query方法上,因为三大执行器都是继承了BaseExecutor,三大执行器的query方法都是利用的BaseExecutor中的query方法,也即为一级缓存执行的源码,如下所示,首先是先执行一级缓存,如果一级缓存命中了直接返回,如果一级缓存未命中则不会执行一级缓存,重要行代码解析如下注释

      public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        /*queryStack非嵌套查询或者对于嵌套查询的最外层,如果mapper中配置了清空缓存,此时会首先清空一级缓存的*/
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
          clearLocalCache();
        }
        List<E> list;
        try {
          queryStack++;
          /*对于查询SQL语句要先从一级缓存中获取*/
          list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
          if (list != null) {
            /*如果命中了一级缓存,则把缓存命中的数据组装到输出中*/
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
          } else {
            /*如果一级缓存未命中,则再继续查数据库*/
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
          }
        } finally {
          queryStack--;
        }
        if (queryStack == 0) {
          /*对于嵌套查询实现延迟加载*/
          for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
          }
        
          deferredLoads.clear();
          /*LocalCacheScope包含SESSION和STATEMENT级别
            1.SESSION级别表示在一次会话请求中命中不了二级缓存的会首先从一级缓存缓存中获取结果。
            2.STATEMENT级别表示SQL语句级别的,每次查询SQL后都会清空缓存,就会导致即使在同一个session中即使相同的SQL也不会命中缓存,
              只有一种例外,对于嵌套查询,子查询完不会清空一级缓存,而是等到最外层的查询结束后才会清空缓存,也就是说外层查询还是有可能会用到子查询后的一级缓存的
          */
          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
          }
        }
        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
    • 39
    • 40
    • 41
    • 42
    • 43

    3.一级缓存失效场景

    在BaseExecutor中清空一级缓存的方法为clearLocalCache,在idea中可以通过查看调用该方法的场景就可以确定一级缓存都是在哪些场景情况下时效的,通过方法调用追踪可以发现调用清空一级缓存的方法如下所示
    在这里插入图片描述

    1. 执行update操作(包括insert、delete):执行任意的增删改都会清空一级缓存;
    2. mapper的SQL文件中配置了flushCache=true也会在查询前清空一级缓存;
    3. mapper的配置文件配置了缓存LocalCacheScop缓存作用于为STATEMENT会在查询后清空一级缓存,相当于查询完缓存再清空缓存,多了一次缓存的命中;
    4. 事务提交前会清空一级缓存;
    5. 事务回滚前会清空一级缓存。

    4. 一级缓存缺点

    虽然一级缓存可以在一个session中提升查询效率,但一级缓存也有其弊端,就是会导致脏读。
    比如在sessionA中查询了同一条SQL:select name, age from user where name=‘tom’,查出来的name=tom,age=20;
    然后在sessionB中执行了update user set age=25 where name=‘tom’,并commit;
    最后继续在sessionA中执行一遍select name, age from user where name=‘tom’,发现还是输出的name=tom,age=20,出现了脏读问题。

    那么如何关闭一级缓存呢,mybatis可以配置如下localCacheScope=STATEMENT,一级缓存只对当前语句执行有效,一旦该SQL执行完后,即使在同一个session中执行了相同的SQL也是重新查数据库了;默认配置是localCacheScope=SESSION,表示一级缓存在整个session中是有效的。

    <settings>
        <setting name="localCacheScope" value="STATEMENT"/>
    settings>
    
    • 1
    • 2
    • 3
  • 相关阅读:
    2022年12月英语六级预测范文—预测范文:Be Willing To Try
    攻防世界题目练习——Web引导模式(三)(持续更新)
    从零开始实现lmax-Disruptor队列(二)多消费者、消费者组间消费依赖原理解析
    【H3C设备组网配置】第二版
    STM32的IAP
    安装rocketmq-dashboard1.0.1
    OpenSSF 基金会总经理 Brian Behlendorf :预计 2026 年将有 4.2 亿个开源
    vue之Error: Unknown option: .devServer.
    【Flink入门修炼】1-2 Mac 搭建 Flink 源码阅读环境
    Mysql_14 存储引擎
  • 原文地址:https://blog.csdn.net/u010502101/article/details/127857028