1. 程式人生 > >Mybatis原始碼手記-從快取體系看責任鏈派發模式與迴圈依賴企業級實踐

Mybatis原始碼手記-從快取體系看責任鏈派發模式與迴圈依賴企業級實踐

一、快取總覽  

  Mybatis在設計上處處都有用到的快取,而且Mybatis的快取體系設計上遵循單一職責、開閉原則、高度解耦。及其精巧,充分的將快取分層,其獨到之處可以套用到很多類似的業務上。這裡將主要的快取體系做一下簡單的分析筆記。以及藉助Mybatis快取體系的學習,進一步窺探責任鏈派發模式企業級實踐,以及物件迴圈依賴場景下如何避免裝載死迴圈的企業級解決方案。

  先來一張之前的執行體系圖:

 

 對照這張執行圖,不難看出,其實對於一次Mybatis查詢呼叫,即SqlSession -> SimpleExecutor/ReuseExecutor/BatchExecutor -> JDBC,其實快取就是在SqlSession到Executor*之間做一層截獲請求的邏輯。從巨集觀上很好理解。CachingExecutor作為BaseExecutor的一個前置增強裝飾器,其增強的功能就是,判斷是否命中了快取,如果命中快取,則不進行BaseExecutor的執行派發。

 1 public class CachingExecutor implements Executor {
 2   // BaseExecutor
 3   private final Executor delegate;
 4   public CachingExecutor(Executor delegate) {
 5     this.delegate = delegate;
 6     delegate.setExecutorWrapper(this);
 7   }
 8   @Override
 9   public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
10       throws SQLException {
11     Cache cache = ms.getCache();
12     if (cache != null) {
13       flushCacheIfRequired(ms);
14       if (ms.isUseCache() && resultHandler == null) {
15         ensureNoOutParams(ms, boundSql);
16         List<E> list = (List<E>) tcm.getObject(cache, key);
17         if (list == null) {
18           list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
19           tcm.putObject(cache, key, list); // issue #578 and #116
20         }
21         return list;
22       }
23     }
24     // 如果未命中快取則向BaseExecutor派發
25     return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
26   }
27 }

所以由此來看,mybatis的快取是先嚐試命中CachingExecutor的二級快取,如果未命中,則派發個BaseExecutor,下來才會去嘗試命中一級快取。由於一級快取比較簡單,我們先來看一級快取。

二、一級快取概覽

之前執行器的那一節講過,Mybatis的執行器和SqlSession都是一對一的關係

1 public class DefaultSqlSession implements SqlSession {
2     // ...
3   private final Executor executor;
4     // ...
5 }

而每個執行器裡邊用一個成員變數來做快取容器

1 public abstract class BaseExecutor implements Executor {
2     // ...
3   protected PerpetualCache localCache;
4     // ...
5 }

那麼也就是說,一旦SqlSession關閉,即物件銷燬,必然BaseExecutor物件銷燬,所以一級快取容器跟著銷燬。由此可以推到出:一級快取是SqlSession級別的快取。也就是要命中一級快取,必須是同一個SqlSession,而且未關閉。

再來看一下一級快取是如何設定快取的:

 1 public abstract class BaseExecutor implements Executor {
 2   protected PerpetualCache localCache;
 3   @Override
 4   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
 5     BoundSql boundSql = ms.getBoundSql(parameter);
 6     CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
 7     return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 8   }
 9   @Override
10   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
11     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
12     if (closed) {
13       throw new ExecutorException("Executor was closed.");
14     }
15     if (queryStack == 0 && ms.isFlushCacheRequired()) {
16       clearLocalCache();
17     }
18     List<E> list;
19     try {
20       queryStack++;
21       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
22       if (list != null) {
23         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
24       } else {
25         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
26       }
27     } finally {
28       queryStack--;
29     }
30     if (queryStack == 0) {
31       for (DeferredLoad deferredLoad : deferredLoads) {
32         deferredLoad.load();
33       }
34       // issue #601
35       deferredLoads.clear();
36       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
37         // issue #482
38         clearLocalCache();
39       }
40     }
41     return list;
42   }
43 }

通過這一段原始碼,可以看到,是在第6行去構建快取key,在第21行嘗試獲取快取。構建快取key,取決於四個維度:MappedStatement(同一個statementId)、parameter(同樣的查詢引數)、RowBounds(同樣的行數)、BoundsSql(同樣的SQL),加上上邊SqlSession的條件,一級快取的命中條件為:相同的SqlSession、statementId、parameter、行數、Sql,才能命中一級快取。

