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同樣在使用。其實我們能從得到很多啟發,比如,對於既定的業務場景,要加入現成安全的考量,那在不侵入業務代碼的前提下,我們是否也能增加一層責任裝飾,進行派發來完成呢?

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

建天然氣管直通西歐遭波蘭罰款 俄國企業要上訴

摘錄自2020年10月7日中央社報導

俄羅斯國營的俄羅斯天然氣工業公司架設通往德國的北溪天然氣2號管線,波蘭反托拉斯當局今(7日)決定罰款76億美元,原因是這項計畫不利波蘭消費者且增加歐盟對俄國進口的仰賴。

美聯社報導,俄羅斯天然氣工業公司(Gazprom)表示,將就波蘭競爭及消費者保護署(OCCP)的這項裁決提起上訴。波蘭競爭及消費者保護署也對其他5家參與這項方案的跨國公司,處以總額6100萬美元(約新台幣17億4900萬元)的罰款。

波蘭競爭及消費者保護署表示,參與這項計畫的各家公司,未取得共同闢建這條管線及融資的必要許可,這違反了反托拉斯法。這項裁決迫使6家公司取消為這項計畫籌募資金的合約。

能源轉型
國際新聞
俄國
天然氣管線
歐盟

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

中國擬在巴紐開礦 聯合國嚴重關切

摘錄自2020年10月8日台灣醒報報導

根據衛報報導,聯合國有毒廢物問題特別報告員通卡克和其他9名聯合國高級官員聯名向巴布亞紐幾內亞、澳洲、中國、加拿大政府和中國國營開發商發函,表示對巴紐北部弗里達河開礦計畫的嚴重關切。

這項計畫如果獲得批准,將是巴紐史上最大的、也是世界上最大的礦場之一,佔地1萬6000公頃。該計畫將建在新幾內亞島北部的塞皮克河支流弗里達河上,預計每年開採的金、銀和銅礦價值可達15億美元,長達30年以上。PanAust持有這項專案8成的股份,是中國國企「廣東崛起」的一部分。

土地利用
國際新聞
巴布亞紐幾內亞
聯合國
開礦

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

加拿大減塑 2021年底前禁六大類一次性塑膠製品

摘錄自2020年10月8日中央社報導

加拿大環境部長今(7日)宣布,2021年底之前,加拿大將禁用塑膠袋、塑膠吸管等一次性塑膠用品;他也坦言,加國在回收方面落後歐洲。

今天公布的禁令另將涵蓋攪拌棒、啤酒提環、餐盤以及難以回收的塑膠製餐具,是總理杜魯道(Justin Trudeau)放眼2030年前消滅塑膠垃圾,兌現氣候與環境政見核心的計畫一環。

但環境部長威金森(Jonathan Wilkinson)坦言:「我們這方面不在世界的前段班。」

政府表示,加拿大人每年丟棄300萬公噸塑膠垃圾,其中包括一年丟棄150億個塑膠袋、每天丟掉5700萬支塑膠吸管,在這之中,僅有9%回收。

污染治理
國際新聞
加拿大
一次性塑膠製品
禁塑
塑膠垃圾
一次性塑膠袋

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

中國若要達2060年碳中和目標 得編150兆預算投入再生能源

摘錄自2020年10月8日自由時報報導

國際能源顧問公司伍德麥肯茲(Wood Mackenzie)今(8日)對中國2060年碳中和目標提出分析,表示此一目標中國恐將花超過新台幣150兆元經費,其中又會牽涉到煤礦省份未來所面臨的轉型問題,對此,該顧問公司認為,碳捕存(CCS)技術將成中國達成目標的關鍵,若少了碳捕存,則碳中和目標幾乎不可能。

據路透社報導,伍德麥肯茲公司今天表示,中國若要達到2060年碳中和目標,預計需要的投資金額將超過5兆美元(約為新台幣150兆元),其中包含發展再生能源發電設施等,中國國家主席習近平9月在聯合國成立75週年視訊大會上表示,中國要在2060年達到「碳中和」目標,即為碳的排放量會等於減少量。目前,中國是世界上最大的碳排放國,約佔全球排放量的28%。

氣候變遷
國際新聞
中國
碳中和
碳捕存技術

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

瑞典船運業推出「海洋鳥」 以風為動力可遠洋運輸

摘錄自2020年9月24日科技新報報導

瑞典造船公司 Wallenius Marine 近日宣布推出以風為主要動力、全新的遠洋運輸船「海洋鳥」(Oceanbird),盼開啟環保航運新的一頁。

由於海洋鳥上的機翼帆採取伸縮式結構,當通過橋下或遭遇強風時便能迅速縮小帆體的表面積來進行控制,船體同時也配備輔助的綠能引擎,做為進出港口使用的安全措施。

