Mybatis一级二级缓存

339 阅读4分钟

一级缓存测试

1)相同会话

关闭二级缓存
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="false"/>

@Test
public void testCache() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
        BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
        System.out.println("第一次查询~~~~~都是同一会话session1~~~~~~~~");
        Blog blog = mapper0.selectBlogById(1);
        System.out.println(blog);

        System.out.println("第二次查询~~~~~都是同一会话session1~~~~~");
        System.out.println(mapper1.selectBlogById(1));

    } finally {
        session1.close();
    }
}

可以看到只有第一次打印SQL执行过程信息,第二次没有打印就是从cache里获取了 image.png

2)不同会话

@Test
public void testCache() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper0 = session1.getMapper(BlogMapper.class);

        System.out.println("第一次查询~~~~~~~~~~session1和session2不同会话~~~~~~~");
        Blog blog = mapper0.selectBlogById(1);
        System.out.println(blog);

        System.out.println("第二次查询~~~~~~~~~~session1和session2不同会话~~~~~~~");
        BlogMapper mapper1 = session2.getMapper(BlogMapper.class);
        System.out.println(mapper1.selectBlogById(1));

    } finally {
        session1.close();
    }
}

image.png

3)更新之后,一级缓存被清空

关键代码
BlogMapper mapper = session.getMapper(BlogMapper.class);
System.out.println(mapper.selectBlogById(1));

Blog blog = new Blog();
blog.setBid(1);
blog.setName("after modified 666");
mapper.updateByPrimaryKey(blog);
session.commit();

// 相同会话执行了更新操作,缓存是否被清空?
System.out.println("在[同一个会话]执行更新操作之后,是否命中缓存?");
System.out.println(mapper.selectBlogById(1));

结果:

image.png

4)被其他会话更新后,一级缓存会读取过时数据

BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println("第一次查询~~~~~~~~~~~~~");
System.out.println(mapper1.selectBlogById(1));

// 会话2更新了数据,会话2的一级缓存更新
Blog blog = new Blog();
blog.setBid(1);
blog.setName("after modified 555555555555");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
mapper2.updateByPrimaryKey(blog);
session2.commit();

// 其他会话更新了数据,本会话的一级缓存还在么?
System.out.println("被会话2更新后,第二次查询会话1查到最新的数据了吗?");
System.out.println(mapper1.selectBlogById(1));

image.png 所以一级缓存工作范围太小,会从cache中读取到过时数据。

二级缓存

1)配置

作用域:namespace,范围就是在一个接口Mapper
<mapper namespace="com.gupaoedu.mapper.BlogMapper">

总开关在mybatis-config.xml中配置,默认是开启的,可以手动关闭
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>
然后在具体的Mapper.xml中和也需要配置<cache>标签,如果不配置,即使二级缓存总开关是打开的,也不会走二级缓存.
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
           size="1024"
           eviction="LRU"
           flushInterval="120000"
           readOnly="false"/>
开启二级缓存后,如果针对某一个方法不用二级缓存,可以单独配置 useCache="false" :
<select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" useCache="false" > ...</select>

注意:useCache 是否启用二级缓存;flushCache是否清空二级缓存

flushCache="true" 是否清空二级缓存:  增删改语句默认为true,查询语句默认是false,如果查询语句想要每次都清空缓存可以显示配置flushCache="true":
<select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" flushCache="true" >

2)测试

@Test
public void testCache() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    try {

        BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
        System.out.println("第一次查询~~~~~~~");
        System.out.println(mapper1.selectBlogById(1));
        // 事务不提交的情况下,二级缓存会写入吗?
        session1.commit();

        BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
        System.out.println("第二次查询~~~~~~~");
        System.out.println(mapper2.selectBlogById(1));
    } finally {
        session1.close();
    }
}
注意:如果把session1.commit();注释了,就不走缓存了,是因为查询的结果会先到待提交的队列中,并不会真正写入到cache中。
二级缓存是和事务绑定在一起的。

image.png

3)二级缓存是和事务关联在一起的

在CachingExecutor中:
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
Cache cache = ms.getCache();
// cache 对象是在哪里创建的?  XMLMapperBuilder类 xmlconfigurationElement()
// 由 <cache> 标签决定
if (cache != null) {
  // flushCache="true" 清空一级二级缓存 >>
  flushCacheIfRequired(ms);
  if (ms.isUseCache() && resultHandler == null) {
    ensureNoOutParams(ms, boundSql);
    // 获取二级缓存
    // 缓存通过 TransactionalCacheManager、TransactionalCache 管理
    @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); // issue #578 and #116
    }
    return list;
  }
}
// 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

public class TransactionalCacheManager {

    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
    ...
}
public class TransactionalCache implements Cache {

     public void commit() {
        if (clearOnCommit) {
          delegate.clear();
        }
        // 真正写入二级缓存
        flushPendingEntries();
        reset();
      }
}
* 为啥二级缓存要个事务绑定在一块呢?
其实就是怕回滚,查询了之后,马上写入缓存,如果后面的commit发生问题,就是数据库没数值,但是缓存里有值,就出现了问题。
* 一级缓存为啥不用和事务绑定呢?
因为一级缓存中如果出现问题,事务回滚了,会话结束了,缓存本身就没有了,不存在跨会话的问题。

4)被其他会话中执行了更新操作,二级缓存会被清空

@Test
public void testCacheInvalid() throws IOException {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession session1 = sqlSessionFactory.openSession();
    SqlSession session2 = sqlSessionFactory.openSession();
    SqlSession session3 = sqlSessionFactory.openSession();
    try {
        BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
        BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
        BlogMapper mapper3 = session3.getMapper(BlogMapper.class);
        System.out.println("第一次查询~~~~~~~");
        System.out.println(mapper1.selectBlogById(1));//session1
        session1.commit();

        // 是否命中二级缓存
        System.out.println("第二次查询~~~~~~~");
        System.out.println(mapper2.selectBlogById(1));//session2

        Blog blog = new Blog();
        blog.setBid(1);
        blog.setName("2019年1月6日15:03:38");
        mapper3.updateByPrimaryKey(blog);
        session3.commit();//session3

        System.out.println("第三次查询~~~~~~~");
        // 在其他会话中执行了更新操作,二级缓存是否被清空?
        System.out.println(mapper2.selectBlogById(1));//session2

    } finally {
        session1.close();
        session2.close();
        session3.close();
    }
}

image.png image.png