首页 > 代码库 > mybatis 与 缓存

mybatis 与 缓存

首先从配置文件说起,有个cacheEnabled的配置项,当设置为true时(默认就是true),Session就会用一个CachingExecutor来包装我们的Executor实例:

  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;
  }

技术分享

 

这是一个装饰者模式,在大部分情况下是直接转发调用的,在update方法和query方法中分别根据mapper中statement的配置来对缓存进行读取和刷新

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.update(ms, parameterObject);
  }
@Override 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, parameterObject, 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); }

其中flushCacheIfRequired和ms.isUseCache都是在Mapper文件中配置的,用于刷新缓存和使用缓存

以上所说的缓存都是二级缓存,所谓二级缓存就是可以在查询结束后还能操作的全局缓存。

相比于全局缓存,那么就有局部缓存,也叫一级缓存,在mybatis中用LocalCache表示

Mybatis的局部缓存有两种作用域可以配置: SESSION和STATEMENT,会话作用域和语句作用域;本地缓存一直是开着的,在执行器内部会存放一个PerpetualCache类型的成员变量localCache来作为查询缓存,一个PerpetualCache类型的localOutputParameterCache成员来缓存存储过程的输出参数。如果是statement级别的缓存,那么在这个statement执行完成后就会清空缓存。如果执行更新操作会立即清空本地缓存。

  @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();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
    	// 如果是statement级别的缓存,那么在这个statement执行完成后就会清空缓存
        clearLocalCache();
      }
    }
    return list;
  }

  // 清空缓存
  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }

  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;
  }


  @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);
  }

我们可以看到,本地缓存使用的是PerpetualCache,它实现了Cache接口,那么二级缓存用的是什么呢:

技术分享

事实上,这又是一个装饰者模式,Cache接口的唯一真正实现是PerpetualCache,其他的实现类都是对它的包装,比如LoggingCache:

public class LoggingCache implements Cache {

  private Log log;  
  private Cache delegate;
  protected int requests = 0;  // 访问数量
  protected int hits = 0;   // 命中数量

  public LoggingCache(Cache delegate) {
    this.delegate = delegate;
    this.log = LogFactory.getLog(getId());
  }
... omitted ...
  @Override
  public Object getObject(Object key) {
    requests++;
    final Object value = http://www.mamicode.com/delegate.getObject(key);"Cache Hit Ratio [" + getId() + "]: " + getHitRatio());  // 记录缓存命中率
    }
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    return delegate.removeObject(key);
  }

 // 缓存命中率 private double getHitRatio() { return (double) hits / (double) requests; } }

LoggingCache是对Cache对象的一个包装,然后记录缓存命中率。

当然还有负责淘汰的包装器,比如 LruCache(默认的淘汰包装器):

public class LruCache implements Cache {

  private final Cache delegate;
  private Map<Object, Object> keyMap;
  private Object eldestKey;

  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }

  // 设置缓存大小,分配缓存存储空间,LinkedHashMap,移除最近最少使用的key的时候讲key交给eldestKey
  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;

      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }

  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }

  @Override
  public Object getObject(Object key) {
    keyMap.get(key); //touch一下,更新时间
    return delegate.getObject(key);
  }

  @Override
  public void clear() {
    delegate.clear();
    keyMap.clear();
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  // 移除 eldestKey
  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }

}

同样,这个缓存和淘汰缓存的实现都是可以在Mapper文件中指定的,可以自己写Cache实现类,然后配置在Mapper文件中

  private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      String eviction = context.getStringAttribute("eviction", "LRU");
      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
      Long flushInterval = context.getLongAttribute("flushInterval");
      Integer size = context.getIntAttribute("size");
      boolean readWrite = !context.getBooleanAttribute("readOnly", false);
      boolean blocking = context.getBooleanAttribute("blocking", false);
      Properties props = context.getChildrenAsProperties();
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }

 

但是这些跟executor好像都没有什么关系,我们的CachingExecutor中是使用tcm成员对象来对缓存进行管理的,这个tcm实际上是TransactionalCacheManager的一个示例,TransactionalCacheManager类是对缓存的事务包装,细想,如果一个事务失败了,那个这个缓存该怎么搞呢,之前写了的缓存该怎么回滚?

在TransactionalCacheManager中是使用TransactionalCache来实现这一点的,他也是对Cache的一层包装,不过在putObject方法中,它并没有直接调用被包装cache对象的putObject,而是将键值对保存到自己的私有成员entriesToAddOnCommit中,待被调用commit方法时,才会将这些键值对通过调用被包装对象cache的putObject刷入真正的缓存:

  private Map<Object, Object> entriesToAddOnCommit;  //内部的键值对
  
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);    //put到内部的键值对
  }

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void flushPendingEntries() {   // 将内部的键值对刷入到 真正的cache中
    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);
      }
    }
  }

TransactionalCacheManager将Cache包装成TransactionalCache

  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

 

之后,来看看缓存key的计算,cacheKey是在BaseExecutor.createCacheKey方法中计算的:

  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    cacheKey.update(ms.getId());  // statementId
    cacheKey.update(rowBounds.getOffset());  // offset
    cacheKey.update(rowBounds.getLimit());   // limit
    cacheKey.update(boundSql.getSql());      // sql语句
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = http://www.mamicode.com/boundSql.getAdditionalParameter(propertyName);"color: #ff0000">// 参数
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());   //数据库
    }
    return cacheKey;
  }

cacheKey的计算是根据statementId, 分页情况,查询参数和数据库链接作为条件来计算的,CacheKey会根据这些条件来区分每一个CacheKey:

public class CacheKey implements Cloneable, Serializable {

  // hashCode扩展因子
  private static final int DEFAULT_MULTIPLYER = 37;
  // hashcode初始值
  private static final int DEFAULT_HASHCODE = 17;

  private int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  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) {
    if (object != null && object.getClass().isArray()) {
      int length = Array.getLength(object);
      for (int i = 0; i < length; i++) {
        Object element = Array.get(object, i);
        doUpdate(element);
      }
    } else {
      doUpdate(object);
    }
  }

  // 计算hashCode和校验和
  private void doUpdate(Object object) {
    int baseHashCode = object == null ? 1 : object.hashCode();

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }

  // IMPORTANT
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    // hash值不同,肯定不是同一个Cache
    if (hashcode != cacheKey.hashcode) {
      return false;
    }
    
    // 校验和不同,也不是相同的Cache
    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 (thisObject == null) {
        if (thatObject != null) {
          return false;
        }
      } else {
        if (!thisObject.equals(thatObject)) {
          return false;
        }
      }
    }
    return true;
  }
// 根据条件产生cahceKey的字符串,效率较慢,给自定义缓存使用 @Override public String toString() { StringBuilder returnValue = http://www.mamicode.com/new StringBuilder().append(hashcode).append(‘:‘).append(checksum);>

 

最后,附上一张缓存工作的时序图,TransactionCachingManager是二级缓存,LocalCache是一级缓存

技术分享

 

mybatis 与 缓存