大纲
- mybatis 缓存的使用
- mybatis 缓存实现类
- mybatis 一级缓存实现原理
- mybatis 二级缓存实现原理
1 mybatis 缓存的使用
- mybatis 一级缓存介绍
Mybatis缓存分为一级缓存和二级缓存,一级缓存默认开启而且无法关闭, 一级缓存只能控制缓存级别,不能关闭.
Mybatis 提供了一个配置参数 localCacheScope,用于控制一级缓存的级别.默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。
- 一级缓存的生命周期有多长?
1 MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;
当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是该对象仍可使用;
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据,但是该对象可以继续使用;
- mybatis 二级缓存的使用 ,可参考官方文档: mybatis – MyBatis 3 | XML 映射器
1 设置 cacheEnabled 为 true
2 在 Mapper.xml 中配置 <cache/> 即可
3 通过 useCache 属性指定执行 sql 时是否使用缓存;f使用lushCache属性清除缓存
<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
基本上就是这样。<cache/>语句的效果如下:
映射语句文件中的所有 select 语句的结果将会被缓存。
映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
缓存不会定时进行刷新(也就是说,没有刷新间隔)。
缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。
如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。
实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。
CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。
参考文档: Mybatis缓存 - Lucky小黄人^_^ - 博客园
2 mybatis 缓存实现类
- mybatis 通过 Cache 接口定义缓存对象的行为
/**
* SPI for cache providers.
*/
public interface Cache {
//用于获取缓存 id,通常缓存 id 为 Mapper 的命名空间名称
String getId();
//将一个 java 对象添加到缓存中
void putObject(Object key, Object value);
//通过缓存 key 获取缓存的对象
Object getObject(Object key);
Object removeObject(Object key);
//清空缓存
void clear();
int getSize();
}
- Cache 接口 有一个基本实现类 PerpetualCache,通过HashMap 缓存对象
LoggingCache 为缓存增加日志输出功能 ,记录缓存请求和命中次数
LruCache 使用LRU 淘汰算法,淘汰最近使用最少的 缓存 key 和 value
ScheduledCache 如果当前时间与上次清空缓存的时间间隔大于指定时间间隔,则清空缓存
BlockingCache:能够保证 同一时间 只有一个线程 到缓存中 查找 指定的Key对应的数据
...
Cache cache = new PerpetualCache("default");
cache = new LruCache(cache);
cache = new FifoCache(cache);
cache = new SoftCache(cache);
cache = new WeakCache(cache);
cache = new ScheduledCache(cache);
cache = new SerializedCache(cache);
// cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
cache = new TransactionalCache(cache);
for (int i = 0; i < 10000; i++) {
cache.putObject(i, i);
((TransactionalCache) cache).commit();
Object o = cache.getObject(i);
assertTrue(o == null || i == ((Integer) o));
}
assertTrue(cache.getSize() < N);
// 另外,MyBatis提供了一个CacheBuilder类,通过生成器模式创建缓存对象
nitializingCache cache =CacheBuilder("test")
.implementation(PerpetualCache.class)
.addDecorator(LruCache.class)
.build());
3 mybatis 一级缓存实现原理
- 一级缓存的逻辑在 BaseExecutor 中完成,一级缓存通过PerpetualCache实现
public abstract class BaseExecutor implements Executor {
//mybatis 一级缓存对象
protected PerpetualCache localCache;
//存储过程输出参数缓存
protected PerpetualCache localOutputParameterCache;
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 如果需要刷新缓存(默认DML需要刷新,也可以语句层面修改), 且queryStack=0 (应该是用于嵌套查询的场景)
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 如果查询不需要应用结果处理器,则先从缓存获取,这样可以避免数据库查询。我们后面会分析到localCache是什么时候被设置进去的
//这里是一级缓存 默认开启的
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//若是缓存中查询不到,则从数据库中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
}
4 mybatis 二级缓存实现原理
- mybatis 二级缓存默认是关闭的,通过设置 cacheEnabled 为 true 开启
- Executor接口有多个实现: SimpleExecutor,ReuseExecutor,BatchExecutor 另外还有一个比较特殊的CachingExecutor ,使用装饰器模式,在其他 executor 基础上增加了二级缓存功能
- Configuration类提供了一个工厂方法 newExecutor() 创建 executor 对象
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
//根据executorType创建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);
}
if (cacheEnabled) {
// 如果启用了缓存,则使用CachingExecutor对executor进行装饰。
executor = new CachingExecutor(executor);
}
// 责任链模式 executor 作为入参,为每个拦截器生成代理executor对象
// 后面的代理executor,持有前面代理executor的引用,返回最终的代理executor
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
- CachingExecutor二级缓存查询实现逻辑
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
//499085486:2272766813:cn.javabus.mapper.UserMapper.getUser:0:2147483647:select * from user where id = ?:1:development
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 读取MappedStatement 对象中维护的二级缓存对象
Cache cache = ms.getCache();
if (cache != null) {
//判断是否需要刷新缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
//从缓存中读取数据
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//二级缓存不存在则走BaseExecutor对应实现类
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将数据缓存到MappedStatement对象对应的二级缓存中
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//没有缓存数据,委托任务给 BaseExecutor对应实现类
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
- 总结
1 缓存时 mybatis 中比较重要的特性
2 Mybatis缓存分为一级缓存和二级缓存,一级缓存默认开启而且无法关闭, 一级缓存只能控制缓存级别,不能关闭.
3 mybatis 二级缓存默认是关闭的,通过设置 cacheEnabled 为 true 开启,通过CachingExecutor实现二级缓存