mybatis 源碼解析之 cache

預備知識

對 Mybatis 的執行流程有一定的瞭解。

一級緩存

在讀多寫少的情況下,減少了網絡的開銷,顯著提升性能。

配置

public enum LocalCacheScope {
  SESSION,STATEMENT
}

對應到配置文件

<setting name="localCacheScope" value="SESSION"/>

支持 SESSION 和 STATEMENT 兩種。其中

SESSION :指在當前事務中都使用查詢緩存。

STATEMENT:指在當前 Sql 語句中使用查詢緩存

注:如果是 SESSION 的情況下,如果有多個 SqlSession 同時操作操作數據庫的同一張表,就可能出現髒讀。

實現原理

一級緩存的實現在 BaseExecutor:每一個SqlSession中持有了自己的Executor,每一個Executor(都繼承自 BaseExecutor)中有一個Local Cache。

當用戶發起查詢時,Mybatis會根據當前執行的MappedStatement生成一個key,去Local Cache中查詢,

  1. 如果緩存命中的話
  2. 如果緩存沒有命中的話,將寫一個佔位符,這樣後續的讀,就可以利用緩存了。之後查詢 DB,將結果寫入Local Cache。
  3. 如果配置爲 STATEMENT,清除緩存

最後返回結果給用戶。

public abstract class BaseExecutor implements Executor {
    
  //本地緩存,一個 Executor 對應一個 localCache
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  
  //key 爲 Statement Id + Offset + Limmit + Sql + Params
  @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 級別,清除緩存
        clearLocalCache();
      }
    }
    return list;
  }
  
  @Override
  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }
    
  private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
    if (ms.getStatementType() == StatementType.CALLABLE) {
      final Object cachedParameter = localOutputParameterCache.getObject(key);
      if (cachedParameter != null && parameter != null) {
        final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
        final MetaObject metaParameter = configuration.newMetaObject(parameter);
        for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
          if (parameterMapping.getMode() != ParameterMode.IN) {
            final String parameterName = parameterMapping.getProperty();
            final Object cachedValue = metaCachedParameter.getValue(parameterName);
            metaParameter.setValue(parameterName, cachedValue);
          }
        }
      }
    }
  }
    
  //這裏緩存更新是有講究的。
  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);
    }
    //將數據庫數據寫入緩存。如果在此之前將來了新的查詢,將返回 EXECUTION_PLACEHOLDER
    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.");
    }
    //每次更新的時候都會清空緩存。update,delete, insert 都調用 update 來完成。
    clearLocalCache();
    return doUpdate(ms, parameter);
  }
}

一級緩存演示&失效情況

同一次會話期間只要查詢過的數據都會保存在當前SqlSession的一個Map中

  • key:hashCode+查詢的SqlId+編寫的sql查詢語句+參數
  • 一級緩存失效的四種情況

1、不同的SqlSession對應不同的一級緩存
2、同一個SqlSession但是查詢條件不同
3、同一個SqlSession兩次查詢期間執行了任何一次增刪改操作
4、同一個SqlSession兩次查詢期間手動清空了緩存

驗證

配置爲 SESSION

  1. 同一個事務 sqlSession 3次查詢同一個表,檢查後續請求是否查詢緩存
  2. 三個事務,每個事實的 sqlSession 各查詢同一張表一次。檢查後續請求是否查詢緩存
  3. 兩個 sqlSession,一個事務 sqlSession1 連續兩次查詢表,sqlSession2 在 sqlSession1 的兩次查詢中間修改表
  4. 兩個 sqlSession,兩個事務 sqlSession1 各一次查詢表,sqlSession2 在兩次查詢中間修改表

配置爲 STATEMENT

  1. 同一個事務 sqlSession 3次查詢同一個表,檢查後續請求是否查詢緩存
  2. 三個事務,每個事實的 sqlSession 各查詢同一張表一次。檢查後續請求是否查詢緩存
  3. 兩個 sqlSession,一個事務 sqlSession1 連續兩次查詢表,sqlSession2 在 sqlSession1 的兩次查詢中間修改表
  4. 兩個 sqlSession,兩個事務 sqlSession1 各一次查詢表,sqlSession2 在兩次查詢中間修改表