由於使用風能為主要航行動力,海洋鳥的航行速度較傳統貨輪來的慢,僅能以約 10 節的平均速度航行,橫渡北大西洋約需要 12 天的時間,但在不需要使用高污染燃油下,海洋鳥可以減少近 90% 的排放。

在瑞典運輸署的支持下,Wallenius 已經建造出海洋鳥的小型模型,將於接下來幾個月進行測試,預計完整設計明年底前將準備就緒,2025 年首艘船將正式登場。

能源轉型
國際新聞
瑞典
航運
海運

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

印尼就業環保新法救經濟 勞工抗議延燒

摘錄自2020年10月7日中央社報導

疫情重創經濟,印尼政府加速鬆綁勞動及環保法規,日前突襲通過新法,盼改善投資環境增加就業機會,卻引發大規模抗議,遭質疑修法犧牲勞工權益及環保,爭議恐持續延燒。

印尼政府年初提出創造就業綜合法案後,民間抗議聲浪不斷,勞工團體多次上街表達不滿。

印尼國會5日趕在全國性大罷工前夕審查完法案。印尼請願網站當天發起拒絕創造就業綜合法案的連署迅速累積逾120萬人支持,各大城市爆發罷工示威潮。

在國會審議前,印尼總統佐科威(Joko Widodo)重要幕僚、海洋事務統籌部長盧胡特(Luhut Panjaitan)指出,政府因處理武漢肺炎疫情,這項法案自4月延宕至今。該法案是促進投資的關鍵,幾經協商,全國8大主要工會組織中有6大工會組織同意立法。

不過,由印尼工會聯盟(KSPI)、印尼工人工會聯合會(KSPSI)等團體發動的罷工6日在各大城市許多工業區吸引成千上萬勞工參與。

除了勞動法規,創造就業綜合法案通過也影響環保及稅務等超過70個法律,主要目的是降低投資障礙,方便投資者取得土地及相關證照。這部分引起環保團憂心將弱化環境影響評估的把關機制,不利環境永續發展。

印尼綠色和平資深森林專員亞塞普(Asep Komarudin)7日對中央社指出,現行法規有很多嚴格確保環境保護的條文都因創造就業綜合法案通過而遭廢除,例如未來有些投資案可不經環境影響評估,環評也將限制只有受影響者才參與,不再開放公民參與。

亞塞普說,根據創造就業綜合法案,未來開發案與環保衝突時,被視為與國家策略發展相關的計畫都要給予優先考量,主導開發的國家與企業肯定會持續與原住民族發生衝突,巴布亞(Papua)、加里曼丹(Kalimantan)、蘇門答臘(Sumatra)等地的林地面積可能會再減少3成以上,「我們非常擔心」。

國際新聞
印尼
修法
環保法
勞工剝削
抗議

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

這3台中型SUV一出來,什麼奔馳寶馬統統得靠邊

頂配的3。6T版本4。8 S的百公里加速性能更是令人折服。當然,300mm的涉水深度令人匪夷所思。而中控台上的空白按鍵時刻在提醒你:囊中羞澀以致某項配置未能選裝。此外,保時捷的售價就像你跟媽媽說晚上9點準時回家一樣不靠譜。

2018北京車展於4.25正式拉開帷幕,因工作關係得以參与這場人山人海的車界盛會。在車展上,除了各大傳統車企紛紛推出SUV新品外,諸多造車新勢力所亮相或展示的車型也是SUV為多,由此可見SUV的市場熱度一直未曾消減。

那些展出的SUV近乎都是:外觀愈發年輕、愈加運動“猙獰”,再難覓得一平庸之輩。若是將它們置於三五年前,必定都是明星車型。可在同質化異常嚴重的當下,已鮮有車型能在這股趨於一致的“年輕運動”的SUV洪流之中脫穎而出。

當然,也有例外。這幾款集情懷、血統、顏值、性能於一身的“尤物”備受青睞。

點評:一貫擅長打造英倫優雅紳士風的捷豹,強盛的藝術生命力在這款SUV上得以延續,俊朗精緻的外觀頗有幾分F-TYpE的神韻。年度風雲車、年度最佳設計等大獎便是對這走在時尚尖端寵兒的肯定。在老東家福特的EcoBoost 2.0T替換為自家的Ingenium 2.0T(2018沃德十佳發動機之一)后,性格變得愈加“狂豹”,低沉的聲浪時刻撩撥着駕駛者的慾望;早已滲透骨子里的獨特基因,令其在拐彎抹角的山間道路中犹如一隻飛馳的野豹。

