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 分布式二级缓存的时候,要确保缓存淘汰等策略是可以用于分布式缓存的;

原文地址:https://www.cnblogs.com/sanzao/p/11414305.html

时间: 2024-09-27 10:45:08

mybatis 源码分析(四)一二级缓存分析的相关文章

Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?

Mybatis源码解析(四) -- SqlSession是如何实现数据库操作的? ??如果拿一次数据库请求操作做比喻,那么前面3篇文章就是在做请求准备,真正执行操作的是本篇文章要讲述的内容.正如标题一样,本篇文章最最核心的要点就是 SqlSession实现数据库操作的源码解析.但按照惯例,我这边依然列出如下的问题: 1. SqlSession 是如何被创建的? 每次的数据库操作都会创建一个新的SqlSession么?(也许有很多同学会说SqlSession是通过 SqlSessionFactor

手把手带你阅读Mybatis源码(三)缓存篇

前言 大家好,这一篇文章是MyBatis系列的最后一篇文章,前面两篇文章:手把手带你阅读Mybatis源码(一)构造篇 和 手把手带你阅读Mybatis源码(二)执行篇,主要说明了MyBatis是如何将我们的xml配置文件构建为其内部的Configuration对象和MappedStatement对象的,然后在第二篇我们说了构建完成后MyBatis是如何一步一步地执行我们的SQL语句并且对结果集进行封装的. 那么这篇作为MyBatis系列的最后一篇,自然是要来聊聊MyBatis中的一个不可忽视的

Mybatis源码阅读之--整体执行流程

Mybatis执行流程分析 Mybatis执行SQL语句可以使用两种方式: 使用SqlSession执行update/delete/insert/select操作 使用SqlSession获得对应的Mapper,然后调用mapper的相应方法执行语句 其中第二种方式获取Mapper的流程在前面已经解析过,请查看文章Mybatis源码阅读之--Mapper执行流程 其实这个方法最后的MapperMthod也是调用SqlSession的相应方法执行增删该的操作,这边文章主要介绍SqlSession执

Mybatis源码分析之Cache二级缓存原理 (五)

一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(ServiceProvider Interface) ,所有的MyBatis内部的Cache缓存,都应该实现这一接口 Cache的实现类中,Cache有不同的功能,每个功能独立,互不影响,则对于不同的Cache功能,这里使用了装饰者模式实现. 看下cache的实现类,如下图: 1.FIFOCache:先进

mybatis源码分析(四) mybatis与spring事务管理分析

mybatis源码分析(四) mybatis与spring事务管理分析 一丶从jdbc的角度理解什么是事务 从mysql获取一个连接之后, 默认是自动提交, 即执行完sql之后, 就会提交事务. 这种事务的范围是一条sql语句. 将该连接设置非自动提交, 可以执行多条sql语句, 然后由程序决定是提交事务, 还是回滚事务. 这也是我们常说的事务. Connection connection = dataSource.getConnection(); // connection.setTransa

MyBatis源码分析(五):MyBatis Cache分析

一.Mybatis缓存介绍 在Mybatis中,它提供了一级缓存和二级缓存,默认的情况下只开启一级缓存,所以默认情况下是开启了缓存的,除非明确指定不开缓存功能.使用缓存的目的就是把数据保存在内存中,是应用能更快获取数据,避免与数据库频繁交互,特别是在查询比较多.命中率比较高的情况下,缓存就显得很重要.但是使用不得当,会产生脏数据. 二.目录 一级缓存介绍及相关配置. 一级缓存工作流程及源码分析. 一级缓存总结. 二级缓存介绍及相关配置. 二级缓存源码分析. 二级缓存总结. 全文总结. 三.一级缓

MyBatis 源码分析——介绍

笔者第一次接触跟MyBatis框架是在2009年未的时候.不过那个时候的他并不叫MyBatis,而是叫IBatis.2010年的时候改为现在的名字--MyBatis.这几年过去了,对于笔者来讲有一点陌生了.而且那个时候他也没有这么出名.hibernate占了大部分市场.虽然笔者早年的时候查看过他的源码,但是并没有很深入去理解他.主要的原因是因为当时我还在看hibernate的源码.太累了所以就没有去认真的理解.现在笔者想要重新在来看一篇关于他的源码并加强对他的理解.也是对自己过程的一种回归吧.

mybatis源码分析之cache创建

XMLMapperBuilder.java //解析<cache /> 配置元素,创建cache对象 private void cacheElement(XNode context) throws Exception {     if (context != null) {       String type = context.getStringAttribute("type", "PERPETUAL");       Class<? exten

MyBatis源码分析-SQL语句执行的完整流程

MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录.如何新建MyBatis源码工程请点击MyBatis源码分析-IDEA新建MyBatis源码工程. MyBatis框架主要完成的是以下2件事情: 根据JD

【MyBatis源码分析】select源码分析及小结

示例代码 之前的文章说过,对于MyBatis来说insert.update.delete是一组的,因为对于MyBatis来说它们都是update:select是一组的,因为对于MyBatis来说它就是select. 本文研究一下select的实现流程,示例代码为: 1 public void testSelectOne() { 2 System.out.println(mailDao.selectMailById(8)); 3 } selectMailById方法的实现为: 1 public M