mybaits源碼分析--緩存模塊(六)


一、緩存模塊

MyBatis作為一個強大的持久層框架,緩存是其必不可少的功能之一,Mybatis中的緩存分為一級緩存和二級緩存。但本質上是一樣的,都是使用Cache接口實現的。緩存位於 org.apache.ibatis.cache包下。

通過結構能夠發現Cache其實使用到了裝飾器模式來實現緩存的處理。先來看看Cache中的基礎類的API;Cache接口的實現類很多,但是大部分都是裝飾器,只有PerpetualCache提供了Cache接口的基本實現。

 

 

1.1 Cache接口

Cache接口是緩存模塊中最核心的接口,它定義了所有緩存的基本行為,Cache接口的定義如下:

public interface Cache {

  /**
   * 緩存對象的 ID
   * @return The identifier of this cache
   */
  String getId();

  /**
   * 向緩存中添加數據,一般情況下 key是CacheKey  value是查詢結果
   * @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);

  /**
   * 根據指定的key,在緩存中查找對應的結果對象
   * @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.
   *   刪除key對應的緩存數據
   *
   * @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.
   * <p>
   * Any locking needed by the cache must be provided internally by the cache provider.
   *  獲取讀寫鎖
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

1.2 PerpetualCache

PerpetualCache在緩存模塊中扮演了ConcreteComponent的角色,其實現比較簡單,底層使用HashMap記錄緩存項,具體的實現如下

/**
 * 在裝飾器模式用 用來被裝飾的對象
 * 緩存中的  基本緩存處理的實現
 * 其實就是一個 HashMap 的基本操作
 * @author Clinton Begin
 */
public class PerpetualCache implements Cache {

  private final String id; // Cache 對象的唯一標識

  // 用於記錄緩存的Map對象
  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();
  }

  @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;
    // 只關心ID
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    // 只關心ID
    return getId().hashCode();
  }

}

然后可以來看看cache.decorators包下提供的裝飾器。他們都實現了Cache接口。這些裝飾器都在PerpetualCache的基礎上提供了一些額外的功能,通過多個組合實現一些特殊的需求。

1.3 BlockingCache

這是一個阻塞同步的緩存,它保證只有一個線程到緩存中查找指定的key對應的數據

/**
 * Simple blocking decorator
 *   阻塞版的緩存 裝飾器
 * Simple and inefficient version of EhCache's BlockingCache decorator.
 * It sets a lock over a cache key when the element is not found in cache.
 * This way, other threads will wait until this element is filled instead of hitting the database.
 *
 * @author Eduardo Macarron
 *
 */
public class BlockingCache implements Cache {

  private long timeout; // 阻塞超時時長
  private final Cache delegate; // 被裝飾的底層 Cache 對象
  // 每個key 都有對象的 ReentrantLock 對象
  private final ConcurrentHashMap<Object, ReentrantLock> locks;

