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

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

緩存在硬件和軟件應(yīng)用廣泛,我們在大學(xué)學(xué)過計算機與操作系統(tǒng)中接觸過高速緩存,閃存等。在工作中,我們也接觸過一些緩存中間件,比如Redis,MemCache。MyBatis作為一款優(yōu)秀的ORM框架,也提供了緩存的功能,減少訪問數(shù)據(jù)庫的次數(shù),從而提高性能。本文將和大家介紹MyBatis的實現(xiàn)和原理。

1. 初識緩存

MyBatis提供的緩存功能包含一級緩存和二級緩存,都是默認開啟的,它們的作用范圍也是不同的。MyBatis的緩存是基于cache接口的。cache接口的繼承關(guān)系如下

cache的繼承關(guān)系

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

public interface Cache {

  /**
   * 唯一標示緩存
   * @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);

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

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

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

}

PerpetualCache 是cache的默認實現(xiàn),也是最簡單的實現(xiàn),它以HashMap作為緩存容器,存儲緩存。其它類型的緩存是對PerpetualCache的包裝。

public class PerpetualCache implements Cache {
  
  private final String id;

  // 以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;
    return getId().equals(otherCache.getId());
  }

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

}

2. 一級緩存

2.1 一級緩存開啟

MyBatis一級緩存是默認開啟的,并且它的作用范圍是SqlSession級別的。我么知道SqlSession是頂層的接口,最終的數(shù)據(jù)庫操作都是交由給執(zhí)行器進行操作的。了解前面的Executor的同學(xué)可知,緩存就是在執(zhí)行Executor中進行維護的,其中l(wèi)ocalCache成員變量就是一級緩存對象,其類型就是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;
  }
}

一級緩存是默認開啟的,Configuration的成員變量localCacheScope的默認就是Sesssion級別的。

// Configuration類
protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;

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

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

如果某個select標簽查詢不需要緩存,在select標簽加上flushCache="true"也可以設(shè)置單個查詢關(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 一級緩存存取

緩存在查詢中才會用到,例如我們用同一個sql語句反復(fù)去查詢數(shù)據(jù)庫,并且在此期間沒有進行過數(shù)據(jù)修改操作,預(yù)期是返回相同的結(jié)果。如果沒有緩存,我們將每次都要訪問數(shù)據(jù)庫返回結(jié)果,這個過程無疑是浪費資源和消耗性能的。因此我們可以將第一次查詢的結(jié)果緩存在內(nèi)存中,第二次用相同的sql語句查詢的時候,先去緩存中查詢,如果命中則直接返回,否則去數(shù)據(jù)庫查詢并放到緩存中返回。我們接下來看看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標簽是否配置了flushCache=true
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      // 清除一級緩存
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      // 查詢一級緩存
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 處理緩存的結(jié)果
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // 緩存中沒有則查詢數(shù)據(jù)庫
        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)閉了一級緩存,查詢完后清除一級緩存
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

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

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

注意這個緩存真的是查詢sql完全一樣,這個一樣還包括參數(shù)的一致,才會從緩存中獲取到結(jié)果,那么如何判斷兩個查詢sql是否一樣呢。createCacheKey就幫忙解答了這個疑惑,它會給每個sql都生成一個key,如果兩個生成的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 一級緩存清除

在執(zhí)行update,commit,或者rollback操作的時候都會進行清除緩存操作,所有的緩存都將失效。

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

3. 二級緩存

一級緩存的作用范圍是SqlSession級別的,但是SqlSession是單線程的,不同線程間的操作會有一些臟數(shù)據(jù)的問題。二級緩存的范圍更大,是Mapper級別的緩存,因此不同sqlSession間可以共享緩存。

3.1 二級緩存開啟
  1. 開啟二級緩存需要配置cacheEnabled為true,這個屬性默認為true。
<setting name="cacheEnabled" value="true"/>
  1. 在需要進行開啟二級緩存的mapper中新增cache配置,cache配置有很多屬性。
  • type : 緩存實現(xiàn)類,默認是PerpetualCache,也可以是第三方緩存的實現(xiàn)

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

  • eviction:緩存回收策略,默認是LRU
    LRU:最近最少使用策略,回收最長時間不被使用的緩存
    FIFO:先進先出策略,回收最新進入的緩存
    SOFT - 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象
    WEAK - 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象

  • flushInterval:緩存刷新的間隔時間,默認是不刷新的

  • readOnly : 是否只讀,true 只會進行讀取操作,修改操作交由用戶處理
    false 可以進行讀取操作,也可以進行修改操作

  <cache type="org.apache.ibatis.cache.impl.PerpetualCache"
         size="1024"
         eviction="LRU"
         flushInterval="120000"
         readOnly="false"/>
  1. 也可以對單個Statement標簽進行關(guān)閉和開啟操作,通過配置useCache="true"來開啟緩存
  <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 二級緩存存取

二級緩存是Mapper級別的緩存,因此SqlSession是不可以管理的,我們再把目光轉(zhuǎn)向Executor,Executor在介紹的時候涉及到了CachingExecutor,在Configuration創(chuàng)建Executor的時候,如果開啟了二級緩存,就使用到了CachingExecutor進行了包裝。

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

CachingExecutor 中只有兩個成員變量,其中一個就是TransactionalCacheManager用來管理緩存。

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

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

public class TransactionalCacheManager {

  // 緩存,TransactionalCache實現(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);
  }

}

二級緩存的的存取過程是怎么樣的呢,我們可以看看CachingExecutor的query方法。如果Statement標簽配置了開啟緩存,則從緩存中去取,否則執(zhí)行執(zhí)行一級緩存的查詢邏輯。如果開啟了緩存,則先從二級緩存中查找,如果命中直接返回,否則執(zhí)行一級緩存的邏輯。因此當二級緩存開啟時,優(yōu)先從二級緩存中查找,再去從一級緩存中查找,最后從數(shù)據(jù)庫查找。

// CachingExecutor
@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) {
      // select標簽是否配置了flushCache屬性
      flushCacheIfRequired(ms);
      // 如果select標簽配置了useCache屬性
      if (ms.isUseCache() && resultHandler == null) {
        // 二級緩存不能緩存輸出類型的參數(shù)
        ensureNoOutParams(ms, boundSql);
        @SuppressWarnings("unchecked")
        // 獲取二級緩存  
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          // 如果二級緩存為空,則再去查詢一級緩存,如果一級緩存也沒命中,則查詢數(shù)據(jù)庫放到緩存中
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 二級緩存存儲時先保存在臨時屬性中,等事務(wù)提交再保存到真實的二級緩存
         // 緩存在一個中間變量
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // 沒開啟緩存
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
3.3 二級緩存清除

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

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

4. 例子

接下來我們將以兩個例子來更加清晰的介紹下一級緩存和二級緩存

4.1 一級緩存

一級緩存是SqlSession級別的緩存,如果用同一個sql執(zhí)行兩次相同的sql,第一次會執(zhí)行查詢打印sql,第二次則是直接從緩存中去獲取,不會打印sql,從日志可以看出來只打印了一次sql,說明第二次是從緩存中獲取的。

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

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

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

    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,說明第二次是從緩存中獲取的

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.

因為是SqlSession級別的,如果不同的SqlSession級別的執(zhí)行相同的sql,應(yīng)該互不影響,應(yīng)該會打印兩次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);

            // 開啟新的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,證明了一級緩存是SqlSession的級別的,不同的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 二級緩存

先開啟二級緩存

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

然后對應(yīng)的mapper中開啟緩存

  <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,說明緩存沒生效,配置都配置正確了,會有其它原因嗎

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
// 二級緩存存儲時先保存在臨時屬性中,等事務(wù)提交再保存到真實的二級緩存
   tcm.putObject(cache, key, list); // issue #578 and #116

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

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

接下來修改上述代碼為如下

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

            // 開啟新的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ù)提交才會將查詢結(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 中包含一級緩存和二級緩存,一級緩存的作用范圍是SqlSession級別的,二級緩存是Mapper級別的。
  • MyBatis 中的一級緩存和二級緩存都是默認開啟的,不過二級緩存還要額外在mapper和statement中配置緩存屬性
  • 一級緩存和二級緩存適用于讀多寫少的場景,如果頻繁的更新數(shù)據(jù),將降低查詢性能。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容