點評:在卡宴身上嘗得SUV甜頭后,保時捷推出與老款Q5共享MLB平台(新款Q5L出自MLB EVO平台)的另一搖錢樹—Macan。萬年不變的青蛙臉令人百看不厭;跑車基因的加持,操控性能自然是無與倫比;頂配的3.6T版本4.8 S的百公里加速性能更是令人折服。當然,300mm的涉水深度令人匪夷所思;而中控台上的空白按鍵時刻在提醒你:囊中羞澀以致某項配置未能選裝。此外,保時捷的售價就像你跟媽媽說晚上9點準時回家一樣不靠譜。

點評:作為阿爾法羅密歐品牌百年歷史中第一款SUV產品,意大利人對藝術獨到的見解與對性能的痴迷在其身上得以極佳體現。精準的轉向、汽車愛好者極易上手的完美操控、恰到好處的零百以及超高的彎道極限令人愛不釋手。在北京車展上完成亞洲首秀的Stelvio QV版本更是紐北最快量產SUV圈速的記錄保持者。只是FCA的尿性,大家心照不宣,難怪有媒體朋友曾言:等我有錢了,就買它個兩台,一台修理一台開。

俗語言:逆水行舟,不進則退。在主流的市場里,品牌的差異性將變得越來越小,不論是國際品牌還是本土品牌。因此,“變通”是明智的生存之道。多數人認為保時捷、阿爾法羅密歐等豪華品牌推出SUV獲取更大市場份額意味着喪失了品牌的獨有特質。

但恰恰是這種“變通”,洞察出市場真正的需求點,才讓人看到更多好看的皮囊,感受到更多有趣的靈魂。畢竟只有存活下來,才能讓自身的基因得到延續。如果保時捷只生產跑車,捷豹只生產轎車,我們將錯過多少“尤物”呢?本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

想買車?看完最低9.9萬起的這6款全新SUV就可以下單了

我們再一起來回顧一下吧現代ENCINO是一款以性能著稱的小型SUV,搭載1。6T渦輪增壓發動機和雙離合器變速箱作為動力總成,177匹馬力的賬面參數也是十分誘人,配合上個性張揚的外觀設計和尚且不貴的售價,現代ENCINO要成為未來年輕人的首選車型的決心顯而易見。

《後來的我們》上映不久票房已超過10億,

繼《前任3》后又再一次給了年輕人5條建議:

1.如果結婚就要找愛你的

2.千萬別輸在“等”這個字身上

3.永遠留住30%的神秘

4.如何才能配得上你的伴侶?

關注玩車是

走向人生巔峰迎娶白富美嫁給高富帥的前提噢!

5.牢記第4條,前3條沒什麼用…

好吧,皮了一下,我們說回正事吧

看看了5月或將上市的車,

心想:除了個別車型,毫無驚喜!

這跟4月份的新車比起來,差距可不是一點點啊

起碼,全新的SUV,就有6款重磅新車了

不是改款,不是換代,

這幾款都是扎紮實實的新車、新SUV噢!

我們再一起來回顧一下吧

現代ENCINO是一款以性能著稱的小型SUV,搭載1.6T渦輪增壓發動機和雙離合器變速箱作為動力總成,177匹馬力的賬面參數也是十分誘人,配合上個性張揚的外觀設計和尚且不貴的售價,現代ENCINO要成為未來年輕人的首選車型的決心顯而易見。

威馬EX5是一款非常具有市場潛力的電動SUV,雖然是新品牌下的新車型,但是威馬公司背後的技術背書和他們自建工廠的生產方式讓他們的品牌形象顯得十分靠譜。而威馬EX5的實車已經正式上市,在2018年下半年就可以正式交付,EX5的設計非常符合大眾審美,屬於十分耐看的一款車型,而且續航里程可以達到400多公里的動力特性,也讓它十分有成為一二線限行城市的熱銷車型的潛力。

自主品牌非承載式SUV對於普通消費者來說更像是一個要麼廉價,要麼粗糙的存在。但是榮威RX8的上市便打破了這一種的傳統認知,榮威RX8的非承載式SUV和全時四驅的特性讓它有着比較出色的越野能力,而且定位豪華的RX8在內飾氛圍的設計上融入了非常多的木紋飾板和檔次感出眾的軟質材料,讓整車的檔次感顯現地非常到位。

對於越野有需求的消費者,或許就在途達上市以後有了新的選擇,非承載式車身,分時四驅,二十五萬以內的頂配售價,這些特點都在表明途達是一款可以入門越野圈的新車;而且久經市場考驗的動力總成更讓它的可靠性有所保障,雖然內飾設計相對廉價,但是對於追求更多在嚴苛環境下有靠譜表現的消費者來說,途達的產品力正是他們想要的。

