mybatis source (4) Analysis twelve cache Analysis

Major topics of this blog mybatis twelve cache configuration, as well as some examples of error-prone parts of the analysis;

A, mybatis caching system

mybatis a secondary cache system is as follows:

  • First, when the twelve cache open simultaneously, first secondary cache hit;
  • Cache is located in a BaseExecutor not be closed, but can be specified range STATEMENT, SESSION;
  • Although the entire second-level cache after a lot of matters related components, but ultimately landing in Cache MapperStatement in (Cache specific examples of the type of cache type can be specified in the tag mapper xml, the default PerpetualCache), while one-MapperStatement and namespace , the secondary cache is scoped mapper namespace;
  • When using secondary cache, if the cache does not hit the back look, then the query results are not directly into the cache, but first place in the local cache TransactionCache, here distinguish entriesToAddOnCommit, entriesMissedInCache to statistics command rate, Finally, when sqlSession commit, will be submitted to the local cache TransactionCache cache, the cache is at this time to other sqlSession visible;
  • Further when it is necessary when distributed cache, you need to put the secondary cache JVM outside, where the interface can be implemented cache write their own cache, this time can be used ehcache, redis other external cache operation in the cache implementation;

Above with respect to the overall configuration is substantially mybatis cache, it will now be divided twelve test split cache module;

Second, the cache

Cache mybatis normal circumstances rarely used, there are two main reasons:

  • Cache life cycle with SqlSession, so prone to dirty read;
  • 一级缓存的 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 分布式二级缓存的时候,要确保缓存淘汰等策略是可以用于分布式缓存的;

Guess you like

Origin www.cnblogs.com/sanzao/p/11414305.html