三、Mybatis学习实践-缓存机制

仓库

文章涉及的代码都将统一存放到此仓库,文章涉及的案例代码存放在包:com.hzchendou.blog.demo.example下

代码地址:Gitee

分支:lesson3

涉及案例:Case3

简介

Mybatis是一款优秀ORM框架,开发者只需要关注SQL,处理记录结果映射规则,同时Mybatis提供了许多功能特性帮助开发者编写高性能程序,本文将介绍Mybatis缓存机制,了解缓存原理。

缓存

Mybtais提供了一级和二级缓存,两个缓存的作用范围是不用的,但都作用于连接会话生命周期内,也就是SqlSession,同时需要明白缓存是针对查询语句的,这也可以理解,其它三个操作(update、insert、delete)都是更新数据库,因此无法缓存,下面我们分别来探讨一下两个有什么不同

一级缓存

首先我们执行以下程序:

private void cacheWithLevel1() {
    /// 使用Java 7提供的 try-with-resource写法,让JVM自动完成资源关闭操作
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
     获取Mapper接口示例(底层使用了JDK的Proxy.newProxyInstance方法创建代理)
    BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
    /// 直接查询
    List<BlogDO> blogs = blogMapper.selectAll();
    log.info("查询博客文章列表信息, {}", blogs);
    blogs = blogMapper.selectAll();
    log.info("查询博客文章列表信息, {}", blogs);
  }
}

输出结果如下:

18:18:10.922 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
18:18:30.369 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 270095066.
18:18:30.370 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [org.sqlite.SQLiteConnection@101952da]
///1.执行SQL语句进行查询操作
18:18:30.413 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==>  Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog
18:18:32.446 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==> Parameters: 
18:18:33.744 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - <==      Total: 4
/// 查询结果1
18:18:35.986 [main] INFO com.hzchendou.blog.demo.example.Case3 - 查询博客文章列表信息, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
/// 查询结果2
18:18:37.041 [main] INFO com.hzchendou.blog.demo.example.Case3 - 查询博客文章列表信息, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
18:18:37.042 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [org.sqlite.SQLiteConnection@101952da]
18:18:37.042 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [org.sqlite.SQLiteConnection@101952da]
18:18:37.042 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 270095066 to pool.

从日志中可以明显看出来,程序中的两次SQL查询实际只执行了一次,第二次查询直接返回了查询结果2没有执行SQL查询,这是一级缓存生效结果。

一级缓存默认开启,可以在配置文件中配置缓存范围

  • STATEMENT,缓存只存在调用方法过程中,方法执行完成就会释放缓存(这里的方法指的是BaseExecutor.query),可以认为不会缓存,只有在极端情况下,例如多线程执行,并且使用的是同一个SqlSession的情况下才可能出现直接使用缓存

  • SESSION, 默认配置,表示在使用同一个SqlSession,同一个事务中可以重复利用缓存,事务的提交、回滚,Sqlsession关闭,以及执行Update、Delete、Insert操作都会清除缓存

可以认为STATEMENT的作用范围比SESSION的作用范围小,通过配置可以限制缓存使用范围

