日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

吃透Mybatis源碼-緩存的理解(三)

作者:墨家巨子@俏如來 更新時間: 2022-01-21 編程語言

來來來,給俏如來扎起。感謝老鐵們對俏如來的支持,2021一路有你,2022我們繼續(xù)加油!你的肯定是我最大的動力

博主在參加博客之星評比,點擊鏈接 , https://bbs.csdn.net/topics/603957267 瘋狂打Call!五星好評 ????? 感謝


前言

對于Mybatis的緩存在上一章節(jié)《吃透Mybatis源碼-Mybatis執(zhí)行流程》我們有提到一部分,這篇文章我們對將詳細分析一下Mybatis的一級緩存和二級緩存。

一級緩存

市面上流行的ORM框架都支持緩存,不管是Hibernate還是Mybatis都支持一級緩存和二級緩存,目的是把數據緩存到JVM內存中,減少和數據庫的交互來提高查詢速度。同時MyBatis還可以整合三方緩存技術。

Mybatis一級緩默認開啟,是SqlSession級別的,也就是說需要同一個SqlSession執(zhí)行同樣的SQL和參數才有可能命中緩存。如:
在這里插入圖片描述
同一個SqlSession執(zhí)行同一個SQL,發(fā)現控制臺日志只執(zhí)行了一次SQL記錄,說明第二次查詢是走緩存了。但是要注意的是,當SqlSession執(zhí)行了delete,update,insert語句后,緩存會被清除。

那么一級緩存在哪兒呢?下面給大家介紹一個類。
在這里插入圖片描述
Mybatis中提供的緩存都是Cache的實現類,但是真正實現緩存的是PerpetualCache,其中維護了一個Map<Object, Object> cache = new HashMap<Object, Object>() 結構來緩存數據。其他的緩存類采用了裝飾模式對PerpetualCache做增強。比如:LruCache 在PerpetualCache 的基礎上增加了最近最少使用的緩存清楚策略,當緩存到達上限時候,刪除最近最少使用的緩存 (Least Recently Use)。代碼如下

public class LruCache implements Cache {
	//對 PerpetualCache 做裝飾
  private final Cache delegate;

下面對其他的緩存類做一個介紹

  • PerpetualCache : 基礎緩存類
  • LruCache : LRU 策略的緩存 當緩存到達上限時候,刪除最近最少使用的緩存 (Least Recently Use),eviction=“LRU”(默 認)
  • FifoCache : FIFO 策略的緩存 當緩存到達上限時候,刪除最先入隊的緩存,配置eviction=“FIFO”
  • SoftCache WeakCache :帶清理策略的緩存 通過 JVM 的軟引用和弱引用來實現緩存,當 JVM 內存不足時,會自動清理掉這些緩存,基于 SoftReference 和 WeakReference
  • SynchronizedCache : 同步緩存 基于 synchronized 關鍵字實現,解決并發(fā)問題
  • ScheduledCache : 定時調度的緩存,在進行 get/put/remove/getSize 等操作前,判斷 緩存時間是否超過了設置的最長緩存時間(默認是 一小時),如果是則清空緩存–即每隔一段時間清 空一次緩存
  • SerializedCache :支持序列化的緩存 將對象序列化以后存到緩存中,取出時反序列化
  • TransactionalCache :事務緩存,在二級緩存中使用,可一次存入多個緩存,移除多個緩存 。通過TransactionalCacheManager 中用 Map 維護對應關系。

一級緩存到底存儲在哪兒?

一級緩存在SimpleExecutor 的父類 BaseExecutor 執(zhí)行器中,如下

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;

PerpetualCache緩存類源碼如下

public class PerpetualCache implements Cache {

  private final String id;
  //緩存
  private Map<Object, Object> cache = new HashMap<Object, Object>();

那么一級緩存在什么時候創(chuàng)建的?

在 BaseExecutor 中的構造器中創(chuàng)建了一級緩存,而執(zhí)行器Executor 是保存在SqlSession中的,也就是說當創(chuàng)建SqlSession的時候,就會創(chuàng)建 SimpleExecutor,而在SimpleExecutor的構造器中會調用BaseExecutor的構造器來創(chuàng)建一級緩存。見:org.apache.ibatis.executor.SimpleExecutor#SimpleExecutor

public class SimpleExecutor extends BaseExecutor {
	//執(zhí)行器構造器
  public SimpleExecutor(Configuration configuration, Transaction transaction) {
  	//調用父類構造器
    super(configuration, transaction);
  }

下面是 BaseExecutor 的執(zhí)行器 org.apache.ibatis.executor.BaseExecutor#BaseExecutor

public abstract class BaseExecutor implements Executor {

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

  protected Transaction transaction;
  protected Executor wrapper;