總結

  1. Mybatis一級緩存的生命週期和SqlSession一致。
  2. 不管是 SESSION 還是 STATEMENT 在跨事務都不共享緩存。因爲在事務提交的時候會清空緩存。
  3. Mybatis的緩存是一個粗粒度的緩存,沒有更新緩存和緩存過期的概念,同時只是使用了默認的hashmap,也沒有做容量上的限定。
  4. Mybatis的一級緩存最大範圍是SqlSession內部,有多個SqlSession或者分佈式的環境下,有操作數據庫寫的話,會引起髒數據,建議是把一級緩存的默認級別設定爲Statement,即不使用一級緩存。

Cache 接口

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

實現類:

  • PerpetualCache:HashMap 保存 key,value

裝飾器類

  • blockingCache : 基於 key 的鎖。每個 key 一個鎖。注:只有當前線程才能釋放鎖。也可以通過設置超時時間,避免一直阻塞等待鎖。問題:putObject 爲什麼沒有加鎖?
  • FifoCache:FIFO 的淘汰策略。默認隊列大小爲 1024。
  • LoggingCache:用日誌記錄查詢的命中率
  • LruCache:利用 LinkHashMap 實現 Lru 算法。由於 LinkHashMap 實現了 LRU 算法,這裏非常取巧
  • ScheduledCache:每間隔 clearInterval(默認一小時)將所有緩存清空。
  • SerializedCache:將 value 用 ObjectInputStream,ByteArrayInputStream 序列化,對 key 沒有序列化。
  • SoftCache:第一次將值存儲在軟引用對象中,每次操作(put 和 remove)時都將已經被 GC 的 valuel 刪除。每次獲取(get),對應的軟引用沒有被刪除,就加入Deqeue隊列。Deque 是一個默認長度爲 256 的先進先出隊列。這種實現機制非常類似 Java 的 GC 的簡化版。
  • SynchronizedCache:每個操作前面加了 synchronized 關鍵詞
  • TransactionalCache:類似數據庫事務機制。將 put 緩存在臨時 Map 中,當調用 commit 的時候,才清除臨時Map,寫入真正 cache 中。
  • WeakCache:第一次將值存儲在弱引用對象中,每次操作(put 和 remove)時都將已經被 GC 的 valuel 刪除。每次獲取(get),對應的弱引用沒有被刪除,就加入Deqeue隊列。Deque 是一個默認長度爲 256 的先進先出隊列。這種實現機制非常類似 Java 的 GC 的簡化版。
  • TransactionalCacheManager:保存 Cache 和 TransactionalCache 的映射關係

Cache 建造者

CacheBuilder。

二級緩存

  1. 二級緩存(second level cache),全局作用域緩存;二級緩存默認不開啓,需要手動配置
  2. MyBatis提供二級緩存的接口以及實現,緩存實現要求 POJO實現Serializable接口
  3. 二級緩存在 SqlSession 關閉或提交之後纔會生效

配置

<setting name="cacheEnabled" value="true"/>

每個 Mapper 支持 CacheNameSpace 和 CacheNameRef 來配置,先解析配置文件,後解析註解,註解的配置會覆蓋配置文件的配置。配置文件先解析 cache-ref 標籤,後解析 cache 標籤。 註解先解析CacheNameSpace,後解析 CacheNameRef。CacheNameRef 會覆蓋 CacheNameSpace的配置。因此,配置文件和接口註釋是不能夠配合使用的。

CacheNameSpace

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespace {
  Class<? extends org.apache.ibatis.cache.Cache> implementation() default PerpetualCache.class;

  Class<? extends org.apache.ibatis.cache.Cache> eviction() default LruCache.class;

  long flushInterval() default 0;

  int size() default 1024;

  boolean readWrite() default true;
  
  boolean blocking() default false;

  /**
   * Property values for a implementation object.
   * @since 3.4.2
   */
  Property[] properties() default {};
}
  private void parseCache() {
    CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
    if (cacheDomain != null) {
      Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
      Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
      Properties props = convertToProperties(cacheDomain.properties());
      assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
    }
  }

 private Properties convertToProperties(Property[] properties) {
    if (properties.length == 0) {
      return null;
    }
    Properties props = new Properties();
    for (Property property : properties) {
      props.setProperty(property.name(),
          PropertyParser.parse(property.value(), configuration.getVariables()));
    }
    return 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;
  }

