<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                了解過Mybatis的同學,大概都知道Mybatis有一級緩存和二級緩存;但是我們使用Mybatis時一般使用默認的配置,對緩存的原理知之甚少。之前介紹Executor的文章中提到了Mybatis緩存相關的內容,但是比較瑣碎,不成體系。 今天通過這篇文章對Mybatis的緩存機制做下詳細解讀,對一級緩存、二級緩存的執行流程、工作原理有個全面的認識,方便開發過程中合理使用。 ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/76827989df754f16804d217e0ee00191~tplv-k3u1fbpfcp-zoom-1.image) 還是先通過一個“全景圖”對Mybatis的緩存機制有個整體了解,然后結合整體流程,對每個環節逐個剖析。如圖所示,Mybatis一級緩存是在BaseExecutor中實現,并且一級緩存僅在SqlSession生命周期內有效;二級緩存是在CachingExecutor實現,二級緩存是在namespace維度且全局共享。 若一二級緩存同時開啟,當執行查詢時:Mybatis先查詢二級緩存;如果二級緩存未命中,則繼續查詢一級緩存;若一級緩存未命中,則查詢數據庫并更新一二級緩存。默認情況下,一級緩存是開啟的,而且沒有辦法關閉;二級緩存默認關閉。大體了解Mybatis緩存機制后,我們逐個了解一下一二級緩存分別是怎么實現的。 ## 一級緩存 先來看下比較簡單的一級緩存。Mybatis一級緩存在BaseExecutor中實現,使用Cache接口實現類PerpetualCache作為緩存存儲的容器,通過源碼可知其內部就是一個HashMap。這就跟我們平時自己做K-V存儲或者使用Redis做緩存的思路類似。那么,我們自然而然的需要弄清楚以下幾個問題: * Mybatis是如何寫緩存、何時寫緩存? * Mybatis如何讀緩存、何時讀緩存? * Mybatis何時使緩存失效,緩存失效采用了什么策略? * Cache接口及實現類提供了哪些接口方法,Mybatis是如何使用它們的? * 一級緩存是應用內的本地緩存,在分布式場景下會不會導致臟讀等不一致問題?該如何解決? 呃,好像問題挺多的!前面三個是緩存的使用問題,第四個是源碼層面的,最后一個是結合一級緩存特點如何使用的問題。帶著這幾個問題,我們進入正題。 ### Cache接口及PerpetualCache Cache接口位于org.apache.ibatis.cache包下,是Mybatis實現緩存的核心接口,Mybatis一級、二級緩存的都依賴于它,通過下面的類圖簡單了解一下繼承關系,以體現Cache的重要地位: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/285015ab9d3e41468c802e5782dcd901~tplv-k3u1fbpfcp-zoom-1.image) 一級緩存僅用到了PerpetualCache,所以這個階段我們先通過源碼認識Cache接口及PerpetualCache實現。先看下Cache接口的注釋: ~~~java /** * SPI for cache providers. * One instance of cache will be created for each namespace. * The cache implementation must have a constructor that receives the cache id as an String parameter. * MyBatis will pass the namespace as id to the constructor. * * <pre> * public MyCache(final String id) { * if (id == null) { * throw new IllegalArgumentException("Cache instances require an ID"); * } * this.id = id; * initialize(); * } * </pre> * * @author Clinton Begin */ 復制代碼 ~~~ 作者Clinton Begin說,Cache接口定義了Mybatis cache體系的SPI,任何一個cache實例都是為namespace創建的,并且通過示例要求每個Cache接口實現類應該通過構造方法接收namespace作為cache的id作為唯一標識。這也就是說,每個Cache實例都是有唯一標識的(id)。再來看下Cache的接口定義: ~~~java public interface Cache { /** * 獲取緩存的唯一標識 * @return The identifier of this cache */ String getId(); /** * 向緩存中添加內容,這里的key使用了CacheKey這個類 * * @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); /** * 從緩存中移除key對應的內容 * * @param key The key * @return Not used */ Object removeObject(Object key); /** * Clears this cache instance,清空緩存 */ void clear(); } 復制代碼 ~~~ Cache接口很少,它提供了對緩存添加、查詢、刪除、清空的基本操作方法,其中添加、查詢、刪除都需要使用類型為CacheKey的key來操作。CacheKey是Mybatis用來生成緩存索引的工具類,通過一定的規則來確保散列的魯棒性。再看PerpetualCache,剛才也提到,它使用HashMap作為緩存的存儲容器,再結合Cache接口定義的方法我們自然就可以知道它是對HashMap元素的操作了,節省篇幅,代碼就不再貼了。 ### 緩存管理 一級緩存管理涉及PerpetualCache創建、查詢緩存、添加緩存、清空緩存幾個過程,除了緩存創建外,其他幾個過程都是由Executor的update/query/commit/rollback等方法執行時觸發。 #### 緩存創建 PerpetualCache的初始化工作在BaseExecutor構造方法中完成,并指定id為`LocalCache`,所以一級緩存也叫本地緩存。初始化代碼如下所示: ~~~java 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; } 復制代碼 ~~~ #### 緩存查詢與添加 緩存的目的是為了提高查詢效率,所以查詢緩存必然是在BaseExecutor#query觸發。如果查詢過程中命中緩存,可直接把緩存內容返回;如果未命中,則需要從數據庫中查詢,然后再把數據庫查詢結果添加到緩存中,以保證下次的查詢效率。查詢過程中的緩存管理流程如下圖所示: ![image.png](data:image/svg+xml;utf8,) 以上流程分別涉及了BaseExecutor#query()、BaseExecutor#queryFromDatabase兩個方法,大家可以對照流程圖及代碼過一下: ~~~java 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(); // 如果本地緩存(一級緩存)的作用范圍是STATEMENT,則清空緩存 if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; } 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; } 復制代碼 ~~~ #### 清空緩存 當數據源發生改變時,為了保證數據一致性,清空緩存中的失效數據,從而保證再次查詢時從數據庫加載最新數據。所以,引起數據源變更的操作即為清空緩存的時機。 從BaseExecutor的接口來看,update、commit、rollback等方法都會導致數據源的變更,通過源碼來看,這些方法內部確實調用了clearLocalCache方法來清空緩存。代碼如下所示: ~~~java 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); } public void commit(boolean required) throws SQLException { if (closed) { throw new ExecutorException("Cannot commit, transaction is already closed"); } clearLocalCache(); flushStatements(); if (required) { transaction.commit(); } } public void rollback(boolean required) throws SQLException { if (!closed) { try { clearLocalCache(); flushStatements(true); } finally { if (required) { transaction.rollback(); } } } } public void clearLocalCache() { if (!closed) { localCache.clear(); localOutputParameterCache.clear(); } } 復制代碼 ~~~ 這里需要注意的是:在update方法中,如果本地緩存(一級緩存)的作用范圍是STATEMENT,則也會調用方法clearLocalCache來清空緩存。 ### 一級緩存總結 #### 生命周期 在之前的文章中我們已經了解到,Executor被SqlSession持有,而一級緩存`LocalCache`只是BaseExecutor的一個字段。所以,當SqlSession的生命周期結束時,一級緩存也會被回收。也就是說,一級緩存僅能作用于同一個SqlSession的聲明周期,不同SqlSession間不可共享。它們之間的關系如下圖所示: ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f616507473e441699b4f0d04e4403c8e~tplv-k3u1fbpfcp-zoom-1.image) #### 分布式系統如何避免數據不一致 既然一級緩存僅可在SqlSession內部共享,那么如果同時存在多個SqlSession或者分布式的環境下,update、commit等引起緩存失效的方法無法對其他SqlSession起作用,必然會帶來臟讀問題。這個問題可以通過更改一級緩存的作用范圍(localCacheScope)為STATEMENT進行避免。 #### 緩存讀寫時機與原理 一級緩存由PerpetualCache實現,其內部通過HashMap完成對緩存的讀寫操作,所以本質上講,一級緩存其實是通過HashMap實現的,Map中的key是由MappedStatement生成CacheKey,來確保查詢的唯一性。 一級緩存是為了提高查詢效率,在執行query操作前會先查詢緩存:若命中緩存,則直接把緩存內容作為結果返回,不再查詢數據庫;若未命中緩存,則執行數據庫查詢,然后把查詢結果加入緩存內,最終返回結果。 ## 二級緩存 一級緩存生命周期僅限于SqlSession,然而實際使用時我們創建多個SqlSession對象,但是多個SqlSession之間無法共享緩存內容。為了解決這個問題,我們可以使用二級緩存。二級緩存由CachingExecutor實現,作用在一級緩存之前。開啟二級緩存后,Executor的查詢流程變為:`二級緩存->一級緩存->數據庫`。 ### 啟用二級緩存 啟用二級緩存,需要依次完成以下幾個配置: * 在Mybatis配置文件中配置cacheEnabled 設置(settings):全局性地開啟或關閉所有映射器配置文件中已配置的任何緩存,默認值為true。 ~~~xml <setting name="cacheEnabled" value="true"/> 復制代碼 ~~~ * 在mapper配置文件中增加節點。 ~~~xml <!--默認情況下,這一行就可以--> <cache/> 復制代碼 ~~~ 這樣會使用默認的緩存策略,cache節點有如下幾個屬性可以設置: * eviction:清除策略。支持LRU(最近最少使用,默認)、FIFO(先進先出)、SOFT、WEAK。 * flushInterval:刷新間隔。可以被設置為任意的正整數,設置的值應該是一個以毫秒為單位的合理時間量。 默認情況是不設置,也就是沒有刷新間隔,緩存僅僅會在調用語句時刷新。 * size:緩存數量,默認值是 1024。可以被設置為任意正整數,要注意欲緩存對象的大小和運行環境中可用的內存資源。 * readOnly(只讀)屬性可以被設置為 true 或 false。只讀的緩存會給所有調用者返回緩存對象的相同實例。 因此這些對象不能被修改。這就提供了可觀的性能提升。而可讀寫的緩存會(通過序列化)返回緩存對象的拷貝。 速度上會慢一些,但是更安全,因此默認值是 false。 默認情況下(如上配置)起到的效果如下: * 映射語句文件中的所有 select 語句的結果將會被緩存。 * 映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。 * 緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。 * 緩存不會定時進行刷新(也就是說,沒有刷新間隔)。 * 緩存會保存列表或對象(無論查詢方法返回哪種)的 1024 個引用。 * 緩存會被視為讀/寫緩存,這意味著獲取到的對象并不是共享的,可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。 ### 二級緩存初始化 二級緩存初始化是在mapper對應的xml文件解析時執行的,可以按照以下鏈路查詢解析流程,其中cacheElement方法讀取了并為配置信息賦值為默認值,然后調用MapperBuilderAssistant#useNewCache初始化二級緩存對象。 * org.apache.ibatis.session.SqlSessionFactoryBuilder#build(java.io.InputStream, java.lang.String, java.util.Properties) * org.apache.ibatis.builder.xml.XMLConfigBuilder#parse * org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration * org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement * org.apache.ibatis.builder.xml.XMLMapperBuilder#parse * org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement * org.apache.ibatis.builder.xml.XMLMapperBuilder#cacheElement * org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache cacheElement方法讀取配置并初始化配置信息,若未配置屬性會采用默認值。 ~~~java private void cacheElement(XNode context) { if (context != null) { // 讀取緩存類型,默認為PERPETUAL String type = context.getStringAttribute("type", "PERPETUAL"); Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); // 讀取數據淘汰策略并獲取其類型,默認為LRU,默認大小1024 String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); // 刷新間隔,默認為空,即不會刷新。 Long flushInterval = context.getLongAttribute("flushInterval"); // 緩存數量:默認為空,會使用LRU的默認大小1024 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); } } 復制代碼 ~~~ useNewCache方法:依次創建緩存對象PerpetualCache,添加數據淘汰策略(裝飾器),設置刷新間隔,設置緩存大小……,這一系列操作采用裝飾器模式進行裝配,最終得到的是一個層層嵌套的Cache對象。 ~~~java 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初始化時,使用currentNamespace(即:mapper文件的namespace)作為Cache的id,最終把該緩存對象存儲在全局緩存Configuration中,以此實現了跨Session共享。 ### 緩存使用 如前文所述,二級緩存在CachingExecutor中生效。查看org.apache.ibatis.executor.CachingExecutor#query方法可知,其執行流程如下: * 根據MappedStatement、parameterObject等信息生成CacheKey; * 獲取MappedStatement中緩存的Cache對象; * 通過TransactionalCacheManager嘗試獲取緩存數據:若緩存命中,則直接返回;若緩存未命中,則執行后續查詢并把結果緩存在TransactionalCacheManager中。 query方法完成了緩存的查詢與添加操作,具體過程可查看如下代碼。 ~~~java @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.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); } 復制代碼 ~~~ ### 緩存失效 為了避免臟讀,當CachingExecutor執行update、commit、rollback等操作時,會執行緩存的刪除或清空重置操作。 update源碼: ~~~java @Override public int update(MappedStatement ms, Object parameterObject) throws SQLException { flushCacheIfRequired(ms); return delegate.update(ms, parameterObject); } private void flushCacheIfRequired(MappedStatement ms) { Cache cache = ms.getCache(); if (cache != null && ms.isFlushCacheRequired()) { tcm.clear(cache); } } 復制代碼 ~~~ commit源碼: ~~~java //org.apache.ibatis.executor.CachingExecutor#commit @Override public void commit(boolean required) throws SQLException { delegate.commit(required); tcm.commit(); } //org.apache.ibatis.executor.CachingExecutor#rollback @Override public void rollback(boolean required) throws SQLException { try { delegate.rollback(required); } finally { if (required) { tcm.rollback(); } } } //org.apache.ibatis.cache.TransactionalCacheManager#commit public void commit() { for (TransactionalCache txCache : transactionalCaches.values()) { txCache.commit(); } } //org.apache.ibatis.cache.decorators.TransactionalCache#commit public void commit() { if (clearOnCommit) { delegate.clear(); } flushPendingEntries(); reset(); } private void reset() { clearOnCommit = false; entriesToAddOnCommit.clear(); entriesMissedInCache.clear(); } 復制代碼 ~~~ ### 二級緩存總結 MyBatis的二級緩存相對于一級緩存來說,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,能夠到namespace級別,通過Cache接口實現類不同的組合,對Cache的可控性也更強。 在分布式環境下,由于默認的MyBatis Cache實現都是基于本地的,分布式環境下必然會出現讀取到臟數據,需要使用集中式緩存將MyBatis的Cache接口實現,有一定的開發成本,直接使用Redis、Memcached等分布式緩存可能成本更低,安全性也更高。 ## 全文總結 本文簡單梳理了mybatis的緩存機制,主要在于了解其運行原理,雖然在實際工作中不會使用,但是某些參數或特性可能會影響我們應用的運行效果,了解其原理重點在于避坑。另外,其緩存原理也可以為我們實際開發工作提供一些好的思路。 作者:碼路印記 鏈接:https://juejin.cn/post/6908703314867650567 來源:掘金 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看