  //一級緩存
  protected PerpetualCache localCache;
  protected PerpetualCache localOutputParameterCache;
  protected Configuration configuration;


  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
    //創(chuàng)建一級緩存
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }

一級緩存怎么存儲的?

一級緩存是在執(zhí)行查詢的時候會先走二級緩存,二級緩存么有就會走一級緩存,以及緩存沒有就會走數據庫查詢,然后放入一級緩存和二級緩存。我們來看一下源碼流程 ,見:org.apache.ibatis.executor.CachingExecutor#query

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //構建緩存的Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    //執(zhí)行查詢
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

這里在嘗試構建Cachekey ,cachekey時由:MappedStatement的id(如:cn.xx.xx.xxMapper.selectByid) ,分頁,Sql,參數值一起構建而成的,一級二級緩存都是如此。

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    //開啟了二級緩存才會存在Cache  
    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);
          //寫入二級緩存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

這里我們看到,在執(zhí)行org.apache.ibatis.executor.CachingExecutor#query 查詢的時候會先走二級緩存,二級緩存沒有會繼續(xù)調用 org.apache.ibatis.executor.BaseExecutor#query 查詢,而BaseExecutor#query會嘗試先走一級緩存

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

上面代碼會先走一級緩存拿數據,如果一級緩存沒有,就走數據庫獲取數據,然后加入一級緩存org.apache.ibatis.executor.BaseExecutor#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 {
      //走數據庫查詢數據
      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;
  }

到這里我們就看到了一級緩存和二級緩存的執(zhí)行流程,注意的是:先執(zhí)行二級緩存再執(zhí)行一級緩存。

這里畫一個一級緩存的圖
在這里插入圖片描述

二級緩存

第一步:二級緩存需要在mybatis-config.xml 配置中開啟,如下

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

當然其實該配置默認是開啟的,也就是默認會使用 CachingExecutor 裝飾基本的執(zhí)行器。
第二步驟:需要在mapper.xml中配置 < cache/>如下

<mapper namespace="cn.whale.mapper.StudentMapper">
	<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
		 size="1024" 
		 eviction="LRU" 
		 flushInterval="120000" 
		 readOnly="false"/> 
...省略...

解釋一下上面的配置,首先<cache/> 是在某個mapper.xml中指定的,也就是說二級緩存作用于當前的namespace.

  • type : 代表的是使用什么類型的緩存,只要是實現了 Cache 接口的實現類都可以
  • size :緩存的個數,默認是1024 個對象
  • eviction : 緩存剔除策略 ,LRU – 最近最少使用的:移除最長時間不被使用的對象(默認);FIFO – 先進先出:按對象進入緩存的順序來移除它們 ;SOFT – 軟引用:移除基于垃圾回收器狀態(tài)和軟引用規(guī)則的對象;WEAK – 弱引用:更積極地移除基于垃圾收集器狀態(tài)和弱引用規(guī)則的對象
  • flushInterval :定時自動清空緩存間隔 自動刷新時間,單位 ms,未配置時只有調用時刷新
  • readOnly :緩存時候只讀
  • blocking :是否使用可重入鎖實現 緩存的并發(fā)控制 true,會使用 BlockingCache 對 Cache 進行裝飾 默認 false

Mapper.xml 配置了之后,select()會被緩存。update()、delete()、insert() 會刷新緩存,下面是測試案例
在這里插入圖片描述

可以看到,這里使用了2個SqlSesion 2次執(zhí)行了相同的SQL,參數相同,看控制臺日志只執(zhí)行了一次SQL,說明是命中的二級緩存。因為滿足條件:同一個 namespace下的相同的SQL被執(zhí)行,盡管使用的SqlSession不是同一個。

但是你可能注意到一個細節(jié),就是session.commit() 為什么要提交事務呢?這就要說到二級緩存的存儲結構了,如果不執(zhí)行commit是不會寫入二級緩存的。在 CachingExecutor 中有一個屬性private final TransactionalCacheManager tcm = new TransactionalCacheManager(); 看名字肯能夠看出二級緩存和事務有關系。結構如下

public class CachingExecutor implements Executor {

  private final Executor delegate;
  //二級緩存,通過TransactionalCacheManager來管理
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

TransactionalCacheManager 中維護了一個 HashMap<Cache, TransactionalCache>()

public class TransactionalCacheManager {
  //二級緩存的HashMap
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

在TransactionCache中維護了一個 Map<Object, Object> entriesToAddOnCommit;

public class TransactionalCache implements Cache {

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

  private final Cache delegate;
  private boolean clearOnCommit;
  //二級緩存臨時存儲
  private final Map<Object, Object> entriesToAddOnCommit;

  ...省略...
  //寫入二級緩存
  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

當執(zhí)行查詢的時候,從數據庫查詢出來數據回寫入TransactionalCache的entriesToAddOnCommit中,我們來看一下二級緩存寫入的流程,見:org.apache.ibatis.executor.CachingExecutor#query

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
      //如果mapper.xml配置了 <cache/> 就會創(chuàng)建 Cache
    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);
          //寫入二級緩存
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

如果mapper.xml配置了 就會創(chuàng)建 Cache,Cache不為null,才會走到二級緩存的流程,此時代碼來到org.apache.ibatis.cache.TransactionalCacheManager#putObject

public class TransactionalCacheManager {
  //存儲二級緩存
  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