這裡在說一句題外話,就是當mybatis與Spring整合時,SqlSession的管理就交給Spring框架了,每次Mybatis的查詢都會由Spring框架新建一個Sqlsession供mybatis用,看起來一級快取永遠失效。解決辦法就是給查詢加上事務,當加上事務的時候,Spring框架會保證在一個事務裡邊只提供給mybatis同一個SqlSession物件。

再看下一級快取何時會被重新整理掉,來上原始碼:

 1 public abstract class BaseExecutor implements Executor {
 2   protected PerpetualCache localCache;
 3   @Override
 4   public void close(boolean forceRollback) {
 5     try {
 6       try {
 7         rollback(forceRollback);
 8       } finally {
 9         if (transaction != null) {
10           transaction.close();
11         }
12       }
13     } catch (SQLException e) {
14       log.warn("Unexpected exception on closing transaction.  Cause: " + e);
15     } finally {
16       transaction = null;
17       deferredLoads = null;
18       localCache = null;
19       localOutputParameterCache = null;
20       closed = true;
21     }
22   }
23   @Override
24   public int update(MappedStatement ms, Object parameter) throws SQLException {
25     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
26     if (closed) {
27       throw new ExecutorException("Executor was closed.");
28     }
29     clearLocalCache();
30     return doUpdate(ms, parameter);
31   }
32   @Override
33   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
34     if (queryStack == 0 && ms.isFlushCacheRequired()) {
35       clearLocalCache();
36     }
37     if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
38       clearLocalCache();
39     }
40   }
41   @Override
42   public void commit(boolean required) throws SQLException {
43     clearLocalCache();
44   }
45 
46   @Override
47   public void rollback(boolean required) throws SQLException {
48     if (!closed) {
49       try {
50         clearLocalCache();
51         flushStatements(true);
52       } finally {
53         if (required) {
54           transaction.rollback();
55         }
56       }
57     }
58   }
59 
60   @Override
61   public void clearLocalCache() {
62     if (!closed) {
63       localCache.clear();
64       localOutputParameterCache.clear();
65     }
66   }

對於這段原始碼,清除快取的場景,著重關注一下clearLocalCache的呼叫的地方:

即觸發更新操作(第29行)、配置flushCache=true(第35行)、配置快取作用於為STATEMENT(第38行)、commit時候(第42行)、rollback時候(第50行)、執行器關閉時候(第7行)都會清除一級快取。

 

三、一級快取對於巢狀子查詢迴圈依賴場景的解決方案

迴圈依賴的情況處處可見,比如:一個班主任,下邊有多個學生,每個學生又有一個對應的班主任。

對於班主任和學生這種場景,在mybatis層面屬於典型的巢狀子查詢。mybatis在處理巢狀查詢的時候,都會查詢,然後在設定屬性的時候,如果發現有子查詢,則發起子查詢。那麼,如果不加特殊干預,這種場景將會陷入設定屬性觸發查詢的死迴圈中。

 1 <select id="selectHeadmasterById" resultMap="teacherMap">
 2     select * from teacher where id = #{id}
 3 </select>
 4 <resultMap id="teacherMap" type="Teacher" autoMapping="true">
 5     <result column="name" property="name"/>
 6     <collection property="students" column="id" select="selectStudentsByTeacherId" fetchType="eager"/>
 7 </resultMap>
 8 <select id="selectStudentsByTeacherId" resultMap="studentMap">
 9     select * from student where teacher_id = #{teacherId}
10 </select>
11 <resultMap id="studentMap" type="comment">
12     <association property="teacher" column="teacher_id" select="selectHeadmasterById" fetchType="eager"/>
13 </resultMap>

mybatis在處理這種情況的時候,巧妙的用了一個臨時一級快取佔位符與延遲裝載(不同於懶載入),解決了查詢死迴圈的問題。這裡我們直接上原始碼:

