一、简述
MyBatis 的一级缓存是基于数据库会话(SqlSession 对象)的,默认开启。二级缓存是基于全局(nameSpace)的,开启需要配置。
对于 JDBC 操作,如果需要连续请求 id=1 的用户数据,那么就要进行两次的数据库连接,获取数据库中的数据。相同的数据,进行两次数据库连接,这肯定会造成资源的浪费。基于面向对象,可以把第一次获取的数据保存到一个对象中,下一次直接从对象中获取就行了,如图:
获取的内容保存在对象中,在一个请求期间,直接使用或者传递对象就可以了。对于 JDBC 的操作,可以自己定义类或者集合来保存数据库中的数据,来避免连续请求数据库的问题。这里用来保存数据的对象或者集合,也能称之为缓存。
但是使用了三层架构之后,Dao 层和 Dao 层之间有可能互相是不清楚的。如果有一个复杂的业务要在 Service 层中进行处理,需要分别调用不同 Dao 层中的数据,那这样简单的缓存还是不够看。此时,要再去处理缓存问题,就会花费过多的精力,得不偿失。在这种层面上的缓存处理 MyBatis 框架已经做好了,就叫做一级缓存。
二、MyBatis 的主要层次结构
使用 MyBatis 对数据库操作的代码,能够看见的就是这个 SqlSession 对象。实际上,这只是 MyBatis 对外暴露的接口,整个 MyBatis 核心部件是下面的这么一堆接口和类:
1️⃣SqlSession:MyBatis 工作的主要顶层 API,表示和数据库交互的会话,完成必要数据库增删改查功能。
2️⃣Executor:MyBatis 执行器,整个 MyBatis 调度的核心,负责 QL 语句的生成和查询缓存的维护。
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 对象之中。
PerpetualCache 中则有一个 HashMap 属性:
总结:
MyBatis 封装了 JDBC 操作,对外暴露了 SqlSession 接口进行数据库的操作。但是实际 MyBatis 最核心的接口是 Executor,它负责 SQL 语句的生成和查询缓存的维护。如果没有缓存就查数据库,有缓存就使用的是 PerpetualCache 中的 HashMap 保存的数据缓存。MyBatis 的一级缓存其实就保存在一个 HashMap 中。HashMap 如何判断查询方法是否相同?其实主要是通过 HashMap 的 key 值。
BaseExecutor:
...
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 在开启一个数据库会话时,会创建一个新的 SqlSession 对象,SqlSession 对象中会有一个新的 Executor 对象,Executor 对象中持有一个新的 PerpetualCache 对象(Cache 接口的实现类);当会话结束时,SqlSession 对象及其内部的 Executor 对象还有 PerpetualCache 对象也一并释放掉。
- 如果 SqlSession 调用了 close(),会释放掉一级缓存 PerpetualCache 对象,一级缓存将不可用。
- 如果 SqlSession 调用了 clearCache(),会清空 PerpetualCache 对象中的数据,但是 SqlSession 对象仍可使用。
- SqlSession 中执行任何一个增/删/改操作之后执行事务提交 commit() ,都会清空PerpetualCache 对象的数据,但是 SqlSession 对象可以继续使用。
四、MyBatis 的二级缓存
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/>
这样就表示在 xxxMapper.xml 中开启二级缓存了,因为 <cache/> 标签的每个属性都有默认值。cache 标签属性:
eviction:缓存回收策略,这个属性又有下面几个值
LRU – 最近最少使用的。移除最长时间不被使用的对象。
FIFO – 先进先出。按对象进入缓存的顺序来移除它们。
SOFT – 软引用。移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用。更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认是LRU
flushInterval:刷新间隔,可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size:引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readOnly:只读属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
- 相关实体类需要序列化
放入二级缓存中保存的 JavaBean 需要实现 Serializable 接口。序列化的意思就是从内存中的数据传到硬盘中。反序列化意思相反。MyBatis 的二级缓存,实际上就是将数据放进了硬盘文件中去了。
如果要使用 MyBatis 的二级缓存,除了要在需要缓存的 mapper.xml 中开启以外,还需要目标实体类实现序列化的接口。当实体类有父类或级联属性,也必须实现序列化。
- useCache 和 flushCache
这一步不是必须的。这两个都是属于查询标签 <select> 的属性
userCache 是用来设置是否禁用二级缓存的,在 statement 中设置 useCache=false 可以禁用当前 select 语句的二级缓存,即每次查询都会发出 sql 去查询,默认情况是 true,即该 sql 使用二级缓存。
flushCache 属性,默认情况下为 true,即刷新缓存,如果改成 false 则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
五、Mybatis 涉及的设计模式
1️⃣Builder模式,例如 SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder
2️⃣工厂模式,例如 SqlSessionFactory、ObjectFactory、MapperProxyFactory
3️⃣单例模式,例如 ErrorContext 和 LogFactory
4️⃣代理模式,Mybatis 实现的核心,比如 MapperProxy、ConnectionLogger,用的 jdk 的动态代理;还有 executor.loader 包使用了 cglib 或者 javassist 达到延迟加载的效果
5️⃣组合模式,例如 SqlNode 和各个子类 ChooseSqlNode 等
6️⃣模板方法模式,例如 BaseExecutor 和 SimpleExecutor,还有 BaseTypeHandler 和所有的子类例如 IntegerTypeHandler
7️⃣适配器模式,例如 Log 的 Mybatis 接口和它对 jdbc、log4j 等各种日志框架的适配实现
8️⃣装饰者模式,例如 Cache 包中的 cache.decorators 子包中等各个装饰者的实现
9️⃣迭代器模式,例如迭代器模式 PropertyTokenizer
六、总结
- 进行 select 后,调用
SqlSession.close()
,会将其一级缓存的数据放进二级缓存中,此时一级缓存随着 SqlSession 的关闭也就不存在了。 - 进行 select 后,调用
SqlSession.commit()
,会将其一级缓存的数据放进二级缓存中,并清空一级缓存。 - 对 SqlSession 执行更新(insert、delete、update)后,同时不调用
SqlSession.commit/SqlSession.close()
,这时只会清空其自身的一级缓存,对二级缓存没有影响。 - 对 SqlSession 执行更新(insert、delete、update)后,执行
SqlSession.commit()
,不仅清空其自身的一级缓存(执行更新操作的结果),也清空二级缓存(执行 commit() 的效果)。 - 对 SqlSession 执行更新(insert、delete、update)后,执行
SqlSession.close()
(没有执行 SqlSession.commit()),需分两类情况。当 autoCommit 为 false 时,只会清空其自身的一级缓存(执行更新操作的效果),对二级缓存没有影响。当 autoCommit 为 true 时,会清空二级缓存。