在Mybatis中所有的SQL最终都是交由Executor组件来执行的,在Executor的子类BaseExecutor中实现了查询缓存(一级缓存),逻辑如下所示:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
     省略无关代码....
    List<E> list;
    try {
      queryStack++;
      /// 1. 尝试从缓存中获取结果
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
       /// 2.这里会将查询的结果缓存到localCache中
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      /// 3.如果配置本地缓存有效范围为STATEMENT时,清除缓存内容
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

这里需要关注两点内容:

  • 缓存key组成
    • 由sql语句id(命名空间+sqlid),rowBounds(查询范围,由offset和limiit组成),sql语句,查询参数组成
    • 只有执行相同SQL语句同时查询参数一致时缓存key才相同
  • 清除缓存时机
    • 当LocalCacheScope为STATEMENT时会立即清除缓存
    • 当事务提交、事务回滚时都会清除缓存
    • 执行更新操作时会清除缓存
    • 关闭session时会清除缓存

一级缓存是默认开启的,如果想要关闭可以在Mybatis配置文件中设置如下属性:

<configuration>
  <!-- 省略不相关配置信息 -->
  <!-- 设置信息 -->
  <settings>
    <!-- 设置缓存范围为STATEMENT时将不会执行一级缓存-->
    <setting name="localCacheScope" value="STATEMENT"/>
  </settings>
</configuration>

完成上述设置后,执行两次相同查询,将会执行两次SQL查询操作,结果如下:

00:05:45.139 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
00:05:45.144 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
00:05:45.144 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
00:05:45.144 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
00:05:45.144 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
00:05:45.236 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
00:05:45.784 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1316061703.
00:05:45.784 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [org.sqlite.SQLiteConnection@4e718207]
/// 第一次SQL查询
00:05:45.787 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==>  Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog
00:05:45.810 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==> Parameters: 
00:05:45.820 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - <==      Total: 4
00:05:45.825 [main] INFO com.hzchendou.blog.demo.example.Case3 - 查询博客文章列表信息, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
/// 第二次SQL查询
00:05:45.826 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==>  Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog
00:05:45.826 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==> Parameters: 
00:05:45.828 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - <==      Total: 4
00:05:45.829 [main] INFO com.hzchendou.blog.demo.example.Case3 - 查询博客文章列表信息, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
00:05:45.829 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [org.sqlite.SQLiteConnection@4e718207]
00:05:45.829 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [org.sqlite.SQLiteConnection@4e718207]
00:05:45.829 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1316061703 to pool.

通过上述日志可以知道一级缓存没有生效,同时可以自行尝试事务提交和回滚操作是否会触发清除缓存操作

需要注意的是一级缓存没有数量限制,因此需要注意缓存大小,防止缓存内容过大导致GC问题

二级缓存

一级缓存作用范围有限(可以认为一级缓存有效范围是在一个事务内),作为补充,Mybatis提供了可选的二级缓存,二级缓存与命名空间绑定,因此作为范围是Mybatis生命周期内,二级缓存默认是开启状态,可以mybatis配置文件中设置属性进行开启和关闭:

<configuration>
  <!-- 此处省略无关配置 -->
  <!-- 设置信息 -->
  <settings>
    <!-- 二级缓存开关属性:true表示开启,false表示关闭,默认开启 -->
    <setting name="cacheEnabled" value="true"/>
  </settings>
   <!-- 此处省略无关配置 -->
</configuration>

Mybatis二级缓存与命名空间绑定,如果需要开启特定命名空间下的耳机缓存,需要在Mapper xml文件中配置cache元素:

<mapper namespace="com.hzchendou.blog.demo.mapper.BlogMapper">
  <!-- 省略无关配置-->
  <cache type="PERPETUAL" eviction="LRU" flushInterval="3600000" size="1024" readOnly="false" blocking="false"/>
  <!-- 省略无关配置-->
</mapper>

缓存有许多配置属性:

  • type,Cache实现类,默认使用PerpetualCache这个基础实现类,其它都是Cache实现类都实现了其它不同功能
  • eviction,Cache装饰类,使用委派模式进行组装多个Cache,实现多种功能,默认使用LRU类型Cache,当缓存空间不足时,将会删除最久未使用对象
  • flushInterval,刷新时间,表示缓存的有效期,单位时毫秒
  • size,表示最大缓存数量,需要注意这里的数量计算方式,一次查询返回的结果计数为1,不论查询返回的记录条数时多少
  • readOnly,表示是否序列化存储,如果为false,表示直接存储原始对象,true则需要对存储对象进行序列化处理,默认时true,(吐槽一下mybatis中的写法,!context.getBooleanAttribute("readOnly", false),难道不能直接设置默认值为true,还要皮一下)
  • blocking,表示当缓存不存在时是否进行阻塞操作,默认为false,表示不阻塞

如果觉得上述配置麻烦可以只配置<cache/>元素,自动提供默认配置。(需要注意readOnly为true时,需要保证缓存的对象都要实现Serializable接口,否则会抛出序列化异常)

有了上述两个配置,那么可以在BlogMapper中使用二级缓存,需要注意的是二级缓存生效时机,前面我们介绍过一级缓存是在事务提交、回滚之前有效,二级缓存作为补充,会在事务提交之后有效,。

private void cacheWithLevel2() {
    /// 使用Java 7提供的 try-with-resource写法,让JVM自动完成资源关闭操作
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
         获取Mapper接口示例(底层使用了JDK的Proxy.newProxyInstance方法创建代理)
        BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
        /// 直接查询
        List<BlogDO> blogs = blogMapper.selectAll();
        log.info("查询博客文章列表信息, {}", blogs);
        /// 提交事务,触发二级缓存生效
        sqlSession.commit();
        blogs = blogMapper.selectAll();
        log.info("查询博客文章列表信息, {}", blogs);
    }
}