每次查詢,如果沒有命中有效快取(即非佔位符快取)mybatis都會事先給一級快取寫入一個佔位符,待資料庫查詢完畢後,再將真正的資料覆蓋掉佔位符快取。

 1 public abstract class BaseExecutor implements Executor {
 2   protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
 3   protected PerpetualCache localCache;
 4   protected int queryStack;
 5   @Override
 6   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 7     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
 8     if (closed) {
 9       throw new ExecutorException("Executor was closed.");
10     }
11     if (queryStack == 0 && ms.isFlushCacheRequired()) {
12       clearLocalCache();
13     }
14     List<E> list;
15     try {
16       queryStack++;
17       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
18       if (list != null) {
19         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
20       } else {
21         // 如果未獲取到快取則查庫
22         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
23       }
24     } finally {
25       queryStack--;
26     }
27     if (queryStack == 0) {
28       for (DeferredLoad deferredLoad : deferredLoads) {
29         deferredLoad.load();
30       }
31       deferredLoads.clear();
32       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
33         clearLocalCache();
34       }
35     }
36     return list;
37   }
38 }

如上Query方法的第22行進去:

 1 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 2     List<E> list;
 3     // 查庫之前先設定佔位符快取
 4     localCache.putObject(key, EXECUTION_PLACEHOLDER);
 5     try {
 6       list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
 7     } finally {
 8       localCache.removeObject(key);
 9     }
10     localCache.putObject(key, list);
11     if (ms.getStatementType() == StatementType.CALLABLE) {
12       localOutputParameterCache.putObject(key, parameter);
13     }
14     return list;
15 }

BaseExecutor.queryFromDataBase方法的第6行,會觸發資料庫查詢,緊接著會進入結果值設定的邏輯。那麼首先會探測有無巢狀的子查詢,如果有,則前一步主查詢暫時等待,立即發起子查詢。

 1   private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
 2       throws SQLException {
 3     final String nestedQueryId = propertyMapping.getNestedQueryId();
 4     final String property = propertyMapping.getProperty();
 5     final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
 6     final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
 7     final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
 8     Object value = null;
 9     if (nestedQueryParameterObject != null) {
10       final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
11       final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
12       final Class<?> targetType = propertyMapping.getJavaType();
13       // 判斷當前的子查詢是否和之前的某一步主查詢相同
14       if (executor.isCached(nestedQuery, key)) {
15         executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
16         value = DEFERRED;
17       } else {
18         final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
19         if (propertyMapping.isLazy()) {
20           lazyLoader.addLoader(property, metaResultObject, resultLoader);
21           value = DEFERRED;
22         } else {
23         // 立即發起子查詢
24           value = resultLoader.loadResult();
25         }
26       }
27     }
28     return value;
29  }

這塊重點關注第13行和第23行。其中第23行又會遞迴到上邊BaseExecutor.query程式碼片段的第22行。如果getNestedQueryMappingValue程式碼段走的是滴15行邏輯,那麼,會對應BaseExecutor.query程式碼片段的第28行。這塊遞迴比較繞。下來做下通俗的解釋:

首先查詢班主任的主查詢給一級快取寫入一個佔位符快取,然後去查庫,然後設定屬性,如果沒有巢狀子查詢,那麼到這裡就把設定好屬性的值寫入覆蓋剛才一級佔位符快取。流暢完畢。

但是恰好有巢狀子查詢,所以查詢班主任的主查詢就停在設定屬性這一步,然後又發起一次查詢,查詢學生,然後又進入查詢學生設定屬性的方法。

設定學生屬性方法又發現又有巢狀子查詢,所以有發起一次學生查詢班主任的查詢操作,又進入到設定屬性這塊,但是發現一級快取裡邊有前邊住查詢的站位快取。所以沒有在查庫,而是將本次子查詢放入延遲裝載的容器裡邊。本次子查詢結束。緊接著前一步子查詢(老師查學生)結束。

緊接著查詢老師的住查詢設定屬性完畢,並將自己的結果覆蓋之前寫入的站位快取。同時啟動了延時裝載的邏輯,延時裝載就是從一級快取取出剛才查詢老師的一級快取資料(老師),給第二步子查詢(學生)做一下MetaObject屬性設定。

說的通俗一點:主查詢(查班主任)執行時先寫入站位快取,緊接著掛起,發起第一個巢狀子查詢(用老師查學生),緊接著該子查詢再掛起,發起學生查老師,但是發現第一步主查詢有一級快取(站位快取),那麼本次子查詢自動加入延遲裝載佇列,然後終結改子查詢,等待主查詢真正查完,然後延遲裝載器再從快取取出資料給第一個子查詢(老師查學生)進行屬性設定。

說了這麼多,肯定暈車了,這裡給出一個時序圖:

