MyBatis的一级缓存、二级缓存
一级缓存与二级缓存联系
- 一级缓存的作用域是在SqlSession中,二级缓存的作用域是针对mapper(namespace)做缓存。多个SqlSession去操作同一个Mapper的sql,不管SqlSession 是否相同,只要 mapper 的 namespace 相同就能共享数据。
- 一级缓存是SqlSession级别的缓存,在当次会话中的相同查询会储存进一级缓存,当SqlSession 的会话关闭了(前提要开启了二级缓存),该SqlSession的一级缓存数据就会储存进二级缓存中。
一级缓存
使用 HshMap 进行缓存
key:statementId + rowBounds + SQL + 参数/(把执行的方法和参数通过算法生成)
value:映射出来的 Java 对象
为什么需要一级缓存
- 在应用运行过程中,我们可能在一次数据库会话中,执行多次查询条件完全相同的 sql,MyBatis 就提供了一级缓存的方法来优化这部分场景,如果是相同的 sql 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
一级缓存的配置
- 一级缓存是默认开启的,可以不做操作
- 也可以在Mybatis 配置文件中添加以下语句,改变它的级别
<setting name="localCacheScope" value="SESSION"/>
- value的值
SESSION:在一个 MyBatis 会话中执行的所有语句,都共享这一个缓存
STATEMENT:缓存只对当前执行的这一个 Statement 有效
注意点
- 在修改操作后执行相同查询,就需要重新查询数据库,即一级缓存失效被清空
- 如果是同一个 SqlSession 对象进行多次相同的查询,则直接进入一级缓存
- 如果不是同一个 SqlSession 对象想进行多次相同的查询,但来自同一个 namespace,则进入二级缓存
如果想清空某个sql操作的一级缓存,可以在方法标签内添加 flushCache=“true” 属性,即在查询数据前会清空当前一级缓存。即该方法每次查询都会从数据库中获取数据,从某种程度上来说他可以保证获取的数据都是正确的不会获取错误的数据,但由于清空了一级缓存,所以会影响当前 SqlSession 中所有缓存的查询,因此在需要反复查询获取只读数据的情况下,会增加数据库的查询次数。尽量避免使用。
一级缓存工作流程
每个SqlSession 中持有 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 LocalCache 进行查询,如果缓存命中,就直接返回给用户,如果缓存没有命中的话,就另外查询数据库,将结果写入 LocalCahe,最后将结果返回给用户。
详细流程
一级缓存命中条件
- 相同的 sql 和参数
- 会话(Session)级别缓存,必须是相同的会话
- 必须是相同的方法
- 必须是相同的 namespce (mapper)
一级缓存使用注意点:
- 不能够在查询之前,执行 clearCache(),会清空缓存
- 不能执行 update,delete,insert 操作,同样会清空缓存(数据更新)
- 查询语句中若包含了 flush 也会清空缓存
一级缓存的实现
一级缓存源码主要属性分析
SqlSession:对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是 DefaultSqlSession
Executor:SqlSession 向用户提供操作数据库的方法,但和数据库相关的职责都会委托给 Executor
一级缓存实现流程图
cache 的生命周期与 SqlSession 相联系
BaseExecutor 类源码中有个变量叫 resultHandler,只有当该变量的值为 null,才会尝试走缓存的方法。 list 不为null,即进行存储过程出参,从缓存中获取数据
(LocalOutputParameterCache)
一级缓存的获取流程
1.对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
3. 如果命中,则直接将缓存结果返回;
4. 如果没命中:
- 去数据库中查询数据,得到查询结果;
- 将key和查询到的结果分别作为key,value对存储到Cache中;
- 将查询结果返回;
5.结束。
追加
Cache最核心的实现其实就是一个Map,将本次查询使用的特征值作为key,将查询结果作为value存储到Map中。
现在最核心的问题出现了:怎样来确定一次查询的特征值?
换句话说就是:怎样判断某两次查询是完全相同的查询?
也可以这样说:如何确定Cache中的key值?
MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:
-
传入的 statementId
-
查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
-
这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )
-
传递给java.sql.Statement要设置的参数值
MyBatis认为的完全相同的查询,不是指使用sqlSession查询时传递给算起来Session的所有参数值完完全全相同,你只要保证statementId,rowBounds,最后生成的SQL语句,以及这个SQL语句所需要的参数完全一致就可以了。
一级缓存性能分析
1.MyBatis 对会话(Session)级别的一级缓存设计的比较简单,简单使用 HashMap 来维护,并没有对其容量大小进行限制
- 如果一直使用某一个 SqlSession 对象查询数据,是否会导致 HashMap 太大而导致 java.lang.OutOfMemoryError 错误?
- 一般而言 SqlSession 的生存时间很短,一般情况下使用一个 SqlSession 对象执行的操作不会太多,执行完就会消亡
- 对于某一个 Sqlsession 对象而言,只要执行 update 操作(update、insert、delete),都会将这个 SqlSession 对象中对应的一级缓存清空,所以一般不会出现缓存过大,影响 JVM 内存空间的问题
- 也可以手动地释放掉 SqlSession 对象中的缓存(clearCache()、flush…)
.
2. 一级缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念
MyBatis 的一级缓存使用了简单的 HashMap,MyBatis 只负责将查询数据库的结果存到缓存中去,不会去判断缓存存放的时间是否过长,是否过期,因此也就没有对缓存结果进行更新的说法。
3.注意点
- 对于数据变化频率大,并且需要高时效准确性的数据要求,我们使用 SqlSession 查询的时候,要控制好 SqlSession 的生存时间,SqlSession 的生存时间越长,他缓存的数据可能越旧,从而造成和真实数据库的误差;同时对于这种情况用户可以手动适当的情况SqlSession中的缓存
- 对于只执行、并且频繁执行大范围的 select 操作的 SqlSession 对象,SqlSession 对象的生存时间不宜过长
一级缓存小结
- MyBatis 一级缓存的生命周期和 SqlSession 一致
- MyBatis 一级缓存内部设计简单,只是一个没有容量限定的 HashMap,在缓存的功能性上有所欠缺
- MyBatis 的一级缓存最大范围是 SqlSession 内部,有多个 SqlSession 或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为 Statement
二级缓存
二级缓存是 mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql ,不管 Session 是否相同,只要 mapper 的namespace 相同就能共享数据,也可以称为 namespace 级别的缓存。
为什么需要二级缓存
一级缓存个中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。
二级缓存的配置
- MyBatis 配置文件中开启二级缓存
<settings>
<setting name = "cacheEnable" value = "true">
</settings>
- 在 mapper 映射文件中使用二级缓存,映射XML中配置cache或者 cache-ref
cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置。
<cache/>
cache 相关属性
-
type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
-
eviction: 定义回收的策略,常见的有FIFO,LRU。
-
flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
-
size: 最多缓存对象的个数。默认1024
-
readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
-
blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
<cache-ref namespace="mapper.StudentMapper"/>
- 需要将要存储数据的 pojo 实现 Serializable 接口,为了将数据取出做反序列化操作,因为二级缓存的存储方式众多,可能存储在内存中,也可能存储在磁盘中
如果 mapper 类里有sql代码:为 Mapper 类加上 @CacheNamespce 注解
禁用二级缓存的方法:例如禁用一条select语句使用二级缓存
<select useCache=false>相关操作语句</select>
配置文件设置定时刷新缓存的设置
//size:引用数目,默认1024
<cache flushInterval=#毫秒 eviction=FIFO size readonly>
二级缓存的工作流程
开启二级缓存后,会使用 CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体工作流程如下:
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局变量
当开启二级缓存后,数据的查询执行流程就是:
二级缓存->一级缓存->数据库
二次缓存的原理:通过序列化拷贝缓存对象
所以实体类 pojo 需要实现序列化接口
二级缓存的命中条件
- 为 Mapper 类加上 @CacheNamespace注解
- 在 put 缓存之前必须关闭会话(填充缓存)
即当两个会话想访问同一个缓存,其中一个需要将会话关闭(slqSession.close(),执行修改操作那个)
原因:数据库的不可重复读、隔离性(比如事务a修改后写入缓存此时事务a未结束,事务b去读取缓存且读取到事务a修改后的数据,这时!事务a突然回滚了事务,那么事务b刚才读取到的数据就不是真正的数据了。所以事务b应该在事务a相关操作完成后,关闭会话后再去读事务a真正写入缓存的真正的数据) - 必须是相同的方法
- 必须是相同的 namespace
- 不能够在查询之前执行 clearCache()
- 不能执行任何的 update,delete,insert操作
二级缓存小结
- 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨 SqlSession的。
- 二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,第一次执行完毕会将数据库中查询到的数据写入缓存(内存),第二次会从缓存中获取数据,将不再从数据库中查询(除非缓存被清空),从而提高查询效率
- MyBatis 默认没有开启二级缓存,需要在 setting 全局参数中开启二级缓存
如果缓存中有数据就不用从数据库中获取,大大提高系统性能。