	public void putObject(Cache cache, CacheKey key, Object value) {
	//通過cache為key拿到 TransactionalCache ,把數據put進去
    getTransactionalCache(cache).putObject(key, value);
  }

存儲數據的是TransactionalCache ,見org.apache.ibatis.cache.decorators.TransactionalCache#putObject

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //正在的二級緩存存儲位置
  private final Cache delegate;
  private boolean clearOnCommit;
  //臨時的二級緩存存儲位置
  private final Map<Object, Object> entriesToAddOnCommit;

  @Override
  public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
  }

我們看到,數據寫到了 TransactionalCache#entriesToAddOnCommit 一個Map中。只有在執(zhí)行commit的時候數據才會真正寫入二級緩存。

我們來看下SqlSession.commit方法是如何觸發(fā)二級緩存真正的寫入的,見:org.apache.ibatis.session.defaults.DefaultSqlSession#commit()

  @Override
  public void commit() {
    commit(false);
  }

  @Override
  public void commit(boolean force) {
    try {
    //調用執(zhí)行器提交事務
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

代碼來到org.apache.ibatis.executor.CachingExecutor#commit

@Override
  public void commit(boolean required) throws SQLException {
    //提交事務
    delegate.commit(required);
    //調用org.apache.ibatis.cache.TransactionalCacheManager#commit提交事務
    tcm.commit();
  }

代碼來到org.apache.ibatis.cache.TransactionalCacheManager#commit

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      //調用 TransactionalCache#commit
      txCache.commit();
    }
  }

代碼來到org.apache.ibatis.cache.decorators.TransactionalCache#commit

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  //真正的二級緩存存儲位置,本質是一個 PerpetualCache
  private final Cache delegate;
  //臨時存儲二級緩存
  private final Map<Object, Object> entriesToAddOnCommit;
  
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    //這里在寫入緩存,保存到TransactionalCache中的delegate字段,本質是一個PerpetualCache
    flushPendingEntries();
    //把entriesToAddOnCommit清除掉
    reset();
  }
  
  private void flushPendingEntries() {
	    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
	      //從entriesToAddOnCommit中拿到臨時的緩存數據,寫入緩存,最終會寫入PerpetualCache#cache字段中
	      delegate.putObject(entry.getKey(), entry.getValue());
	    }
	    for (Object entry : entriesMissedInCache) {
	      if (!entriesToAddOnCommit.containsKey(entry)) {
	        delegate.putObject(entry, null);
	      }
	    }
   }
	
	private void reset() {
	    clearOnCommit = false;
	    //清除entriesToAddOnCommit
	    entriesToAddOnCommit.clear();
	    entriesMissedInCache.clear();
  }

所以我們總結一下二級緩存的寫入流程,二級緩存通過 TransactionalCacheManager中的一個Map<Cache, TransactionalCache>管理的,當執(zhí)行query查詢處數據的時候,會把數據寫入TransactionalCache中的 Map<Object, Object> entriesToAddOnCommit 中臨時存儲。當執(zhí)行commit的時候才會把entriesToAddOnCommit中的數據寫入TransactionalCache中的 Cache delegate ,其本質和一級緩存一樣,也是一個 PerpetualCache

當我們做第二次query的時候會嘗試通過 TransactionalCacheManager#getObject 從二級緩存獲取數據

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
  //獲取二級緩存
  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }

然后會從 TransactionalCache中的delegate中獲取緩存

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);
 //二級緩存
  private final Cache delegate;
  ...省略...
  
  @Override
  public Object getObject(Object key) {
    // issue #116
    //從二級緩存獲取數據
    Object object = delegate.getObject(key);
    if (object == null) {
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

所以記得,二級緩存一定要commit才會起作用。下面花了一個一級緩存和二級緩存的結構圖
在這里插入圖片描述

三方緩存框架

除了使用Mybatis自帶的緩存,也可以使用第三方緩存方式,比如:比如 ehcache 和 redis 下面以Redis為例 ,首先導入mybatis整合redis的依賴

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

第二步驟:在mapper.xml配置緩存

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

這里type使用了RedisCache,RedisCache也是實現了Cache接口的,接著我們需要配置Redis的鏈接屬性,默認RedisCache類會讀取名字為 : redis.properties 的配置文件

host=127.0.0.1
password=123456
port=6379
connectionTimeout=5000
soTimeout=5000
database=0

再次執(zhí)行測試代碼,查看Redis效果如下
在這里插入圖片描述
博主在參加博客之星評比,點擊鏈接 , https://bbs.csdn.net/topics/603957267 瘋狂打Call!五星好評 ????? 感謝

原文鏈接:https://blog.csdn.net/u014494148/article/details/122313499

欄目分類
最近更新