總結一下:
1、佔位符快取作用在於標識與當前查詢相同的前邊的巢狀查詢。比如:查詢學生所屬班主任,發現前邊的主查詢就是查詢班主任,所以就不在執行班主任查詢。等待真正的班主任查詢完畢,我們只需去快取裡邊取即可。所以我們不執行查詢,只是將本次屬性設定放入延遲裝載佇列即可。
2、queryStack用來記錄當前查詢處於巢狀的第幾層。當queryStack == 0時,證明整個查詢已經迴歸到最初的主查詢上,此時,所有過程中需要延遲裝載的物件,都能啟動真實裝載了。
3、一級快取在解決巢狀子查詢屬性設定迴圈依賴上啟至關作用。所以以及快取是不能完全關閉的。但是我們可以設定:LocalCacheScope.STATEMENT,來讓一級快取及時清空。參見原始碼

 

 1   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 2     // ...
 3     try {
 4       queryStack++;
 5       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
 6       if (list != null) {
 7         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
 8       } else {
 9         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
10       }
11     } finally {
12       queryStack--;
13     }
14     if (queryStack == 0) {
15       for (DeferredLoad deferredLoad : deferredLoads) {
16         deferredLoad.load();
17       }
18       deferredLoads.clear();
19       // 設定LocalCacheScope.STATEMENT來及時清空快取
20       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
21         clearLocalCache();
22       }
23     }
24     return list;
25   }

 四、二級快取

來先上一個二級快取的執行流程:

 

二級快取是BaseExecutor的前置增強包裝類CachingExecutor裡邊實現的,即如果從CachingExecutor裡邊命中快取,則不進行BaseExecutor的派發(如下第14行)。

 1   public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
 2       throws SQLException {
 3     Cache cache = ms.getCache();
 4     if (cache != null) {
 5       flushCacheIfRequired(ms);
 6       if (ms.isUseCache() && resultHandler == null) {
 7         ensureNoOutParams(ms, parameterObject, boundSql);
 8         @SuppressWarnings("unchecked")
 9         List<E> list = (List<E>) tcm.getObject(cache, key);
10         if (list == null) {
11           list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
12           tcm.putObject(cache, key, list); // issue #578 and #116
13         }
14         return list;
15       }
16     }
17     return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
18   }

  11行與17行的區別在於,是否啟動二級快取,如果啟動了,則將派發給BaseExecutor的查詢結果寫入暫存區(第12行,TransactionCacheManager),等事務提交在真正刷入二級快取。下來我們重點來關注一下快取的讀寫(第9行、第12行),這裡邊真正的執行物件是一系列Cache介面的實現,按職責有:執行緒安全、日誌記錄、過期清理、溢位淘汰、序列化、執行儲存等等環節。而二級快取的設計精巧之處就在於此處,完美的按職責進行責任派發,完全解耦。

  接下來我們來看下,預設情況下,快取責任鏈的初始化過程:

 1   public Cache useNewCache(Class<? extends Cache> typeClass,
 2       Class<? extends Cache> evictionClass,
 3       Long flushInterval,
 4       Integer size,
 5       boolean readWrite,
 6       boolean blocking,
 7       Properties props) {
 8     Cache cache = new CacheBuilder(currentNamespace)
 9         // 這裡設定預設的儲存為記憶體
10         .implementation(valueOrDefault(typeClass, PerpetualCache.class))
11         // 這裡設定預設的溢位淘汰為LRU
12         .addDecorator(valueOrDefault(evictionClass, LruCache.class))
13         .clearInterval(flushInterval)
14         .size(size)
15         .readWrite(readWrite)
16         .blocking(blocking)
17         .properties(props)
18         .build();
19     configuration.addCache(cache);
20     currentCache = cache;
21     return cache;
22   }

  然後是初始化過程:

 1   public Cache build() {
 2     setDefaultImplementations();
 3     Cache cache = newBaseCacheInstance(implementation, id);
 4     setCacheProperties(cache);
 5     // issue #352, do not apply decorators to custom caches
 6     if (PerpetualCache.class.equals(cache.getClass())) {
 7       for (Class<? extends Cache> decorator : decorators) {
 8         cache = newCacheDecoratorInstance(decorator, cache);
 9         setCacheProperties(cache);
10       }
11       cache = setStandardDecorators(cache);
12     } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
13       cache = new LoggingCache(cache);
14     }
15     return cache;
16   }
17   private Cache setStandardDecorators(Cache cache) {
18     try {
19       MetaObject metaCache = SystemMetaObject.forObject(cache);
20       if (size != null && metaCache.hasSetter("size")) {
21         metaCache.setValue("size", size);
22       }
23       if (clearInterval != null) {
24         cache = new ScheduledCache(cache);
25         ((ScheduledCache) cache).setClearInterval(clearInterval);
26       }
27       if (readWrite) {
28         cache = new SerializedCache(cache);
29       }
30       cache = new LoggingCache(cache);
31       cache = new SynchronizedCache(cache);
32       if (blocking) {
33         cache = new BlockingCache(cache);
34       }
35       return cache;
36     } catch (Exception e) {
37       throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
38     }
39   }