  public BlockingCache(Cache delegate) {
    // 被裝飾的 Cache 對象
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public void putObject(Object key, Object value) {
    try {
      // 執行 被裝飾的 Cache 中的方法
      delegate.putObject(key, value);
    } finally {
      // 釋放鎖
      releaseLock(key);
    }
  }

  @Override
  public Object getObject(Object key) {
    acquireLock(key); // 獲取鎖
    Object value = delegate.getObject(key); // 獲取緩存數據
    if (value != null) { // 有數據就釋放掉鎖,否則繼續持有鎖
      releaseLock(key);
    }
    return value;
  }

  @Override
  public Object removeObject(Object key) {
    // despite of its name, this method is called only to release locks
    releaseLock(key);
    return null;
  }

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

  private ReentrantLock getLockForKey(Object key) {
    return locks.computeIfAbsent(key, k -> new ReentrantLock());
  }

  private void acquireLock(Object key) {
    Lock lock = getLockForKey(key);
    if (timeout > 0) {
      try {
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId());
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {
      lock.lock();
    }
  }

  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }

  public long getTimeout() {
    return timeout;
  }

  public void setTimeout(long timeout) {
    this.timeout = timeout;
  }
}

通過源碼我們能夠發現,BlockingCache本質上就是在操作緩存數據的前后通過ReentrantLock對象來實現了加鎖和解鎖操作。

緩存實現類 緩存實現類 作用 裝飾條件

基本緩存



緩存基本實現類

默認是PerpetualCache,也可以自定義比如RedisCache、EhCache等,具備基本功能的緩存類
LruCache

LRU策略的緩存

當緩存到達上限時候,刪除最近最少使用的緩存(Least Recently Use)

eviction="LRU"(默認)
FifoCache

FIFO策略的緩存

當緩存到達上限時候,刪除最先入隊的緩存

eviction="FIFO"
SoftCacheWeakCache

帶清理策略的緩存

通過JVM的軟引用和弱引用來實現緩存,當JVM內存不足時,會自動清理掉這些緩存,基於SoftReference和WeakReference

eviction="SOFT"eviction="WEAK"
LoggingCache

帶日志功能的緩存

比如:輸出緩存命中率 基本
SynchronizedCache

同步緩存

基於synchronized關鍵字實現,解決並發問題

基本
BlockingCache

阻塞緩存

通過在get/put方式中加鎖,保證只有一個線程操作緩存,基於Java重入鎖實現

blocking=true
SerializedCache

支持序列化的緩存

將對象序列化以后存到緩存中,取出時反序列化

readOnly=false(默認)
ScheduledCache

定時調度的緩存

在進行get/put/remove/getSize等操作前,判斷緩存時間是否超過了設置的最長緩存時間(默認
是一小時),如果是則清空緩存--即每隔一段時間清空一次緩存

flushInterval不為空
TransactionalCache

事務緩存

在二級緩存中使用,可一次存入多個緩存,移除多個緩存

在TransactionalCacheManager中用Map維護對應關系

 

1.4 緩存的應用

1.4.1 緩存對應的初始化

在之前寫的代碼中斷個點看下可能直接點,在斷點前說明下要求,如要開啟緩存要在配置文件開啟一級和二級緩存

然后呢在mapper.XML文件加入<cache/>標簽就可以了

 

 

 下面來斷點看下

 

 

 

 

 

 通過上面截圖可以很清楚的看到這是一個裝飾器過程,接下來看下在Configuration初始化的時候怎么給我們的各種Cache實現注冊對應的別名

 

 

 在解析settings標簽的時候,設置的默認值有如下;因為前面源碼跟了好多次,這里面我直接進到解析這一段代碼了

 

 

 

 public Configuration parse() {
    //檢查是否已經解析過了
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // XPathParser,dom 和 SAX 都有用到 >>
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      // 對於全局配置文件各種標簽的解析
      propertiesElement(root.evalNode("properties"));
      // 解析 settings 標簽
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      // 讀取文件
      loadCustomVfs(settings);
      // 日志設置
      loadCustomLogImpl(settings);
      // 類型別名
      typeAliasesElement(root.evalNode("typeAliases"));
      // 插件
      pluginElement(root.evalNode("plugins"));
      // 用於創建對象
      objectFactoryElement(root.evalNode("objectFactory"));
      // 用於對對象進行加工
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      // 反射工具箱
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      // settings 子標簽賦值,默認值就是在這里提供的 >>
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 創建了數據源 >>
      environmentsElement(root.evalNode("environments"));
      //解析databaseIdProvider標簽,生成DatabaseIdProvider對象(用來支持不同廠商的數據庫)。
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析引用的Mapper映射器
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

在上面的全局配置文件中在settingsElement(settings);的賦值中會做一些默認的處理,點進去看下

 

 通過上面發現cacheEnabled默認為true,localCacheScope默認為 SESSION,在初始化過程中關鍵的還是映射文件的解析,點擊mapperElement(root.evalNode("mappers"));進去看下

 private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 不同的定義方式的掃描,最終都是調用 addMapper()方法(添加到 MapperRegistry)。這個方法和 getMapper() 對應
        // package    包
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            // resource    相對路徑
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 解析 Mapper.xml,總體上做了兩件事情 >>
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            // url    絕對路徑
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            // class     單個接口
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

直接進入他的關鍵代碼mapperParser.parse();,

 public void parse() {
    // 總體上做了兩件事情,對於語句的注冊和接口的注冊
    if (!configuration.isResourceLoaded(resource)) {
      // 1、具體增刪改查標簽的解析。
      // 一個標簽一個MappedStatement。 >>
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // 2、把namespace(接口類型)和工廠類綁定起來,放到一個map。
      // 一個namespace 一個 MapperProxyFactory >>
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

上面是映射文件的解析操作,可以看他進了標簽的解析,進去看下

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      // 添加緩存對象
      cacheRefElement(context.evalNode("cache-ref")); // 解析 cache 屬性,添加緩存對象
      cacheElement(context.evalNode("cache")); // 創建 ParameterMapping 對象
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // 創建 List<ResultMapping>
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析可以復用的SQL
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析增刪改查標簽,得到 MappedStatement >>
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

看到這里好像找到了想找的東西,可以看到上面代碼我標的兩個地方的標簽解析,跟進去看下

 private void cacheElement(XNode context) {
    // 只有 cache 標簽不為空才解析
    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);
    }
  }

會發現上面開始解析相關的屬性信息了,並在最后一步進行了保存,繼續跟進去看下

 public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache; return cache;
  }

然后可以發現 如果存儲 cache 標簽,那么對應的 Cache對象會被保存在 currentCache 屬性中。

 

 

 

