本篇分析缓存的查询流程
1.二级缓存的全局配置
配置中的设置配置cacheEnabled标签可以全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存,默认是真,也就是默认开启,这个配置就是二级缓存的全局开关。
2.缓存接口
在MyBatis的的包org.apache.ibatis.cache中有接口高速缓存,所有的缓存实现都是围绕该接口的。
package org.apache.ibatis.cache;
import java.util.concurrent.locks.ReadWriteLock;
/**
* SPI for cache providers.
*
* One instance of cache will be created for each namespace.
*
* The cache implementation must have a constructor that receives the cache id as an String parameter.
*
* MyBatis will pass the namespace as id to the constructor.
*
* <pre>
* public MyCache(final String id) {
* if (id == null) {
* throw new IllegalArgumentException("Cache instances require an ID");
* }
* this.id = id;
* initialize();
* }
* </pre>
*
* @author Clinton Begin
*/
public interface Cache {
/**
* @return The identifier of this cache
*/
String getId();
/**
* @param key Can be any object but usually it is a {@link CacheKey}
* @param value The result of a select.
*/
void putObject(Object key, Object value);
/**
* @param key The key
* @return The object stored in the cache.
*/
Object getObject(Object key);
/**
* As of 3.3.0 this method is only called during a rollback
* for any previous value that was missing in the cache.
* This lets any blocking cache to release the lock that
* may have previously put on the key.
* A blocking cache puts a lock when a value is null
* and releases it when the value is back again.
* This way other threads will wait for the value to be
* available instead of hitting the database.
*
*
* @param key The key
* @return Not used
*/
Object removeObject(Object key);
/**
* Clears this cache instance
*/
void clear();
/**
* Optional. This method is not called by the core.
*
* @return The number of elements stored in the cache (not its capacity).
*/
int getSize();
/**
* Optional. As of 3.2.6 this method is no longer called by the core.
*
* Any locking needed by the cache must be provided internally by the cache provider.
*
* @return A ReadWriteLock
*/
ReadWriteLock getReadWriteLock();
}
该接口有一个实现,官方叫做永久实现,PerpetualCache
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
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();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
本地缓存的实现就是使用该类来存储缓存的。
3.CacheKey
存储一个查询和其结果的对应关系使用的是地图结构,那么标识一个查询就是键,这个键的MyBatis使用了一个对象CacheKey,
public class CacheKey implements Cloneable, Serializable {
private static final long serialVersionUID = 1146682552656046210L;
public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier;
private int hashcode;
private long checksum;
private int count;
// 8/21/2017 - Sonarlint flags this as needing to be marked transient. While true if content is not serializable, this is not always true and thus should not be marked transient.
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
public CacheKey(Object[] objects) {
this();
updateAll(objects);
}
public int getUpdateCount() {
return updateList.size();
}
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
public void updateAll(Object[] objects) {
for (Object o : objects) {
update(o);
}
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (!(object instanceof CacheKey)) {
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {
return false;
}
if (checksum != cacheKey.checksum) {
return false;
}
if (count != cacheKey.count) {
return false;
}
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
return hashcode;
}
@Override
public String toString() {
StringBuilder returnValue = new StringBuilder().append(hashcode).append(':').append(checksum);
for (Object object : updateList) {
returnValue.append(':').append(ArrayUtil.toString(object));
}
return returnValue.toString();
}
@Override
public CacheKey clone() throws CloneNotSupportedException {
CacheKey clonedCacheKey = (CacheKey) super.clone();
clonedCacheKey.updateList = new ArrayList<Object>(updateList);
return clonedCacheKey;
}
}
大致分析可以看出是将查询有关的因素都放在一个目录中,在存放入列表中的时候会改变响应的系统值,最后比较的时候会比较这些值,以及参数个数,每个参数的值等等,具体可以看该类的equals()方法,测试过程中的带参和不带参的CacheKey toString值如下:
1037017427:-322002377:org.mapper.UserMapper.selectAll:0:2147483647:select * from tb_oder:development
170601707:446667724:org.mapper.UserMapper.selectByName:0:2147483647:select * from tb_oder其中name为concat(?, '%'):记者:发展
也就是说同一个映射的同一个方法,传入的参数个数顺序值都相同才能算是同一个查询,这个时候才能匹配上缓存的。
4.缓存优先使用顺序
当本地缓存和二级缓存同时存在时,查询是优先使用那个缓存呢?优先使用二级缓存,看看源码DefaultSqlSessionFactory在创建的一个SqlSession的时候会去查询是配置中的cacheEnabled配置,该配置为真则创建一个CachingExecutor,该执行器是二级缓存的执行器。如果在该执行器中没有找到则去BaseExecutor查询,该查询中使用的是本地缓存。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
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);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
1037017427:-322002377:org.mapper.UserMapper.selectAll:0:2147483647:select * from tb_oder:development
1037017427:-322002377:org.mapper.UserMapper.selectAll:0:2147483647:select * from tb_oder:development
缓存的cacheKey是根据相同的接口中相同方法参数相同环境相同
5.CachingExecutor查询逻辑
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.<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);
}
查询方法中我们可以看出先从中医变量中查询,如果没有则继续去BaseExecutor查询,查询后会将查询结果存入到TCM中。下次执行查询的时候,二级缓存的查询会从该中医变量中查找.tcm的源码如下:
public class TransactionalCacheManager {
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}
private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}
}
TransactionalCacheManager,我们可以称之为事务缓存管理器,因为它里面存储了一个地图,地图中以缓存为重点,TransactionalCache为值,TransactionalCache的作用就是事务性的管理缓存,其内部持有一个缓存,也就是MappedStatement的缓存对象,当TransactionalCacheManager提交的时候,会调用每个TransactionalCache的提交方法,该方法中,会将存入到TransactionalCache中的查询结果集存入到它委派的缓存对象中源码如下:
TransactionalCacheManager的提交()方法
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
TransactionalCache的提交()方法
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
TransactionalCache的flushPendingEntries()方法
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
该方法的作用就是在事务提交的时候,将把进入私有最终Map <Object,Object> entriesToAddOnCommit; 图中的元素存放进真正的缓存中,而不是装饰器的缓存中。这也就是为什么会话1的第二次全选()方法没有从二级缓存中取出缓存结果的原因,因为在TransactionalCacheManager中的放置方法其实只是将查询结果存储在了TransactionalCache的entriesToAddOnCommit集合中了,并没有存储进PerpetualCahce中现在来看看TransactionalCache的作用,官方的说明是:
6.缓存装饰器
看看源码包结构有那么多类实现了高速缓存接口,我们来分析下以上提到的缓存对象
以上实现缓存接口的永久类是PerpetualCache,其他都是对该类的装饰,比如LoggingCache,就是对其缓存命中的统计,源码如下:
public class LoggingCache implements Cache {
private final Log log;
private final Cache delegate;
protected int requests = 0;
protected int hits = 0;
public LoggingCache(Cache delegate) {
this.delegate = delegate;
this.log = LogFactory.getLog(getId());
}
@Override
public String getId() {
return delegate.getId();
}
@Override
public int getSize() {
return delegate.getSize();
}
@Override
public void putObject(Object key, Object object) {
delegate.putObject(key, object);
}
@Override
public Object getObject(Object key) {
requests++;
final Object value = delegate.getObject(key);
if (value != null) {
hits++;
}
if (log.isDebugEnabled()) {
log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
}
return value;
}
@Override
public Object removeObject(Object key) {
return delegate.removeObject(key);
}
@Override
public void clear() {
delegate.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
public int hashCode() {
return delegate.hashCode();
}
@Override
public boolean equals(Object obj) {
return delegate.equals(obj);
}
private double getHitRatio() {
return (double) hits / (double) requests;
}
}
可以看出在从缓存中取出时候,它会统计命中情况,在调试级别的日志时会打印出命中率,这也就是我们在上一篇中看到的
缓存命中率[org.test.mapper.UserMapper]:0.0
我们从MappedStatement中获取的缓存对象是经过四层装饰的SynchronizedCache,结构如下图
7.缓存查询顺序总结:
注意:如果第一次会话上没有提交,则第一次会议中的多次相同查询都是不能从二级缓存中查询出来的,只有第一次缓存提交后,后续的会话的查询才能使用二级缓存中的结果。