MyBatis的一级缓存与二级缓存
一、什么是缓存
要理解MyBatis的一级缓存,至少,需要明白什么是缓存。如图:
对于JDBC操作,如果需要连续请求id=1的用户数据,那么就需要进行两次的数据库连接,获取数据库中的数据。相同的数据,进行两次数据库连接,这肯定会造成资源的浪费。基于面向对象,可以把第一次获取的数据保存到一个对象中,下一次直接从对象中获取就行了,也就是下面这个样子:
获取的内容保存在对象中,在一个请求期间,直接使用或者传递对象就可以了。对于JDBC的操作,可以自己定义类或者集合来保存数据库中的数据,来避免连续请求数据库的问题。这里用来保存数据的对象或者集合,也能称之为缓存。
但是使用了三层架构之后,Dao层和Dao层之间有可能互相是不清楚的。如果有一个复杂的业务要在Service层中进行处理,需要分别调用不同Dao层中的数据,那这样简单的缓存还是不够看。
这种情况,要再去处理缓存问题,就会花费过多的精力,得不偿失。在这种层面上的缓存处理MyBatis框架已经做好了,就叫做一级缓存。
MyBatis的一级缓存就是基于数据库会话(SqlSession)的。
二、MyBatis的主要层次结构
使用MyBatis对数据库操作的代码,能够看见的就是这个SqlSession对象。实际上,这只是MyBatis对外暴露的接口,整个MyBatis核心部件是下面的这么一堆接口和类:
1️⃣SqlSession:MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能。
2️⃣Executor:MyBatis执行器,整个MyBatis调度的核心,负责SQL语句的生成和查询缓存的维护。
3️⃣StatementHandler:封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
4️⃣ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所需要的参数。
5️⃣ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。
6️⃣TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换。
7️⃣MappedStatement:MappedStatement维护了一条节点的封装。
8️⃣SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
9️⃣BoundSql:表示动态生成的SQL语句以及相应的参数信息。
1️⃣0️⃣Configuration:MyBatis所有的配置信息都维持在Configuration对象之中。
上面这堆接口和类的层次关系是大概是下面这个样子的:
MyBatis对外暴露的接口是SqlSession,而最重要的是Executor接口。Executor的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,如下:
而在PerpetualCache中则有一个HashMap属性:
总结就是:
MyBatis封装了JDBC操作,对外暴露了SqlSession接口进行数据库的操作。但是实际MyBatis最核心的接口是Executor,它负责SQL语句的生成和查询缓存的维护。如果没有缓存就查数据库,有缓存就使用的是PerpetualCache中的HashMap保存的数据缓存。MyBatis的一级缓存其实就保存在一个HashMap中。那么HashMap中又是怎么判断查询方法是否相同了呢?其实主要是通过HashMap的key值。
BaseExecutor.java:
...
public CacheKey createCacheKey(MappedStatement ms,
Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
Iterator var8 = parameterMappings.iterator();
while(var8.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var8.next();
if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value;
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (this.configuration.getEnvironment() != null) {
cacheKey.update(this.configuration.getEnvironment().getId());
}
return cacheKey;
}
}
...
从代码中可以看出,如果下面条件一样,就可以判断为两个查询相同:
1️⃣statementId
2️⃣RowBounds的offset、limit的结果集分页属性;
3️⃣SQL语句;
4️⃣传给JDBC的参数值
三、MyBatis的一级缓存
1️⃣一级缓存最简单的组织形式
MyBatis会在一次会话的表示——一个SqlSession对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
类似于最开始保存的方式,只是从一个简单的对象,换成了封装好了的更加复杂的Local Cache对象。
实际上,SqlSession只是一个MyBatis对外的接口,SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装在Cache接口中。它们之间的组织关系,大概如下图:
2️⃣一级缓存的生命周期
MyBatis默认打开一级缓存,不需要做任何设置,直接就可以用。一级缓存的生命周期:
- MyBatis在开启一个数据库会话时,会创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象(Cache接口的实现类);当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用。
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是SqlSession对象仍可使用。
- SqlSession中执行了任何一个增删改操作(update()、delete()、insert())之后执行事务提交commit() ,都会清空PerpetualCache对象的数据,但是SqlSession对象可以继续使用。
四、MyBatis的二级缓存
一级缓存是基于SqlSession对象的,也就是一次数据库会话期间。而二级缓存是则是基于全局的。二级缓存的存在形式如图:
1️⃣二级缓存使用场景
类似于这种统计排行榜这种的查询,可能会涉及到很多张表很多字段的查询统计排序,是非常费时费力的。如果每次都需要去数据库查询显示一次这个排行榜数据,那到查询排行榜这里,必定会卡顿很久,而且这种卡顿是用户不能忍受的。做成一级缓存也是不可行的,每次SqlSession请求,每个客户上来难道都要卡顿一次吗?所以,这种查询肯定要做成全局的缓存,当应用启动的时候就缓存这种查询数据,然后每一周刷新一次这种数据就可以了。
由此,简单总结二级缓存的特点和使用场景:二级缓存作用于全局,对于一些相当消耗性能的,并且对于时效性不明感的查询可以使用二级缓存。而且注意,如果开启了二级缓存,查询的顺序是二级缓存 → 一级缓存 → 数据库
。
2️⃣MyBatis二级缓存的配置
在MyBatis中使用二级缓存就必须要进行配置了,必须要有下面的步骤才能正常使用二级缓存:
- 在全局设置中开启二级缓存
<settings>...<setting name="cacheEnabled" value="true"/>...</settings>
- 在XXXMapper.xml中开启<cache>标签
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache>
可以简写为<cache/>
这样就表示在Mapper.xml中开启二级缓存了,因为<cache>标签的每个属性都有默认值。cache标签属性:
eviction:缓存回收策略,这个属性又有下面几个值
LRU – 最近最少使用的。移除最长时间不被使用的对象。
FIFO – 先进先出。按对象进入缓存的顺序来移除它们。
SOFT – 软引用。移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用。更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认是LRU
flushInterval:刷新间隔,可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size:引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readOnly:只读属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
- 相关实体类需要序列化
放入二级缓存中保存的JavaBean需要实现Serializable接口。
- useCache和flushCache
这一步不是必须的。这两个都是属于查询标签<select>的属性
userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。
flushCache属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
3️⃣使用二级缓存示例