zoukankan      html  css  js  c++  java
  • 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 = 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);
      }
    
     // 缓存命中率 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 = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
              value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
              value = parameterObject;
            } else {
              MetaObject metaObject = configuration.newMetaObject(parameterObject);
              value = metaObject.getValue(propertyName);
            }
            cacheKey.update(value);      // 参数
          }
        }
        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 = new StringBuilder().append(hashcode).append(':').append(checksum); for (Object object : updateList) { returnValue.append(':').append(object); } return returnValue.toString(); } }

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

  • 相关阅读:
    JavaScript实现HTML导航栏下拉菜单[悬浮显示]
    Paper Pal:一个中英文论文及其代码大数据搜索平台
    小程序定位地图模块全系列开发教学(超详细)
    简单的个人介绍网页主页面【附代码】
    前端分页功能(通用)
    打造完美写作系统:Gitbook+Github Pages+Github Actions
    移动端布局
    三剑客var,let,const
    包含多个段的程序01 零基础入门学习汇编语言29
    更灵活的定位内存地址的方法01 零基础入门学习汇编语言32
  • 原文地址:https://www.cnblogs.com/cookiezhi/p/6250692.html
Copyright © 2011-2022 走看看