执行程序,得到结果如下:

11:50:41.081 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper - Cache Hit Ratio [com.hzchendou.blog.demo.mapper.BlogMapper]: 0.0
11:50:41.085 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
11:50:41.260 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 415186196.
11:50:41.260 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [org.sqlite.SQLiteConnection@18bf3d14]
 1、执行SQL查询操作
11:50:41.263 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==>  Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog
11:50:41.278 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - ==> Parameters: 
11:50:41.288 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectAll - <==      Total: 4
 2、打印结果
11:50:41.290 [main] INFO com.hzchendou.blog.demo.example.Case3 - 查询博客文章列表信息, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
11:50:41.299 [main] WARN org.apache.ibatis.io.SerialFilterChecker - As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
 3、提示缓存命中率,为50%(命中次数 / 查询缓存次数)
11:50:41.301 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper - Cache Hit Ratio [com.hzchendou.blog.demo.mapper.BlogMapper]: 0.5
 4、打印查询结果
11:50:41.301 [main] INFO com.hzchendou.blog.demo.example.Case3 - 查询博客文章列表信息, [BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=3, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=4, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)]
11:50:41.301 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [org.sqlite.SQLiteConnection@18bf3d14]
11:50:41.301 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [org.sqlite.SQLiteConnection@18bf3d14]
11:50:41.301 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 415186196 to pool.

可以看出两次查询实际上只执行了一次SQL查询操作,二级缓存生效了,而且从日志中可以看到缓存命中率为50%,查询了两次操作,命中了一次缓存。

下面我们需要讨论一下缓存失效时机

  • 同一个命名空间下的update、insert、delete操作
  • 执行同一个命名空间下配置了flushCache="true"元素SQL,会刷新缓存

总结

Mybatis提供了一级和二级缓存,一级缓存总是开启状态,但是可以设置缓存有效范围、二级缓存需要配置相关属性,在Mybatis配置文件中设置开启属性,同时需要在Mapper XML文件中设置<cache/>元素

  • 一级缓存
    • 没有开关属性,但是可以设置缓存有效范围
      • SESSION,在一个事务中有效
      • STATEMENT,在同一个方法中有效
    • 生效时机
      • 在同一个事务中查询时有效
    • 失效时机
      • update、insert、delete操作时失效
      • 事务提交、回滚时失效
  • 二级缓存
    • 有开关属性,在Mybatis配置文件中设置cacheEnabled属性进行开启和关闭
    • 作用于一个命名空间下,需要在Mapper xml配置文件中配置<cache/>元素启动缓存
    • 生效时机
      • 事务提交之后才会生效
    • 失效时机
      • update、insert、delete操作时失效
      • 执行同一个命名空间下配置了flushCache="true"元素SQL,会刷新缓存

联系方式

技术更新换代速度很快,我们无法在有限时间掌握全部知识,但我们可以在他人的基础上进行快速学习,学习也是枯燥无味的,加入我们学习牛人经验:

QQ:901856121

点击:加群讨论 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值