缓存
在MyBatis中缓存是一个很重要的概念,缓存的存在可以提升我们的查询性能(非SQL性能),减少没有必要的SQL编译和结果的查询,缓存又分为两种,一级缓存、二级缓存。这两种缓存有不同的作用,各司其职,下面我就详细介绍一下吧 !
一、一级缓存
- 作用域:一级缓存的作用域默认为SqlSession共享,只要操作是在同一个SqlSession内的就可以共享一级缓存
- 缓存命中必要条件&命中场景
- 相同的
SqlSession
- 相同的
StatementId
也就是相同的Mapper接口 - 相同的
SQL
语句和参数
- 相同的
RowBounds
返回行数
- 相同的
- 缓存何时会清空&&失效
- 调用
sqlSession
的close()
方法 - 未配置刷新缓存
flushCache=true
- 未执行Update操作
- 缓存的作用域不是
STATEMENT
- 调用
一级缓存执行流程&调用过程图
一级缓存的实现逻辑
之前在 Executor
文章里说过,一级缓存是在 BaseExecutor
内部进行实现的,所有我将对 BaseExecutor
内部代码进行刨析 !
具体的执行流程可参考上述第二张图的调用流程
java
/**
* 执行器的查询方法
*/
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取动态sql
BoundSql boundSql = ms.getBoundSql(parameter);
// 获取缓存信息描述对象 内部会有 statementId、分页参数、sql 这些参数
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 内部重载的查询方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
query方法
java
/**
* 缓存类、内部很简单只有一个HashMap用来缓存数据
* key就是缓存的元数据、value就是缓存的泛型集合
*/
protected PerpetualCache localCache;
@Override
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.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
// 最终的反参集合
List<E> list;
try {
// 用于实现嵌套子查询
queryStack++;
// 从缓存中获取泛型对象集合
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();
// 如果配置的缓存作用域为 Statement的话 则会清除缓存、可用于关闭一级缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// 清除缓存的操作
clearLocalCache();
}
}
return list;
}
PerpetualCache
一级缓存类
里边就一些 get、set、remove、size 操作 没什么好解释的
java
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
}
小知识:cache
属性为什么不是 ConcurrentHashMap
? 因为SqlSession本身就不是线程安全的,所以即便是ConcurrentHashMap
也没啥意义
首先在DefaultSqlSession
的源码中明确说了不是线程安全的:
java
/**
*
* The default implementation for {@link SqlSession}.
* Note that this class is not Thread-Safe.
*
* @author Clinton Begin
*/
我的理解主要是两方面:
- 首先由于
JDBC
的`Connection对象本身不是线程安全的,而session中又只有一个connection,所以不是线程安全的 - 一级缓存 由于一级缓存是session级别的,所以如果多个线程同时使用session,当线程A进行了插入操作未完成,但是此时线程B进行查询并缓存了数据,这是就出现了一级缓存与数据库数据不一致的问题。
为什么SqlSession不是线程安全的 转自简书 作者: 没有花的雪
二、二级缓存
- 作用域:整个应用都可以用,而且可以进行跨线程使用
- 二级缓存的命中率会更高,适合缓存一些修改较少的数据
- 存在的问题:因为作用域是整个应用,所以要控制缓存的大小,以免造成OOM的发生
- 命中条件
- 要开启二级缓存
- 必须要手动提交
sqlSession.commit()
- 和一级缓存一致 (除了一致的SqlSession)
- 二级缓存的扩展性功能
- 存储:可存储在硬盘、内存
- 溢出淘汰机制 FIFO (First In First Out)
- LRU 最近最少使用
- 过期清理
- 序列化
- 线程安全
- 命中率统计
二级缓存执行流程图
二级缓存源码解析
二级缓存的入口 CachingExecutor
类
1、获取缓存
java
/**
* 缓存管理器,全局唯一
*/
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
/**
* 执行查询的方法
*/
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 获取缓存key
Cache cache = ms.getCache();
if (cache != null) {
// 是否刷新缓存
flushCacheIfRequired(ms);
// 开启缓存并且不能手动指定结果集处理器
if (ms.isUseCache() && resultHandler == null) {
// 校验存储过程中是否有OUT返回值 如果有的话就抛个异常出去,提示说需要关闭缓存!
ensureNoOutParams(ms, boundSql);
// 从缓存管理器中获取数据 代码如下
List<E> list = (List<E>) tcm.getObject(cache, key);
// 如果没获取到
if (list == null) {
// 就执行查询操作并添加进二级缓存
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list);
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
再看tcm.getObject(cache, key);
java
/**
* 获取对象
*/
public Object getObject(Cache cache, CacheKey key) {
// getTransactionalCache(cache) 可以理解为 map.get(cache)
return getTransactionalCache(cache).getObject(key);
}
/**
* 最后调用的 getObject(key) 方法
*/
@Override
public Object getObject(Object key) {
// 上边讲到的责任链 会一层一层往下掉用 最终到达 PerpetualCache 中
Object object = delegate.getObject(key);
// 如果得到的是空的话就添加一个空对象、目的是为了防止缓存穿透,会在调用commit方法时提交到真正的二级缓存中去
if (object == null) {
entriesMissedInCache.add(key);
}
// 如果调用了 clear方法则返回空
if (clearOnCommit) {
return null;
}
// 返回结果
else {
return object;
}
}
2、提交事务
java
/**
* 提交事务
*/
@Override
public void commit(boolean required) throws SQLException {
// 调用BaseExecutor的commit 提交事务
delegate.commit(required);
// 提交缓存暂存区的数据至真正的二级缓存 调用TransactionalCacheManager的方法
tcm.commit();
}
/**
* 循环Map中的值 调用TransactionalCache的commit()方法
* 这个map就是缓存的暂存区Map
* private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
*/
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
/**
* TransactionalCache的commit()方法
*/
public void commit() {
// 如果调用clear方法 清空二级缓存
if (clearOnCommit) {
delegate.clear();
}
// 提交到二级缓存
flushPendingEntries();
// 清空暂存区
reset();
}
二级缓存有这么多的附加功能 MyBatis是如何实现的 ?
MyBatis在实现这些附加功能的时候采用了设计模式: 装饰者模式+责任链模式
使用责任链模式的好处:只有一个顶级接口,在接口的实现类里一层套一层,层层套娃,某个层不需要关心下一个层需要实现什么,提升了代码的层次感。如图:
此图就是缓存的附加功能实现方式,顺序也是如此 (有些功能是需要开启才可以添加进去的)
源码如下
- 入口
CacheBuilder
java
/**
* 构建一个Cache对象
*
* @return 装饰完成的cache对象
*/
public Cache build() {
// 设置默认的缓存实现 如果为null的话默认的缓存对象就是 PerpetualCache
setDefaultImplementations();
// 创建责任链顶端的Cache对象
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) {
// 设置 LruCache
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 装饰 Cache
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
- 实现 装饰&责任链的主方法
java
/**
* 装饰缓存对象和责任链赋值
*
* @param cache 最中的缓存对象
* @return 装饰后的缓存对象
*/
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 过期清理缓存区
if (clearInterval != null) {
// 过期清理缓存区
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
if (readWrite) {
// 对象序列化
cache = new SerializedCache(cache);
}
// 统计命中率
cache = new LoggingCache(cache);
// 线程同步缓存区
cache = new SynchronizedCache(cache);
// 第一个 简单阻塞
if (blocking) {
cache = new BlockingCache(cache);
}
// 因为是责任链所以要倒着看
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
最终该Cache对象会存放于 MappedStatement
中 供其他用到缓存的地方使用
为什么二级缓存虚需要提交之后才可以命中
tex
因为二级缓存是跨线程使用的,但是在数据库中两个不同的事务是互相不可见的,正因为有了二级缓存这个东西就导致了他们是可见的,如果不提交就可以命中缓存的话,就可能导致脏读的情况发生 !