支持配置文件和註解兩種,配置文件先解析,之後註解,註解會覆蓋配置文件。最終還是調用 CacheBuilder 來創建 Cache。

方式一:配置文件

解析 cache 標籤

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.TestMapper">
 
    <cache type="com.example.MybatisRedisCache" eviction="FIFO" flushInterval="60000" readOnly="false" size="1024" blocking="false">
        <property name="key1" value="value1" />
        <property name="key2" value="value2" />
    </cache>

</mapper>
  • type:默認只實現了 PerPetualCache,默認也只能是PerPetualCache,如果你實現了自己的 Cache,可以配置。比如,你可以實現用 Redis 或 ECache,實現 Cache 接口,即可。

  • eviction:實現了 LruCache、FifoCache、SoftCache、WeakCache

  • flushInterval:多長時間將所有緩存全部清空。實際爲 ScheculedCache

  • size:緩存數量

  • readOnly:默認 false,如果爲 false,對 value 進行序列化。否則不進行序列化

  • blocking:默認 false。是否是阻塞式的,如果拿不到鎖,一直阻塞。

方式二:註解方式

@CacheNamespace(implementation = MybatisRedisCache.class, size=256, flushInterval=10000, readWrite=true, blocking=false, eviction=FiFoCache.class, properties={"eviction":"LRU", "flushInterval":"100",})
public interface TestMapper(
    @Select("select t_user.* from t_user where t_user.t_user_id = #{tUserId}")
    @Options(useCache = true)
    List<PeoplePo> getUser(PeopleVo p);
}

CacheNamespaceRef

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CacheNamespaceRef {
  /**
   * A namespace type to reference a cache (the namespace name become a FQCN of specified type)
   */
  Class<?> value() default void.class;
  /**
   * A namespace name to reference a cache
   * @since 3.4.2
   */
  String name() default "";
}
  private void parseCacheRef() {
    CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
    if (cacheDomainRef != null) {
      Class<?> refType = cacheDomainRef.value();
      String refName = cacheDomainRef.name();
      //不能都不配置
      if (refType == void.class && refName.isEmpty()) {
        throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
      }
      //不能都配置
      if (refType != void.class && !refName.isEmpty()) {
        throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
      }
      //value 的優先級高於 name 的優先級
      String namespace = (refType != void.class) ? refType.getName() : refName;
      try {
        assistant.useCacheRef(namespace);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteCacheRef(new CacheRefResolver(assistant, namespace));
      }
    }
  }

  public Cache useCacheRef(String namespace) {
    if (namespace == null) {
      throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
      unresolvedCacheRef = true;
      //從已有的 Cache 中查找
      Cache cache = configuration.getCache(namespace);
      if (cache == null) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
      }
      currentCache = cache;
      unresolvedCacheRef = false;
      return cache;
    } catch (IllegalArgumentException e) {
      throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
  }

用來引用已配置的 Cache。支持配置文件和註解兩種,配置文件先解析,之後註解,註解會覆蓋配置文件。

方式一:配置文件

CacheNamespaceRef 爲 cache-ref 標籤下的 namespace

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.TestMapper">
    <cache-ref namespace="MybatisRedisCache"</cache-ref>
</mapper>

方式二:註解方式

支持 value 和 name 兩種方式,但只能配置一種,value 的優先級高於 name 的優先級。

@CacheNamespaceRef(implementation = MybatisRedisCache.class)
public interface TestMapper(
    @Select("select t_user.* from t_user where t_user.t_user_id = #{tUserId}")
    @Options(useCache = true)
    List<PeoplePo> getUser(PeopleVo p);
}

配置緩存的方式

1、全局setting的cacheEnable:

  • 配置二級緩存的開關。一級緩存一直是打開的。

2、select標籤的useCache屬性:

  • 配置這個select是否使用二級緩存。一級緩存一直是使用的