Jeep在中國市場上的發力趨勢愈發明顯,相繼自由光,指南者這類車型在國內市場獲得一定成功以後,現如今又推出了一款中國特供的中型SUV,大指揮官。雖然這款車型是一款低於大切諾基的中型SUV,但是它的外觀與內飾設計,配置供給與裝配工藝等等方面都基本上稱得上是現如今合資國產的Jeep車型中的旗艦。售價區間與漢蘭達,銳界等車型有着高度重合,未來中型7座SUV中的強有力競爭者勢必將會多出大指揮官的身影。

GLC和Q5在BBA的豪華中型SUV市場稱雄已久,操控大師寶馬也就看不過眼了,華晨寶馬X3正式在中國上市,都說它沒有加長會影響車內空間,但其實這款車型換代后本就有了一定車身尺寸上的增加,所以空間並不是什麼硬傷,相反,更原汁原味的引入國產化后的Q3勢必會成為BBA中型SUV中最具運動基因的一款車型,而廣大寶馬迷們在X3上市以後也必定有了更為接地氣和更實惠的車型備選方案。

怎麼樣,這幾款新SUV很吸引吧~

感興趣的話不妨去4S店看看實車噢本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

15萬買啥車?有轎車有SUV,這2款合資車可以放心買

勁客的優勢在於成熟可靠的動力總成,採用1。5L自然吸氣發動機+CVT無級變速器的搭配能夠帶來非常平順線性的輸出,油門調校靈敏,能夠很好地應付日常市區內行駛,並且也非常省油,可靠性較高。不過其檔次感與配置水平不如哈弗H2。

15萬左右比較推薦的是本田XR-V與豐田卡羅拉,這兩款車目前在市場上都有着不錯的銷量、保值率以及口碑,XR-V在小型SUV市場混得風生水起,靠的就是其成熟的動力系統和越級的空間表現,家用是個非常棒的選擇。而卡羅拉乃全球銷量神車,中庸就是其最大的武器,除了汽油版本外還有雙擎版本可選,擁有非常優秀的燃油經濟性,能夠滿足許許多多的消費者,除了隔音濾振差點,其它方面的表現都非常不錯,乃家用車的不二之選。

一輛是接近中型車的緊湊型車,一輛是標準的中型車,凌渡280DSG豪華版擁有更加豐富的配置,不過因為其比較扁平的設計所以頭部空間表現一般,而君威中型車的身份自然擁有更大的空間以及更高的檔次感,同時全系標配9AT也讓其競爭力進一步上升,採用的1.5T發動機也有着不錯的動力輸出,因此更推薦君威1.5T中配。

510採用的是6擋手動變速器和5擋AMT變速器,假設會開手動擋的話更加推薦手動車型,6個擋位在高速行駛時把轉速壓得更低,能帶來更好的燃油經濟性,同時擁有更高的傳動效率及可靠性,510的手動擋換擋手感不錯,有吸入感並且行程不長,離合器的力度也不沉,沒那麼容易疲勞。而AMT變速器雖然說省去了踩離合器的麻煩,但是其換擋邏輯不清晰,而且頓挫比較嚴重,尤其是在起步階段或者是在堵車狀況下,因此更加推薦510的手動版車型。

兩車都是定位小型SUV,哈弗H2擁有更加親民的售價,更加大氣上檔次的外觀內飾設計,同時配置更加豐富,採用的1.5T發動機+7擋雙離合的搭配,擁有不錯的爆發力,但是雙離合變速器的邏輯有待提高,並且油耗也會偏高。

勁客的優勢在於成熟可靠的動力總成,採用1.5L自然吸氣發動機+CVT無級變速器的搭配能夠帶來非常平順線性的輸出,油門調校靈敏,能夠很好地應付日常市區內行駛,並且也非常省油,可靠性較高。不過其檔次感與配置水平不如哈弗H2。

綜上,假如你預算充足的話,更加建議購買勁客的中配以上車型,而預算不是很足的話,那麼家用選擇H2是一個非常具有性價比的選擇。

2018款飛度新增了運動套件車型,不過在配置方面還是一如既往的寒酸,比較推薦指導價為8.88萬的1.5L CVT潮跑版,在配置上面擁有主/副駕駛座安全氣囊、運動外觀套件、行車電腦显示屏、前霧燈等,類似比較常用的电子車身穩定系統、駐車雷達要到頂配才配備,但是飛度的動力和空間還是非常不錯的,1.5L自吸發動機就能爆發出131匹馬力,日常市區駕駛毫無壓力,加上本田“MM”理念,讓它的空間能夠滿足大多數人的使用需求,市面上大量的改裝件也能讓每位飛度車主把愛車改成獨一無二的樣子。

以上就是本期網友問答欄目的全部內容,假如你也想上牆的話,點擊下方留言留下你的問題並且點個贊,就有機會在下期欄目看見你的身影!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化