文章涉及的代码都将统一存放到此仓库,文章涉及的案例代码存放在包: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 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中实现了查询缓存(一级缓存),逻辑如下所示:
publicList query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 省略无关代码.... List list; try { queryStack++; /// 1. 尝试从缓存中获取结果 list = resultHandler == null ? (List ) 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配置文件中设置如下属性:
完成上述设置后,执行两次相同查询,将会执行两次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配置文件中设置属性进行开启和关闭:
Mybatis二级缓存与命名空间绑定,如果需要开启特定命名空间下的耳机缓存,需要在Mapper xml文件中配置cache元素:
缓存有许多配置属性:
- type,Cache实现类,默认使用PerpetualCache这个基础实现类,其它都是Cache实现类都实现了其它不同功能
- eviction,Cache装饰类,使用委派模式进行组装多个Cache,实现多种功能,默认使用LRU类型Cache,当缓存空间不足时,将会删除最久未使用对象
- flushInterval,刷新时间,表示缓存的有效期,单位时毫秒
- size,表示最大缓存数量,需要注意这里的数量计算方式,一次查询返回的结果计数为1,不论查询返回的记录条数时多少
- readOnly,表示是否序列化存储,如果为false,表示直接存储原始对象,true则需要对存储对象进行序列化处理,默认时true,(吐槽一下mybatis中的写法,!context.getBooleanAttribute("readOnly", false),难道不能直接设置默认值为true,还要皮一下)
- blocking,表示当缓存不存在时是否进行阻塞操作,默认为false,表示不阻塞
如果觉得上述配置麻烦可以只配置
有了上述两个配置,那么可以在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 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文件中设置
- 一级缓存
- 没有开关属性,但是可以设置缓存有效范围
- SESSION,在一个事务中有效
- STATEMENT,在同一个方法中有效
- 生效时机
- 在同一个事务中查询时有效
- 失效时机
- update、insert、delete操作时失效
- 事务提交、回滚时失效
- 没有开关属性,但是可以设置缓存有效范围
- 二级缓存
- 有开关属性,在Mybatis配置文件中设置cacheEnabled属性进行开启和关闭
- 作用于一个命名空间下,需要在Mapper xml配置文件中配置
元素启动缓存 - 生效时机
- 事务提交之后才会生效
- 失效时机
- update、insert、delete操作时失效
- 执行同一个命名空间下配置了flushCache="true"元素SQL,会刷新缓存
技术更新换代速度很快,我们无法在有限时间掌握全部知识,但我们可以在他人的基础上进行快速学习,学习也是枯燥无味的,加入我们学习牛人经验:
QQ:901856121
点击:加群讨论