 進而在 Cache 對象 保存在了 MapperStatement 對象的 cache 屬性中。這就是cache節點創建的整個過程。

1.4.2 一級緩存

一級緩存也叫本地緩存(Local Cache),MyBatis的一級緩存是在會話(SqlSession)層面進行緩存的。MyBatis的一級緩存是默認開啟的,不需要任何的配置(如果要關閉,localCacheScope設置為STATEMENT)。在BaseExecutor對象的query方法中有關閉一級緩存的邏輯

 

 

 

 

 

 從上面的效果可以很清楚的感受到在一個會話內,第二次查詢是直接走緩存的,在不同會話內緩存是不起效的。下面會了解緩存做了啥跟進代碼看下。入口從上面演示就可以猜到是從

SqlSession sqlSession = factory.openSession();進入的
  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }
 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);
      // 根據事務工廠和默認的執行器類型,創建執行器 >>執行SQL語句操作
      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) {//針對Statement做緩存
      executor = new ReuseExecutor(this, transaction);
    } else {
      // 默認 SimpleExecutor,每一次只是SQL操作都創建一個新的Statement對象
      executor = new SimpleExecutor(this, transaction);
    }
    // 二級緩存開關,settings 中的 cacheEnabled 默認是 true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 植入插件的邏輯,至此,四大對象已經全部攔截完畢;這里面是一個攔截器鏈
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

從上面代碼可以知道如果 cacheEnabled 為 true 就會通過 CachingExecutor 來裝飾executor 對象,然后就是在執行SQL操作的時候會涉及到緩存的具體使用。這個就分為一級緩存和二級緩存,通過這個跟蹤會發現在創建會話時會創建執行器,而執行器里面跟緩存有關系的是二級緩存,跟我想找的一級緩存沒什么關系;那么一級緩存在哪呢,這時候我想一級緩存是跟會話有關,那么他的位置一定在會話內的這段代碼里,那我就找下一段代碼

 // 4.通過SqlSession中提供的 API方法來操作數據庫
        List<User1> list = sqlSession.selectList("com.ghy.mapper.UserMapper.selectUserList");

進入selectList看下

  @Override
  public <E> List<E> selectList(String statement) {
    return this.selectList(statement, null);
  }
  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 如果 cacheEnabled = true(默認),Executor會被 CachingExecutor裝飾
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

在上面代碼中可以看到一個查詢操作,那肯定是要進去看下他在查詢前有沒有緩存判斷,如果沒有說明selectList代碼是不走緩存的;

 

 

 在上面代碼中發現了一些跟緩存相關的操作CacheKey

 @Override
  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());
    cacheKey.update(rowBounds.getOffset()); // 0
    cacheKey.update(rowBounds.getLimit()); // 2147483647 = 2^31-1
    cacheKey.update(boundSql.getSql());
    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); // development
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

發現上面是一個緩存創建的邏輯,這個東西debugger看一下其實就明白了;其實這寫了一堆就是生成一個東西,生成一個緩存的KEY,而且這個KEY是跟我們寫的SQL有關;明白了這個key的作用后回退一步跟進query看他拿這個key去做了什么

 @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 異常體系之 ErrorContext
    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()) {
      // flushCache="true"時,即使是查詢,也清空一級緩存
      clearLocalCache();
    }
    List<E> list;
    try {
      // 防止遞歸查詢重復處理緩存
      queryStack++;
      // 查詢一級緩存
      // ResultHandler 和 ResultSetHandler的區別
      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;
  }

從上面就找到了查找一級緩存的位置了,如果list判斷是空說明緩存沒數據他會走queryFromDatabase去查詢並且把數據緩存起來

 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 {
      // 三種 Executor 的區別,看doUpdate
      // 默認Simple
      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;
  }

 

1.4.3 二級緩存

