一、一级缓存
我们知道mybatis的一级缓存是默认开启的,不需要配置,也没有相应的配置功能去关闭。所以有必要了解一级缓存的使用,以及实现原理,这样我们才能更好的使用其缓存机制。
前文分析知道,mybatis的缓存实现逻辑在Executor中实现的,所以这里我们还是要重新分析一下BaseExecutor中的query方法。
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//如果查询栈为0并且设置了flushCache=true,那么先清理缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
//就是调用map集合的clear方法
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();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
从上可知,查询的时候会先从localCache(PerpetualCache实例)中取是否存在,如果缓存存在直接取值,不存在查询数据库,然后存缓存。还有一个设置就是在configuration配置文件中设置localCacheScope=STATEMENT即可,默认是SESSION,此两种方式可以清楚一级缓存。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//设置占位符
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
//把查到的数据加入到缓存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
所以针对同一个SqlSession和相同的查询语句,会直接使用一级缓存来实现。这里查询和存缓存的时候有两个操作queryStack的自增和自减,以及占位符
queryStack: 查询栈,查询开始的时候会判断当前值是否为0以及flushCache是否为true来清楚缓存,然后进行加一操作,查询完成之后,在finally语句块中进行减一操作,只有当满足queryStack=0的时候才会执行延迟加载和再次判断是否需要清空缓存。这样的设置是为了在单线程环境下,解决嵌套查询下不清除缓存操作,以及不执行延迟加载操作。
占位符: localCache.putObject(key, EXECUTION_PLACEHOLDER)在进行数据库查询之前,先用一个占位符,保存在缓存中,等查询完成之后再更新具体的值。这里设计是为了解决循环嵌套的问题,也就是子查询配置了和主查询一样的查询,那么在进行子查询的时候,就会从缓存中取出这个占位符,那么在转换成List集合时候就会报错,从而退出循环嵌套查询。
以上两个都是在单线程环境下有效,所以在多线程环境下要注意线程安全问题。
二、二级缓存
二级缓存需要配置才能生效,也就是设置< cache/>标签即可,也可以通过useCache=false来关闭当前查询语句的二级缓存功能。在分析Executor的时候我们知道。Executor默认会用CachingExecutor来包装(因为配置文件中会默认全局开启二级缓存功能),所以我们分析CachingExecutor中的query方法即可。
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//如果我们在映射文件中配置了使用缓存
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) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//设置缓存
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
仔细分析可以发现一级缓存和二级缓存整体逻辑相似,但是不同的地方就是缓存值保存的载体不一样,一级缓存是通过Cache实例来保存,二级缓存是通过CachingExecutor实例中的TransactionalCacheManager来保存,这里就存在一个问题,二级缓存是怎么在不同的SqlSession实例中共享缓存的呢?我们先看它的putObject方法。
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
先获取到一个TransactionalCache对象(从名称来看,这是带事务的缓存器),然后再调用其putObject方法,TransactionalCache也是Cache接口的一个实现,它被保存在TransactionalCacheManager的一个transactionalCaches的map集合中。键是Cache对象,也就是当前执行语句中配置的二级缓存对象。
TransactionalCache的putObject方法
public void putObject(Object key, Object object) {
//这是一个Map<Object, Object>集合
entriesToAddOnCommit.put(key, object);
}
到这里,似乎还是没有实现二级缓存功能,但是仔细查看集合名称,似乎需要提交之后才生效。所以我们需要查看其CachingExecutor的commit方法->tcm.commit()
public void commit() {
//执行当前所有事务缓存提交
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
txCache.commit()-> flushPendingEntries()
private void flushPendingEntries() {
//也就是把entriesToAddOnCommit集合中的所有值都用委托缓存对象保存
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
}
delegate这个实例是通过MappedStatement获取,这是在解析映射文件的时候,解析器cache标签的时候,得到的,可以发现最外层是SynchronizedCache->LoggingCache->SerializedCache->LruCache->PerpetualCache,这是一种级联调用。所以这里的putObject方法会按照这样的顺序调用下去,然后在SerializedCache中会对value进行序列化,所以要使用二级缓存,缓存对象必须要实现Serializable否则就会报错。最后都是把结果保存到PerpetualCache对象的HashMap集合中。getObject的时候也会存在一个反序列化过程。
二级缓存之所以能够在多个不同的SqlSession实例共享,是因为缓存对象是通过MappedStatement对象获取的,这是一个映射文件级别的缓存对象,只要是这个映射文件中的任何查询语句拿到的这个对象都是一样的,又因为映射文件是共享的,所以这样达到了二级缓存的效果。
因为二级缓存是事务级别的缓存,所以只有当事务提交之后才会生效,也就是只有事务提交之后才会真正的保存缓存值。
这里有个缓存值序列化的问题,是通过SerializedCache来实现的,这是在CacheBuilder的setStandardDecorators方法中通过判断是否可读写readWrite来实现的,默认是可读写,也可以通过修改设置成readOnly=true,这样就不会使用序列化,我们在分析映射文件解析的时候,也讲过设置成readOnly,那么返回的都是同一个缓存对象,如果某个SqlSession实例修改了这个对象,就会对其它实例造成影响,所以这是不安全的,但是性能很好。相反,如果可读写,就采用了序列化,每次返回的是对象的一个拷贝,所以相对安全,但是性能降低,这个可以根据具体的需求去设置。
三、Cache接口
以上简单的分析了一级缓存和二级缓存的实现逻辑,分析结果来看最终的缓存结果都是保存在Cache实例中
public interface Cache {
//缓存Id
String getId();
//保存缓存值
void putObject(Object var1, Object var2);
//获取缓存值
Object getObject(Object var1);
//移除缓存值
Object removeObject(Object var1);
// 清空缓存
void clear();
//获取缓存大小
int getSize();
//获取读写锁
default ReadWriteLock getReadWriteLock() {
return null;
}
}
接口的方法简单也好理解,难于理解的在于它的实现类。
- BlockingCache:带阻塞式缓存,这就是类似于生产者消费者模型
- FifoCache:先进先出缓存,这是缓存的失效策略,也就是在保存缓存的时候,会判断当前缓存值是否超过了设置值(默认是1024,如果超过了就移除,移除规则就是先进先出)
- LoggingCache:带日志的缓存,取缓存的时候,会打印出缓存命中率
- LruCache:最少使用原则,通过LinkedHashMap来实现,移除最少使用的缓存(默认大小1024)
- PerpetualCache:永久缓存,也就是缓存不会被清理
- ScheduledCache:带定时器的缓存,默认是清理间隔是一个小时,可以设置flushInterval清理间隔
- SerializedCache:带序列化的缓存
- SoftCache:软引用缓存,这是通过jdk自带的软引用方式,也就是当内存不足的时候会清楚缓存
- WeakCache:弱引用缓存,这是通过jdk自带的弱引用方式,也就是当进行垃圾回收的时候会清楚缓存
- SynchronizedCache:同步缓存,也就是加了锁的缓存
- TransactionalCache :事务级缓存,也是实现二级缓存的关键
以上就是大致分析了Cache接口的实现类,具体实现逻辑不是很难,难点在于我们怎么合理的使用这些缓存。
四、总结
Mybatis默认会开启全局缓存功能,一级缓存是默认开启的,二级缓存需要配置对应的cache标签,如果我们想关闭一级缓存就需要设置flushCahe=true或者设置localCacheScope=STATEMENT,这样就会让一级缓存失效。二级缓存如果想关闭的话需要配置useCache=false,一级缓存保存对象是PerpetualCache,二级缓存需要不停的封装,二级缓存还可以配置对应的失效策略等。
以上,有任何不对的地方,请指正,敬请谅解。