一般提到MyBatis缓存的时候,都是指二级缓存
。一级缓存(也叫本地缓存)默认会启用,并且不能控制,因此很少会提到。
SqlSession
: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession。
Executor
: SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。Executor有两个实现类,和一级缓存关联的是BaseExecutor。
BaseExecutor
: BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。
PerpetualCache
:对Cache接口最基本实现,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。
1、一级缓存
1.1、原理
MyBatis的一级缓存存在于SqlSession的生命周期中
,在同一个SqlSession中查询时,MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map对象中(没有容量限定的HashMap)
。如果同一个SqlSession中执行的方法和参数完全一致,那么通过算法会生成相同的键值,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。
1.2、源码分析
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
//如果启用二级缓存,使用CahingExecutor装饰类
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
flushCache = true
会在执行查询操作的时候清空以及缓存,一旦这样设置,会影响一个SqlSession
中的缓存查询,增加数据库的查询量。
同一个SqlSwssion生命周期内,任何
DELETE
、UPDATE
、INSERT
操作都会清空一级缓存,其他SqlSwssion则不会,所以容易产生脏数据。
mybatis和spring整合后进行mapper代理开发,不支持一级缓存。
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int delete(String statement) {
return update(statement, null);
}
1.3、失效情况
一级缓存以下情况会失效:
- 一个SqlSession生命周期内,相同条件查询之间执行了update、delete、insert操作;
- 一个SqlSession生命周期内查询条件不一致;
- 不同的SqlSession生命周期;
- 手动刷新。
//手动刷新用户一级缓存,导致用户一级缓存原有的内容消失掉 session.clearCache();
2、二级缓存
2.1、原理
MyBatis的二级缓存非常强大,它不同于一级缓存只存在于SqlSession的生命周期中,而是可以理解为存在于SqlSessionFactory的生命周期中
。默认是关闭的,开启后数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库
默认二级缓存有如下效果:
- 映射文件中的所有SELEST语句将会被缓存;
- 映射语句文件中的所有INSERT、UPDATE、DELETE语句会刷新缓存;
- 缓存会使用
LeastRecentlyUsed(LRU,最近最少使用的)
算法来收回;- 根据时间表(如noFlushInterval,没有刷新间隔),缓存不会以任何时间顺序来刷新;
- 缓存会存储集合或对象(无论查询方法返回什么类型的值)的
1024个引用
;- 缓存会被视为
read/write(可读/可写)
的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改.
2.2、配置
<settings>
<!--开启二级缓存,默认值是true-->
<setting name="cacheEnabled" value="true"/>
</settings>
在对应的Mapper文件中添加cache标签
<mapper namespace="com.demo.mapper.UserMapper">
<cache type="PerpetualCache"
blocking="false"
eviction="FIFO"
flushInterval="60000"
readOnly="true"
size="512"></cache>
<cache-ref namespace="userMapper"/>
<select id="selectAll" resultType="User">
select id,password,user_name,email,mobile_phone,user_id from t_user
</select>
</mapper>
cache标签属性
type
:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
eviction
: 定义回收的策略,常见的有FIFO(进入缓存的顺序进行移除)
,LRU(最近最少使用,默认值)
,SOFT(软引用)
,WEAK(弱引用)
。
flushInterval
: 配置一定时间自动刷新缓存,单位是毫秒
,默认不设置即没有刷新间隔,仅在调用语句的时候刷新。
size
: 最多缓存对象的个数,默认1024
。
readOnly
: 是否只读,若配置可读写,则需要对应的实体类能够序列化,默认false
。
blocking
: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。
cache-ref
代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
如果xml与接口同时配置二级缓存,接口使用注解@CacheNamespaceRef
,指定的命名空间必须与xml中配置的cache-ref
指定的值保持一致。
2.3、源码分析
MyBatis在为SqlSession对象创建Excutor对象时候,会给Executor对象加上一个装饰者
:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在二级缓存中是否有缓存,如果有则直接返回缓存结果;如果没有再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后再返回给用户。
以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。
SynchronizedCache
: 同步Cache,实现比较简单,直接使用synchronized修饰方法。
LoggingCache
: 日志功能,装饰类,用于记录缓存的命中率
,如果开启了DEBUG模式,则会输出命中率日志。
SerializedCache
: 序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
LruCache
: 采用了Lru算法的Cache实现,移除最近最少使用的key/value。
PerpetualCache
: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。
可以为每一句SQL指定是否刷新缓存
<select id="selectAll" resultType="User" flushCache="false">
select id,password,user_name,email,mobile_phone,user_id from t_user
</select>
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//从MappedStatement中获得在配置初始化时赋予的Cache
Cache cache = ms.getCache();
if (cache != null) {
//获取Mapper语句的flushCache配置 判断是否刷新缓存
flushCacheIfRequired(ms);
//ensureNoOutParams主要是用来处理存储过程的
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
//从TransactionalCacheManager获取缓存的列表
//在getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。
List<E> list = (List<E>) tcm.getObject(cache, key);
//如果缓存中没有找到则调用实际的Executor执行查询语句,查询到数据,则调用 tcm.putObject方法,往缓存中放入值
//tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
2.4、失效情况
- 非同一SqlSessionFactory的数据并非同一二级获取,
除非适用Redis这样的缓存数据库
;- 二级缓存未开启;
- 超过过期时间
flushInterval
;
2.5、适用场景
- 以
查询为主
的应用中,只有尽可能少的增、删、改操作。- 绝大多数以
单表操作
存在时,由于很少存在互相关联的情况,因此不会出现脏数据。- 可以按业务划分对表进行分组时,如关联的表比较少,可以通过
参照缓存
进行配置。
参考资料:
MyBatis|缓存机制