• mybatis 源码分析(四)一二级缓存分析


    本篇博客主要讲了 mybatis 一二级缓存的构成,以及一些容易出错地方的示例分析;

    一、mybatis 缓存体系

    mybatis 的一二级缓存体系大致如下:

    • 首先当一二级缓存同时开启的时候,首先命中二级缓存;
    • 一级缓存位于 BaseExecutor 中不能关闭,但是可以指定范围 STATEMENT、SESSION;
    • 整个二级缓存虽然经过了很多事务相关的组件,但是最终是落地在 MapperStatement 的 Cache 中(Cache 的具体实例类型可以在 mapper xml 的 cache type 标签中指定,默认 PerpetualCache),而 MapperStatement 和 namespace 一一对应,所以二级缓存的作用域是 mapper namespace;
    • 在使用二级缓存的时候,如果 cache 没有命中则向后查找,然后查询的结果不是直接放到 cache 中,而是首先放到 TransactionCache 的本地缓存中,这里区分 entriesToAddOnCommit、entriesMissedInCache 是为了统计命令率,最后在 sqlSession commit 的时候,才会将 TransactionCache 的本地缓存提交到 cache 中,此时 cache 才是对其他 sqlSession 可见的;
    • 此外当需要分布式缓存的时候,就需要将二级缓存放到 JVM 之外,这里可以实现 cache 接口编写自己的 cache,此时在实现的 cache 中就可以使用 ehcache、redis 等外部缓存进行操作;

    以上就大致是 mybatis 缓存的整体结构,下面将分模块拆分测试一二级缓存;

    二、一级缓存

    mybatis 的一级缓存一般情况很少使用,其原因主要有两个:

    • 一级缓存的生命周期同 SqlSession,所以容易出现脏读;
    • 一级缓存的 cache 的实现只能是 PerpetualCache,所以不能指定容量等设置;

    1. 脏读测试

    指定一级缓存范围为 SESSION:

    <setting name="localCacheScope" value="SESSION"/>
    
    @Test
    public void test01() {
      SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
      try (
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
      ) {
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        log.info("---get: {}", userMapper1.getUser(1L));
        log.info("---get: {}", userMapper2.getUser(1L));
        log.info("---update: {}", userMapper1.setNameById(1L, "LiSi"));
        log.info("---get: {}", userMapper1.getUser(1L));
        log.info("---get: {}", userMapper2.getUser(1L));
      }
    }
    

    结果如下:

    [DEBUG] sanzao.db.UserMapper.getUser - ==>  Preparing: select * from user where id = ? 
    [DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
    [TRACE] sanzao.db.UserMapper.getUser - <==    Columns: id, username, password, address
    [TRACE] sanzao.db.UserMapper.getUser - <==        Row: 1, ZhangSan, 123456, TT
    [DEBUG] sanzao.db.UserMapper.getUser - <==      Total: 1
    [INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
    [DEBUG] org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
    [DEBUG] org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 61073295.
    [DEBUG] sanzao.db.UserMapper.getUser - ==>  Preparing: select * from user where id = ? 
    [DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
    [TRACE] sanzao.db.UserMapper.getUser - <==    Columns: id, username, password, address
    [TRACE] sanzao.db.UserMapper.getUser - <==        Row: 1, ZhangSan, 123456, TT
    [DEBUG] sanzao.db.UserMapper.getUser - <==      Total: 1
    [INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
    [DEBUG] sanzao.db.UserMapper.setNameById - ==>  Preparing: update user set username = ? where id = ? 
    [DEBUG] sanzao.db.UserMapper.setNameById - ==> Parameters: LiSi(String), 1(Long)
    [DEBUG] sanzao.db.UserMapper.setNameById - <==    Updates: 1
    [INFO] sanzao.Test01 - ---update: 1
    [DEBUG] sanzao.db.UserMapper.getUser - ==> Parameters: 1(Long)
    [TRACE] sanzao.db.UserMapper.getUser - <==    Columns: id, username, password, address
    [TRACE] sanzao.db.UserMapper.getUser - <==        Row: 1, LiSi, 123456, TT
    [DEBUG] sanzao.db.UserMapper.getUser - <==      Total: 1
    [INFO] sanzao.Test01 - ---get: User{id=1, user_name='LiSi', password='123456', address='TT'}
    [INFO] sanzao.Test01 - ---get: User{id=1, user_name='ZhangSan', password='123456', address='TT'}
    

    可以看到当 sqlSession1 更新的时候,sqlSession2 的缓存仍然有效所以出现了脏读;所以通常都设置一级缓存的范围为:STATEMENT;

    2. 源码分析

    mybatis 的一级缓存主要和 Executor 整合比较多,所以建议先查看我上一篇博客 Executor 详解 ,详细了解缓存命中的整体流程;这里一级缓存的源码也很简单:

    • 查询的时候,首先查缓存,命中则返回,未命中就查数据库,然后填充缓存;
    • 更新、提交等操作情况缓存;
    @SuppressWarnings("unchecked")
    @Override
    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."); }
      
      // 查询的时候一般不清楚缓存,但是可以通过 xml配置或者注解强制清除,queryStack == 0 是为了防止递归调用
      if (queryStack == 0 && ms.isFlushCacheRequired()) { clearLocalCache(); }
      List<E> list;
      try {
        queryStack++;
        // 首先查看一级缓存
        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();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      	  // 一级缓存本身不能关闭,但是可以设置作用范围 STATEMENT,每次都清除缓存
          clearLocalCache();
        }
      }
      return list;
    }
    

    三、二级缓存

    mybatis 二级缓存要稍微复杂一点,中间多了一步事务缓存:

    • 首先无论是查询还是更新,都会按要求清空缓存 flushCacheIfRequired,默认更新清空,查询不清空,也可以在 xml 或者注解中指定;
    • 查询的时候,先查缓存,命中返回,未命中查一级缓存、数据库,然后回填事务缓存,注意这里不是直接填充到缓存中;此时的事务缓存对任何的 SqlSession 都是不可见的,因为自己查询的时候也是直接查询的目标缓存;
    • 更新就直接委托给目标 Executor 执行;
    • 最后 SqlSession 执行commit 的时候,将事务缓存刷新到目标缓存中;

    1. 事务缓存测试

    设置二级缓存:

    <setting name="cacheEnabled" value="true"/>
    
    <mapper namespace="***">
      <cache/>
    </mapper>
    
    @Test
    public void test02() {
      SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
      try (
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
      ) {
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    
        User u1 = userMapper1.getUser(1L);
        System.out.println("---get u1: " + u1);
    
        User u2 = userMapper2.getUser(1L);
        System.out.println("---get u2: " + u2);
    
        User u3 = userMapper1.getUser(1L);
        System.out.println("---get u3: " + u3);
      }
    }
    

    打印:

    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 1(Long)
    DEBUG [main] - <==      Total: 1
    ---get u1: User{id=1, user_name='sanzao', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    DEBUG [main] - Opening JDBC Connection
    DEBUG [main] - Created connection 1613095350.
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 1(Long)
    DEBUG [main] - <==      Total: 1
    ---get u2: User{id=1, user_name='sanzao', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    ---get u3: User{id=1, user_name='sanzao', password='123456', address='TT'}
    

    可以看到:

    • SqlSession1 为提交事务缓存,所以 SqlSession2 又从数据库中查了一次;
    • 当SqlSession1 再次查询的时候,二级缓存未命中 Cache Hit Ratio 为 0,但是命中了一级缓存,所以并未再查数据库;

    2. 二级缓存测试

    这次我们提交缓存看看是否命中:

    @Test
    public void test03() {
      SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
      try (
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
      ) {
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    
        User u1 = userMapper1.getUser(1L);
        System.out.println("---get u1: " + u1);
        sqlSession1.commit();
    
        User u2 = userMapper2.getUser(1L);
        System.out.println("---get u2: " + u2);
    
        int i = userMapper1.setNameById(1L, "LiSi");
        System.out.println("---update user: " + i);
        sqlSession1.commit();
    
        User u3 = userMapper1.getUser(1L);
        System.out.println("---get u3: " + u3);
        sqlSession1.commit();
    
        User u4 = userMapper2.getUser(1L);
        System.out.println("---get u4: " + u4);
      }
    }
    

    打印:

    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 1(Long)
    DEBUG [main] - <==      Total: 1
    ---get u1: User{id=1, user_name='sanzao', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
    ---get u2: User{id=1, user_name='sanzao', password='123456', address='TT'}
    DEBUG [main] - ==>  Preparing: update user set username = ? where id = ? 
    DEBUG [main] - ==> Parameters: LiSi(String), 1(Long)
    DEBUG [main] - <==    Updates: 1
    ---update user: 1
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 1(Long)
    DEBUG [main] - <==      Total: 1
    ---get u3: User{id=1, user_name='LiSi', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.5
    ---get u4: User{id=1, user_name='LiSi', password='123456', address='TT'}
    

    这次就能看到当 SqlSession1 提交事务缓存后,SqlSession2 就能看到了;

    3. 缓存配置测试

    此外还可以配置各种二级缓存策略,比如大小,刷新间隔时间,淘汰策略等,这里主要就是使用了 Cache 接口的装饰者模式:

    • LRU – 最近最少使用:移除最长时间不被使用的对象。
    • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
    • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
    • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

    但是需要注意的是这里的策略也能用户本地缓存,对于分布式缓存有些策略还是有问题;比如:

    <cache eviction="FIFO" flushInterval="60000" size="2" readOnly="true"/>
    

    这里主要定义了缓存大小2,使用 FIFO 策略更新;

    @Test
    public void test04() {
      SqlSessionFactory sqlSessionFactory = DBUtils.getSessionFactory();
      try (
        SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
        SqlSession sqlSession2 = sqlSessionFactory.openSession(true);) {
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    
        System.out.println("---get user: " + userMapper1.getUser(1L));
        sqlSession1.commit();
    
        System.out.println("---get user: " + userMapper1.getUser(2L));
        sqlSession1.commit();
    
        System.out.println("---get user: " + userMapper1.getUser(3L));
        sqlSession1.commit();
    
        System.out.println("---get user: " + userMapper2.getUser(1L));
    
        System.out.println("---get user: " + userMapper2.getUser(2L));
    
        System.out.println("---get user: " + userMapper1.getUser(1L));
        sqlSession2.commit();
    
        System.out.println("------------");
        System.out.println("---get user: " + userMapper1.getUser(1L));
        System.out.println("---get user: " + userMapper1.getUser(2L));
        System.out.println("---get user: " + userMapper1.getUser(3L));
      }
    }
    

    打印:

    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 1(Long)
    DEBUG [main] - <==      Total: 1
    ---get user: User{id=1, user_name='s1', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 2(Long)
    DEBUG [main] - <==      Total: 1
    ---get user: User{id=2, user_name='s2', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 3(Long)
    DEBUG [main] - <==      Total: 1
    ---get user: User{id=3, user_name='s3', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.0
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 1(Long)
    DEBUG [main] - <==      Total: 1
    ---get user: User{id=1, user_name='s1', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2
    ---get user: User{id=2, user_name='s2', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.16666666666666666
    DEBUG [main] - ==>  Preparing: select * from user where id = ? 
    DEBUG [main] - ==> Parameters: 1(Long)
    DEBUG [main] - <==      Total: 1
    ---get user: User{id=1, user_name='s1', password='123456', address='TT'}
    ------------
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.2857142857142857
    ---get user: User{id=1, user_name='s1', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.25
    DEBUG [main] - ==> Parameters: 2(Long)
    DEBUG [main] - <==      Total: 1
    ---get user: User{id=2, user_name='s2', password='123456', address='TT'}
    DEBUG [main] - Cache Hit Ratio [sanzao.db.UserMapper]: 0.3333333333333333
    ---get user: User{id=3, user_name='s3', password='123456', address='TT'}
    

    从日志中可以看到对于 SqlSession1,大小2,FIFO 是生效的,但是 SqlSession2 提交了之后,就发现缓存 s1,s2,s3 都命中了;

    至于源码太多了就不一次分析了,对于上面说的使用装饰者模式,可以在 CacheBuilder 中看到;

    public Cache build() {
      setDefaultImplementations();
      Cache cache = newBaseCacheInstance(implementation, id);
      setCacheProperties(cache);
      // issue #352, do not apply decorators to custom caches
      if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
          cache = newCacheDecoratorInstance(decorator, cache);
          setCacheProperties(cache);
        }
        cache = setStandardDecorators(cache);
      } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
      }
      return cache;
    }
    

    总结

    • mybatis 一级缓存的生命周期和 SqlSession 是一样的,通常情况下不建议使用一级缓存,通常将一级缓存范围设置为 STATEMENT;
    • 使用 mybatis 二级的时候,务必记得 SqlSession.commit ,否则二级缓存是不生效的;
    • 在配置 mybatis 分布式二级缓存的时候,要确保缓存淘汰等策略是可以用于分布式缓存的;
  • 相关阅读:
    负载均衡(负载平衡)
    JavaScript中绑定事件监听函数的通用方法[ addEvent() ]
    有趣的浏览器检测
    IE6 bug之 href= “javascript:void(0);”
    SVN使用技巧 不要把不必要的文件版本化 *.suo,*.bin,*.obj
    CacheDependency缓存依赖里面的 absoluteExpiration(绝对到期时间),弹性到期时间(slidingExpiration)
    TimeSpan 和 DateTime
    字符串数组 string[] 转换为 字符串(用逗号,作为分隔符),linq Except的用法,linq获取两个字符串数组相同的部分
    List的ToLookup 分组方法
    mysql 返回查询结果,返回out返回值,多表联合查询的分页存储过程
  • 原文地址:https://www.cnblogs.com/sanzao/p/11414305.html
Copyright © 2020-2023  润新知