這裡從第3、8、24、28、30、31、33行分別進行了責任裝飾初始化。這種依據職責分別拆分然後巢狀的解耦方式,其實是一種很成熟的企業級責任派發設計模式。而且形如第8行的迴圈裝飾巢狀,在很多開源框架中都能見到,比如Dubbo的AOP機制就是這樣初始化的。

下邊直接列一下Mybatis的二級快取在設計上所覆蓋的功能,以及各功能責任鏈派發的結構圖:

 

 從上邊的程式碼可以看出,如果設定了blocking的話,那麼最外層將會包裹BlockingCache、下來是SynchronizedCache,這兩個均是進行執行緒安全,防止快取穿透的處理。

 1 public class BlockingCache implements Cache {
 2   private final Cache delegate;
 3   private final ConcurrentHashMap<Object, ReentrantLock> locks;
 4   public BlockingCache(Cache delegate) {
 5     this.delegate = delegate;
 6     this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
 7   }
 8   @Override
 9   public void putObject(Object key, Object value) {
10     try {
11       delegate.putObject(key, value);
12     } finally {
13       releaseLock(key);
14     }
15   }
16   @Override
17   public Object getObject(Object key) {
18     acquireLock(key);
19     Object value = delegate.getObject(key);
20     if (value != null) {
21       releaseLock(key);
22     }        
23     return value;
24   }
25 }
 1 public class SynchronizedCache implements Cache {
 2   private Cache delegate;
 3   @Override
 4   public synchronized void putObject(Object key, Object object) {
 5     delegate.putObject(key, object);
 6   }
 7   @Override
 8   public synchronized Object getObject(Object key) {
 9     return delegate.getObject(key);
10   }
11 }

再看一下負責溢位淘汰的LruCache:

 1 public class LruCache implements Cache {
 2   private final Cache delegate;
 3   private Map<Object, Object> keyMap;
 4   // 記錄當溢位時,需要淘汰的Key
 5   private Object eldestKey;
 6   public void setSize(final int size) {
 7       // LinkedHashMap.accessOrder設定為true,即,每個被訪問的元素會一次放到佇列末尾。當溢位的時候就能從首部來移除了
 8     keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
 9       @Override
10       protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
11         boolean tooBig = size() > size;
12         if (tooBig) {
13           eldestKey = eldest.getKey();
14         }
15         return tooBig;
16       }
17     };
18   }
19   @Override
20   public void putObject(Object key, Object value) {
21     delegate.putObject(key, value);
22     cycleKeyList(key);
23   }
24   private void cycleKeyList(Object key) {
25     keyMap.put(key, key);
26     if (eldestKey != null) {
27       delegate.removeObject(eldestKey);
28       eldestKey = null;
29     }
30   }
31 }

二級快取就講到這裡,總結一下二級快取件:

1、預設開啟,cachEnable開關。作用於提交後。

2、相同的StatementId。

3、相同的SQL、引數、行數。

4、跨Mapper呼叫。

 

五、總結

  雖然在目前各種分散式應用的場景下,一級快取和二級快取都有很大概率的髒讀現象,而被禁止,但是Mybatis對這種區域性場景的設計是及其精巧的。比如,解決物件迴圈巢狀查詢的場景設計,其實這種成熟的解決方案也被Spring(也存在物件迴圈注入的情景)所應用。以及責任裝飾的設計,Dubbo同樣在使用。其實我們能從得到很多啟發,比如,對於既定的業務場景,要加入現成安全的考量,那在不侵入業務程式碼的前提下,我們是否也能增加一層責任裝飾,進行派發來完成呢?

&n