二級緩存是用來解決一級緩存不能跨會話共享的問題的,范圍是namespace級別的,可以被多個SqlSession共享(只要是同一個接口里面的相同方法,都可以共享),生命周期和應用同步。二級緩存的設置,首先是settings中的cacheEnabled要設置為true,當然默認的就是為true,這個步驟決定了在創建Executor對象的時候是否通過CachingExecutor來裝飾。前面源碼中也有說明過;要想看二級緩存效果,</cache>標簽要打開

 

 然后把一級緩存配置關閉了,其實由於一級緩存的作用域太小,在實際生產中用的也比較少

 

 

 

 從上面可以發現第二次查詢就沒走數據庫查詢,說明二級緩存生效了。接下來看下二級緩存源碼,其實在上面已經寫出來了,入口是factory.openSession();

  @Override
  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }
  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);
      // 根據事務工廠和默認的執行器類型,創建執行器 >>執行SQL語句操作
      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) {//針對Statement做緩存
      executor = new ReuseExecutor(this, transaction);
    } else {
      // 默認 SimpleExecutor,每一次只是SQL操作都創建一個新的Statement對象
      executor = new SimpleExecutor(this, transaction);
    }
    // 二級緩存開關,settings 中的 cacheEnabled 默認是 true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 植入插件的邏輯,至此,四大對象已經全部攔截完畢;這里面是一個攔截器鏈
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

從這里可以看到如果判斷成立,那么會對executor做一個裝飾;后面做查詢操作時就要從sqlSession.selectList("com.ghy.mapper.UserMapper.selectUserList");跟蹤起了

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 如果 cacheEnabled = true(默認),Executor會被 CachingExecutor裝飾
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

一樣,進入query方法看他是怎么執行的

 

 這里要進的就是CachingExecutor里面了,這里面是二級緩存的東西

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 獲取SQL
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 創建CacheKey:什么樣的SQL是同一條SQL? >>
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
這里面創建createCacheKey的過程和一級緩存一樣,這里就不想再寫一次了;
 @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    // cache 對象是在哪里創建的?  XMLMapperBuilder類 xmlconfigurationElement()
    // 由 <cache> 標簽決定
    if (cache != null) {
      // flushCache="true" 清空一級二級緩存 >>
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // 獲取二級緩存
        // 緩存通過 TransactionalCacheManager、TransactionalCache 管理
        @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;
      }
    }
    // 走到 SimpleExecutor | ReuseExecutor | BatchExecutor
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

這就是二級緩存過程;

cache屬性詳解:

屬性 含義 取值
type

緩存實現類

需要實現Cache接口,默認是PerpetualCache,可以使用第三方緩存
size

最多緩存對象個數

默認1024
eviction

回收策略(緩存淘汰算法)

LRU – 最近最少使用的:移除最長時間不被使用的對象(默認)。FIFO
– 先進先出:按對象進入緩存的順序來移除它們。SOFT – 軟引用:移除
基於垃圾回收器狀態和軟引用規則的對象。WEAK – 弱引用:更積極地
移除基於垃圾收集器狀態和弱引用規則的對象。

flushInterval

定時自動清空緩存間隔

自動刷新時間,單位 ms,未配置時只有調用時刷新
readOnly

是否只讀

true:只讀緩存;會給所有調用者返回緩存對象的相同實例。因此這些
對象不能被修改。這提供了很重要的性能優勢。false:讀寫緩存;會返
回緩存對象的拷貝(通過序列化),不會共享。這會慢一些,但是安
全,因此默認是 false。改為false可讀寫時,對象必須支持序列化。

blocking

啟用阻塞緩存

通過在get/put方式中加鎖,保證只有一個線程操作緩存,基於Java重入鎖實現


1.4.4 第三方緩存

在實際開發的時候我們一般也很少使用MyBatis自帶的二級緩存,這時我們會使用第三方的緩存工具Ehcache獲取Redis來實現 https://github.com/mybatis/redis-cache

添加依賴

        <dependency>
            <groupId>org.mybatis.caches</groupId>
            <artifactId>mybatis-redis</artifactId>
            <version>1.0.0-beta2</version>
        </dependency>

然后加上Cache標簽的配置

<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

然后添加redis的屬性文件

 

 這樣緩存就存入redis中了,至於怎么讀到redis.properites文件的,這個可以從源碼中找下

 

 從上面看到在構造方法中會做一些初始的操作,其中的JedisPool是操作連接去操作redis的;

    public RedisConfig parseConfiguration() {
        return parseConfiguration(getClass().getClassLoader());
    }

 

 從源碼中可以發現他已經做好了redis連接配置文件的默認命名了;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM