MyBatis原理系列(八)-手把手帶你了解一級(jí)緩存和二級(jí)緩存

MyBatis原理系列(一)-手把手帶你閱讀MyBatis源碼
MyBatis原理系列(二)-手把手帶你了解MyBatis的啟動(dòng)流程
MyBatis原理系列(三)-手把手帶你了解SqlSession,SqlSessionFactory,SqlSessionFactoryBuilder的關(guān)系
MyBatis原理系列(四)-手把手帶你了解MyBatis的Executor執(zhí)行器
MyBatis原理系列(五)-手把手帶你了解Statement浑侥、StatementHandler、MappedStatement間的關(guān)系
MyBatis原理系列(六)-手把手帶你了解BoundSql的創(chuàng)建過(guò)程
MyBatis原理系列(七)-手把手帶你了解如何自定義插件
MyBatis原理系列(八)-手把手帶你了解一級(jí)緩存和二級(jí)緩存
MyBatis原理系列(九)-手把手帶你了解MyBatis事務(wù)管理機(jī)制

緩存在硬件和軟件應(yīng)用廣泛,我們?cè)诖髮W(xué)學(xué)過(guò)計(jì)算機(jī)與操作系統(tǒng)中接觸過(guò)高速緩存,閃存等会宪。在工作中,我們也接觸過(guò)一些緩存中間件巍沙,比如Redis,MemCache削咆。MyBatis作為一款優(yōu)秀的ORM框架,也提供了緩存的功能蠢笋,減少訪問(wèn)數(shù)據(jù)庫(kù)的次數(shù)拨齐,從而提高性能。本文將和大家介紹MyBatis的實(shí)現(xiàn)和原理昨寞。

1. 初識(shí)緩存

MyBatis提供的緩存功能包含一級(jí)緩存和二級(jí)緩存瞻惋,都是默認(rèn)開(kāi)啟的,它們的作用范圍也是不同的援岩。MyBatis的緩存是基于cache接口的蹂匹。cache接口的繼承關(guān)系如下

cache的繼承關(guān)系

cache作為頂層接口,定義了緩存的基本操作庶近,比如設(shè)置緩存,獲取緩存的方法。

public interface Cache {

  /**
   * 唯一標(biāo)示緩存
   * @return
   */
  String getId();

  /**
   * 以key value形式設(shè)置緩存
   * @param key
   * @param value
   */
  void putObject(Object key, Object value);

  /**
   * 獲取緩存
   * @param key
   * @return
   */
  Object getObject(Object key);

  /**
   * 刪除緩存
   */
  Object removeObject(Object key);

  /**
   * 清空緩存實(shí)例
   */
  void clear();

  /**
   * 緩存中元素的數(shù)量
   * @return
   */
  int getSize();

  /**
   * 讀寫(xiě)鎖
   * @return
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

PerpetualCache 是cache的默認(rèn)實(shí)現(xiàn)钞螟,也是最簡(jiǎn)單的實(shí)現(xiàn)熔任,它以HashMap作為緩存容器,存儲(chǔ)緩存。其它類(lèi)型的緩存是對(duì)PerpetualCache的包裝交洗。

public class PerpetualCache implements Cache {
  
  private final String id;

  // 以map存儲(chǔ)緩存
  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;
    return getId().equals(otherCache.getId());
  }

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

}

2. 一級(jí)緩存

2.1 一級(jí)緩存開(kāi)啟

MyBatis一級(jí)緩存是默認(rèn)開(kāi)啟的蜜笤,并且它的作用范圍是SqlSession級(jí)別的暖混。我么知道SqlSession是頂層的接口泪勒,最終的數(shù)據(jù)庫(kù)操作都是交由給執(zhí)行器進(jìn)行操作的税产。了解前面的Executor的同學(xué)可知,緩存就是在執(zhí)行Executor中進(jìn)行維護(hù)的为居,其中l(wèi)ocalCache成員變量就是一級(jí)緩存對(duì)象,其類(lèi)型就是PerpetualCache楼入。

public abstract class BaseExecutor implements Executor {

  private static final Log log = LogFactory.getLog(BaseExecutor.class);

  protected Transaction transaction;
  protected Executor wrapper;

  protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;

  protected int queryStack;
  private boolean closed;

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }
}

一級(jí)緩存是默認(rèn)開(kāi)啟的削罩,Configuration的成員變量localCacheScope的默認(rèn)就是Sesssion級(jí)別的。

// Configuration類(lèi)
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

如果要關(guān)閉伴鳖,我們可以在mybatis-config.xml中的settings標(biāo)簽中將這個(gè)配置設(shè)置成Statement類(lèi)型的

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

如果某個(gè)select標(biāo)簽查詢不需要緩存宛徊,在select標(biāo)簽加上flushCache="true"也可以設(shè)置單個(gè)查詢關(guān)閉緩存

  <select id="selectByPrimaryKey" parameterType="java.lang.Long" 
          resultMap="BaseResultMap" flushCache="true">
    select 
    <include refid="Base_Column_List" />
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>
2.1 一級(jí)緩存存取

緩存在查詢中才會(huì)用到定硝,例如我們用同一個(gè)sql語(yǔ)句反復(fù)去查詢數(shù)據(jù)庫(kù)坝疼,并且在此期間沒(méi)有進(jìn)行過(guò)數(shù)據(jù)修改操作膀钠,預(yù)期是返回相同的結(jié)果誉结。如果沒(méi)有緩存,我們將每次都要訪問(wèn)數(shù)據(jù)庫(kù)返回結(jié)果雌澄,這個(gè)過(guò)程無(wú)疑是浪費(fèi)資源和消耗性能的屎媳。因此我們可以將第一次查詢的結(jié)果緩存在內(nèi)存中湃崩,第二次用相同的sql語(yǔ)句查詢的時(shí)候薄扁,先去緩存中查詢面哥,如果命中則直接返回县匠,否則去數(shù)據(jù)庫(kù)查詢并放到緩存中返回顶瞳。我們接下來(lái)看看BaseExecutor的query方法是怎么做的吧玖姑。

@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());
    // Executor是否關(guān)閉
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    // select標(biāo)簽是否配置了flushCache=true
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      // 清除一級(jí)緩存
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 查詢一級(jí)緩存
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 處理緩存的結(jié)果
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 緩存中沒(méi)有則查詢數(shù)據(jù)庫(kù)
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      // 如果關(guān)閉了一級(jí)緩存,查詢完后清除一級(jí)緩存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

第一次查詢肯定從緩存中查詢不到東西慨菱,于是走向了queryFromDatabase分支焰络,這個(gè)方法就直接從數(shù)據(jù)庫(kù)中去查詢

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 添加占位符,標(biāo)示正在執(zhí)行
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 調(diào)用子類(lèi)的查詢方法獲取結(jié)果
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    // 將查詢結(jié)果放到緩存中
    localCache.putObject(key, list);
    // 如果是存儲(chǔ)過(guò)程則需要處理輸出參數(shù)
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

注意這個(gè)緩存真的是查詢sql完全一樣符喝,這個(gè)一樣還包括參數(shù)的一致闪彼,才會(huì)從緩存中獲取到結(jié)果,那么如何判斷兩個(gè)查詢sql是否一樣呢。createCacheKey就幫忙解答了這個(gè)疑惑畏腕,它會(huì)給每個(gè)sql都生成一個(gè)key缴川,如果兩個(gè)生成的key一致,那就表明不管是sql還是參數(shù)都是一致的。

 @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());
    cacheKey.update(rowBounds.getLimit());
    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);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }
2.3 一級(jí)緩存清除

在執(zhí)行update,commit沟娱,或者rollback操作的時(shí)候都會(huì)進(jìn)行清除緩存操作,所有的緩存都將失效恋日。

  @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.");
    }
    // 清除一級(jí)緩存
    clearLocalCache();
    return doUpdate(ms, parameter);
  }

3. 二級(jí)緩存

一級(jí)緩存的作用范圍是SqlSession級(jí)別的,但是SqlSession是單線程的嘹狞,不同線程間的操作會(huì)有一些臟數(shù)據(jù)的問(wèn)題谚鄙。二級(jí)緩存的范圍更大,是Mapper級(jí)別的緩存刁绒,因此不同sqlSession間可以共享緩存。

3.1 二級(jí)緩存開(kāi)啟
  1. 開(kāi)啟二級(jí)緩存需要配置cacheEnabled為true烤黍,這個(gè)屬性默認(rèn)為true知市。
<setting name="cacheEnabled" value="true"/>
  1. 在需要進(jìn)行開(kāi)啟二級(jí)緩存的mapper中新增cache配置,cache配置有很多屬性速蕊。
  • type : 緩存實(shí)現(xiàn)類(lèi)嫂丙,默認(rèn)是PerpetualCache,也可以是第三方緩存的實(shí)現(xiàn)

  • size:最多緩存對(duì)象的個(gè)數(shù)

  • eviction:緩存回收策略规哲,默認(rèn)是LRU
    LRU:最近最少使用策略跟啤,回收最長(zhǎng)時(shí)間不被使用的緩存
    FIFO:先進(jìn)先出策略唉锌,回收最新進(jìn)入的緩存
    SOFT - 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對(duì)象
    WEAK - 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對(duì)象

  • flushInterval:緩存刷新的間隔時(shí)間隅肥,默認(rèn)是不刷新的

  • readOnly : 是否只讀,true 只會(huì)進(jìn)行讀取操作袄简,修改操作交由用戶處理
    false 可以進(jìn)行讀取操作腥放,也可以進(jìn)行修改操作

  <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
         size="1024"
         eviction="LRU"
         flushInterval="120000"
         readOnly="false"/>
  1. 也可以對(duì)單個(gè)Statement標(biāo)簽進(jìn)行關(guān)閉和開(kāi)啟操作,通過(guò)配置useCache="true"來(lái)開(kāi)啟緩存
  <select id="selectByPrimaryKey" parameterType="java.lang.Long"
          resultMap="BaseResultMap" useCache="true">
    select 
    <include refid="Base_Column_List" />
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>
3.2 二級(jí)緩存存取

二級(jí)緩存是Mapper級(jí)別的緩存绿语,因此SqlSession是不可以管理的秃症,我們?cè)侔涯抗廪D(zhuǎn)向Executor,Executor在介紹的時(shí)候涉及到了CachingExecutor吕粹,在Configuration創(chuàng)建Executor的時(shí)候种柑,如果開(kāi)啟了二級(jí)緩存,就使用到了CachingExecutor進(jìn)行了包裝匹耕。

// Configuration
  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);
    }
    // 是否開(kāi)啟了二級(jí)緩存
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 創(chuàng)建插件對(duì)象
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

CachingExecutor 中只有兩個(gè)成員變量聚请,其中一個(gè)就是TransactionalCacheManager用來(lái)管理緩存。

 // 1. 委托執(zhí)行器泌神,也就是被包裝的三種執(zhí)行器的中的一種
  private final Executor delegate;
  // 2. 緩存管理類(lèi)良漱,用來(lái)管理TransactionalCache
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

TransactionalCacheManager 結(jié)構(gòu)也比較簡(jiǎn)單舞虱,內(nèi)部也維護(hù)著一個(gè)HashMap緩存,其中TransactionalCache實(shí)現(xiàn)了Cache接口母市。

public class TransactionalCacheManager {

  // 緩存矾兜,TransactionalCache實(shí)現(xiàn)了Cache接口
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

  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) {
    return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
  }

}

二級(jí)緩存的的存取過(guò)程是怎么樣的呢,我們可以看看CachingExecutor的query方法患久。如果Statement標(biāo)簽配置了開(kāi)啟緩存椅寺,則從緩存中去取,否則執(zhí)行執(zhí)行一級(jí)緩存的查詢邏輯蒋失。如果開(kāi)啟了緩存返帕,則先從二級(jí)緩存中查找,如果命中直接返回篙挽,否則執(zhí)行一級(jí)緩存的邏輯荆萤。因此當(dāng)二級(jí)緩存開(kāi)啟時(shí),優(yōu)先從二級(jí)緩存中查找铣卡,再去從一級(jí)緩存中查找链韭,最后從數(shù)據(jù)庫(kù)查找。

// CachingExecutor
@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 獲取二級(jí)緩存配置標(biāo)簽
    Cache cache = ms.getCache();
    if (cache != null) {
      // select標(biāo)簽是否配置了flushCache屬性
      flushCacheIfRequired(ms);
      // 如果select標(biāo)簽配置了useCache屬性
      if (ms.isUseCache() && resultHandler == null) {
        // 二級(jí)緩存不能緩存輸出類(lèi)型的參數(shù)
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 獲取二級(jí)緩存  
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 如果二級(jí)緩存為空煮落,則再去查詢一級(jí)緩存敞峭,如果一級(jí)緩存也沒(méi)命中,則查詢數(shù)據(jù)庫(kù)放到緩存中
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 二級(jí)緩存存儲(chǔ)時(shí)先保存在臨時(shí)屬性中蝉仇,等事務(wù)提交再保存到真實(shí)的二級(jí)緩存
         // 緩存在一個(gè)中間變量
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 沒(méi)開(kāi)啟緩存
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
3.3 二級(jí)緩存清除

清空緩存也是在執(zhí)行更新操作的時(shí)候進(jìn)行刪除緩存

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 清空緩存
    flushCacheIfRequired(ms);
    // 調(diào)用實(shí)際執(zhí)行器的update方法
    return delegate.update(ms, parameterObject);
  }

4. 例子

接下來(lái)我們將以兩個(gè)例子來(lái)更加清晰的介紹下一級(jí)緩存和二級(jí)緩存

4.1 一級(jí)緩存

一級(jí)緩存是SqlSession級(jí)別的緩存旋讹,如果用同一個(gè)sql執(zhí)行兩次相同的sql,第一次會(huì)執(zhí)行查詢打印sql轿衔,第二次則是直接從緩存中去獲取沉迹,不會(huì)打印sql,從日志可以看出來(lái)只打印了一次sql害驹,說(shuō)明第二次是從緩存中獲取的胚股。

先將二級(jí)緩存關(guān)閉

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

然后執(zhí)行兩次相同的語(yǔ)句

    public static void main(String[] args) {
        try {
            // 1. 讀取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 創(chuàng)建SqlSessionFactory工廠
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 獲取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 獲取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 執(zhí)行接口方法
            TTestUser user = userMapper.selectByPrimaryKey(1000L);
            TTestUser user1 = userMapper.selectByPrimaryKey(1000L);
            // 6. 提交事物
            sqlSession.commit();
            // 7. 關(guān)閉資源
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

最后打印了一次sql,說(shuō)明第二次是從緩存中獲取的

16:37:33.088 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:37:35.027 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1995250556.
16:37:35.028 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.050 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:37:35.108 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:37:35.171 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:37:35.174 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.191 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@76ed1b7c]
16:37:35.191 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1995250556 to pool.

因?yàn)槭荢qlSession級(jí)別的裙秋,如果不同的SqlSession級(jí)別的執(zhí)行相同的sql琅拌,應(yīng)該互不影響,應(yīng)該會(huì)打印兩次sql摘刑,我們將上面的代碼稍微修改下

public static void main(String[] args) {
        try {
            // 1. 讀取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 創(chuàng)建SqlSessionFactory工廠
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 獲取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 獲取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 執(zhí)行接口方法
            TTestUser user = userMapper.selectByPrimaryKey(1000L);

            // 開(kāi)啟新的sqlSession
            SqlSession sqlSession2 = sqlSessionFactory.openSession();
            TTestUserMapper userMapper2 = sqlSession2.getMapper(TTestUserMapper.class);
            TTestUser user2 = userMapper2.selectByPrimaryKey(1000L);
            // 6. 提交事物
            sqlSession.commit();
            // 7. 關(guān)閉資源
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

打印了兩次sql进宝,證明了一級(jí)緩存是SqlSession的級(jí)別的,不同的SqlSession間不能共享緩存枷恕。

16:44:06.871 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:44:08.297 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 34073107.
16:44:08.297 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.316 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:44:08.365 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:44:08.447 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:44:08.448 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:44:08.717 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 1527254842.
16:44:08.718 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b080f3a]
16:44:08.740 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:44:08.741 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:44:08.764 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:44:08.764 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.788 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@207ea13]
16:44:08.789 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 34073107 to pool.

4.1 二級(jí)緩存

先開(kāi)啟二級(jí)緩存

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

然后對(duì)應(yīng)的mapper中開(kāi)啟緩存

  <select id="selectByPrimaryKey" parameterType="java.lang.Long"
          resultMap="BaseResultMap" useCache="true">
    select 
    <include refid="Base_Column_List" />
    from t_test_user
    where id = #{id,jdbcType=BIGINT}
  </select>

  <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
         size="1024"
         eviction="LRU"
         flushInterval="120000"
         readOnly="false"/>

復(fù)用上面的代碼党晋,我們看看不同SqlSession間是否能夠共享緩存。
發(fā)現(xiàn)還是打印了2次sql,說(shuō)明緩存沒(méi)生效未玻,配置都配置正確了灾而,會(huì)有其它原因嗎

16:56:34.043 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:56:35.278 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 316335490.
16:56:35.279 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.292 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:56:35.341 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:56:35.386 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:56:35.387 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.0
16:56:35.387 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:56:35.544 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 375074687.
16:56:35.544 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@165b2f7f]
16:56:35.560 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
16:56:35.560 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
16:56:35.571 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
16:56:35.583 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.602 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
16:56:35.602 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 316335490 to pool.

再看看CachingExecutor中的query方法,有這一行代碼

// CachingExecutor
// 二級(jí)緩存存儲(chǔ)時(shí)先保存在臨時(shí)屬性中扳剿,等事務(wù)提交再保存到真實(shí)的二級(jí)緩存
   tcm.putObject(cache, key, list); // issue #578 and #116

再看看CachingExecutor的commit方法旁趟,在commit的時(shí)候才會(huì)將緩存放到真正的緩存中,這樣做的目的就是為了防止不通SqlSession間的臟讀庇绽,一個(gè)SqlSession讀取了另一個(gè)SqlSession還未提交的數(shù)據(jù)锡搜。

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

接下來(lái)修改上述代碼為如下

 public static void main(String[] args) {
        try {
            // 1. 讀取配置
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. 創(chuàng)建SqlSessionFactory工廠
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. 獲取sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. 獲取Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. 執(zhí)行接口方法
            TTestUser user = userMapper.selectByPrimaryKey(1000L);
            sqlSession.commit();

            // 開(kāi)啟新的sqlSession
            SqlSession sqlSession2 = sqlSessionFactory.openSession();
            TTestUserMapper userMapper2 = sqlSession2.getMapper(TTestUserMapper.class);
            TTestUser user2 = userMapper2.selectByPrimaryKey(1000L);
            sqlSession2.commit();

            // 7. 關(guān)閉資源
            sqlSession.close();
            sqlSession2.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
    }

第一次查詢提交了事務(wù)后,第二次直接命中了緩存瞧掺,從而印證了事務(wù)提交才會(huì)將查詢結(jié)果放到緩存中耕餐。

17:08:20.993 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.0
17:08:21.011 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
17:08:22.568 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 316335490.
17:08:22.568 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.589 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==>  Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user where id = ? 
17:08:22.643 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - ==> Parameters: 1000(Long)
17:08:22.692 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPrimaryKey - <==      Total: 1
17:08:22.706 [main] DEBUG com.example.demo.dao.TTestUserMapper - Cache Hit Ratio [com.example.demo.dao.TTestUserMapper]: 0.5
17:08:22.707 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.733 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12dae582]
17:08:22.733 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 316335490 to pool.

5. 總結(jié)

  • MyBatis 中包含一級(jí)緩存和二級(jí)緩存,一級(jí)緩存的作用范圍是SqlSession級(jí)別的辟狈,二級(jí)緩存是Mapper級(jí)別的肠缔。
  • MyBatis 中的一級(jí)緩存和二級(jí)緩存都是默認(rèn)開(kāi)啟的,不過(guò)二級(jí)緩存還要額外在mapper和statement中配置緩存屬性
  • 一級(jí)緩存和二級(jí)緩存適用于讀多寫(xiě)少的場(chǎng)景哼转,如果頻繁的更新數(shù)據(jù)桩砰,將降低查詢性能。

參考 給我五分鐘释簿,帶你徹底掌握 MyBatis 緩存的工作原理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市硼莽,隨后出現(xiàn)的幾起案子庶溶,更是在濱河造成了極大的恐慌,老刑警劉巖懂鸵,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偏螺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡匆光,警方通過(guò)查閱死者的電腦和手機(jī)套像,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)终息,“玉大人夺巩,你說(shuō)我怎么就攤上這事≈苷福” “怎么了柳譬?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)续镇。 經(jīng)常有香客問(wèn)我美澳,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任制跟,我火速辦了婚禮舅桩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雨膨。我一直安慰自己擂涛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布哥放。 她就那樣靜靜地躺著歼指,像睡著了一般。 火紅的嫁衣襯著肌膚如雪甥雕。 梳的紋絲不亂的頭發(fā)上踩身,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音社露,去河邊找鬼挟阻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛峭弟,可吹牛的內(nèi)容都是我干的附鸽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瞒瘸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼坷备!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起情臭,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤省撑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后俯在,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體竟秫,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年跷乐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肥败。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡愕提,死狀恐怖馒稍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浅侨,我是刑警寧澤筷黔,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站仗颈,受9級(jí)特大地震影響佛舱,放射性物質(zhì)發(fā)生泄漏椎例。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一请祖、第九天 我趴在偏房一處隱蔽的房頂上張望订歪。 院中可真熱鬧,春花似錦肆捕、人聲如沸刷晋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)眼虱。三九已至,卻和暖如春席纽,著一層夾襖步出監(jiān)牢的瞬間捏悬,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工润梯, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留过牙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓纺铭,卻偏偏與公主長(zhǎng)得像寇钉,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子舶赔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容