3、sql標籤的flushCache屬性:

  • 增刪改默認flushCache=true。sql執行以後,會同時清空一級和二級緩存。
  • 查詢默認flushCache=false。

4、sqlSession.clearCache():

  • 只是用來清除一級緩存。

5、二級緩存需要手動開啓和配置,他是基於namespace級別的緩存。當在某一個作用域 (一級緩存Session/二級緩存Namespaces) 進行了 C/U/D 操作後,默認該作用域下所有 select 中的緩存將被clear。

緩存初始化

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

有了前面的基礎,這裏理解起來就非常容易了。

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

  @Override
  public Transaction getTransaction() {
    return delegate.getTransaction();
  }

  @Override
  public void close(boolean forceRollback) {
    try {
      //issues #499, #524 and #573
      if (forceRollback) { 
        tcm.rollback();
      } else {
        tcm.commit();
      }
    } finally {
      delegate.close(forceRollback);
    }
  }

  @Override
  public boolean isClosed() {
    return delegate.isClosed();
  }

  @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) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
    flushCacheIfRequired(ms);
    return delegate.queryCursor(ms, parameter, rowBounds);
  }

  @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, 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);
          //加入二級緩存。問題:如果一級緩存返回 PLACEHOLDER,這裏是否始終PLACEHOLDER,導致返不期望的結果?
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    //緩存沒有命中,從代理中查。繼續查詢一級緩存。
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  public List<BatchResult> flushStatements() throws SQLException {
    return delegate.flushStatements();
  }

  @Override
  public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }

  @Override
  public void rollback(boolean required) throws SQLException {
    try {
      delegate.rollback(required);
    } finally {
      if (required) {
        tcm.rollback();
      }
    }
  }

  private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
    if (ms.getStatementType() == StatementType.CALLABLE) {
      for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
        if (parameterMapping.getMode() != ParameterMode.IN) {
          throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
        }
      }
    }
  }

  @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
  }

  @Override
  public boolean isCached(MappedStatement ms, CacheKey key) {
    return delegate.isCached(ms, key);
  }

  @Override
  public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
    delegate.deferLoad(ms, resultObject, property, key, targetType);
  }

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

  private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);
    }
  }

  @Override
  public void setExecutorWrapper(Executor executor) {
    throw new UnsupportedOperationException("This method should not be called");
  }

}

驗證

  1. 不提交事務,sqlSession1查詢完數據後,sqlSession2相同的查詢是否會從緩存中獲取數據。

  2. 當提交事務時,sqlSession1查詢完數據後,sqlSession2相同的查詢是否會從緩存中獲取數據。

  3. 同一個事務 sqlSession 3次查詢同一個表,檢查後續請求是否查詢緩存

  4. 兩個 sqlSession,一個事務 sqlSession1,sqlSession2 各連續兩次查詢表,sqlSession3 在 sqlSession1 的兩次查詢中間修改表,

    SqlSession1 SqlSession2 SqlSession3
    查詢
    查詢 查詢
    提交
    查詢
    更新
    查詢
  5. 兩個 sqlSession,兩個事務 sqlSession1 各一次查詢表,sqlSession2 在兩次查詢中間修改表

總結

  1. Mybatis的二級緩存相對於一級緩存來說,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,能夠到Mapper級別,通過Cache接口實現類不同的組合,對Cache的可控性也更強。
  2. Mybatis在多表查詢時,極大可能會出現髒數據,有設計上的缺陷,安全使用的條件比較苛刻。
  3. 在分佈式環境下,由於默認的Mybatis Cache實現都是基於本地的,分佈式環境下必然會出現讀取到髒數據,需要使用集中式緩存將Mybatis的Cache接口實現,有一定的開發成本,不如直接用Redis,Memcache實現業務上的緩存就好了

總結

mybatis 的 cache 設計還是很讚的。尤其是初始化部分 CacheBuilder。

但是 mybatis 的緩存總體上來說不如直接上 Redis 好。原因在於一不小心就會出現髒讀。

參考

實例參考 https://www.jianshu.com/p/c553169c5921

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章