1. 程式人生 > >MyBatis二級快取解析

MyBatis二級快取解析

一、建立Cache的完整過程

我們從SqlSessionFactoryBuilder解析mybatis-config.xml配置檔案開始:

Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

然後是:

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());

看parser.parse()方法

parseConfiguration(parser.evalNode("/configuration"));

看處理Mapper.xml檔案的位置:

mapperElement(root.evalNode("mappers"));

看處理Mapper.xml的XMLMapperBuilder:

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, 
                    resource, configuration.getSqlFragments());
mapperParser.parse();

繼續看parse方法:

configurationElement(parser.evalNode("/mapper"));

到這裡:

String namespace = context.getStringAttribute("namespace");
if (namespace.equals("")) {
     throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));

從這裡看到namespace就是xml中<mapper>元素的屬性。然後下面是先後處理的cache-refcache,後面的cache會覆蓋前面的cache-ref,但是如果一開始cache-ref沒有找到引用的cache,他就不會被覆蓋,會一直到最後處理完成為止,最後如果存在cache,反而會被cache-ref覆蓋。這裡是不是看著有點暈、有點亂?所以千萬別同時配置這兩個,實際上也很少有人會這麼做。

看看MyBatis如何處理<cache/>

private void cacheElement(XNode context) throws Exception {
    if (context != null) {
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        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);
    }
}

從原始碼可以看到MyBatis讀取了那些屬性,而且很容易可以到這些屬性的預設值。

建立Java的cache物件方法為builderAssistant.useNewCache,我們看看這段程式碼:

public Cache useNewCache(Class<? extends Cache> typeClass,
                         Class<? extends Cache> evictionClass,
                         Long flushInterval,
                         Integer size,
                         boolean readWrite,
                         boolean blocking,
                         Properties props) {
    typeClass = valueOrDefault(typeClass, PerpetualCache.class);
    evictionClass = valueOrDefault(evictionClass, LruCache.class);
    Cache cache = new CacheBuilder(currentNamespace)
            .implementation(typeClass)
            .addDecorator(evictionClass)
            .clearInterval(flushInterval)
            .size(size)
            .readWrite(readWrite)
            .blocking(blocking)
            .properties(props)
            .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
}

從呼叫該方法的地方,我們可以看到並沒有使用返回值cache,在後面的過程中建立MappedStatement的時候使用了currentCache

二、使用Cache過程

在系統中,使用Cache的地方在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();

獲取cache後,先判斷是否有二級快取。 
只有通過<cache/>,<cache-ref/>@CacheNamespace,@CacheNamespaceRef標記使用快取的Mapper.xml或Mapper介面(同一個namespace,不能同時使用)才會有二級快取。

if (cache != null) {

如果cache存在,那麼會根據sql配置(<insert>,<select>,<update>,<delete>flushCache屬性來確定是否清空快取。

flushCacheIfRequired(ms);

然後根據xml配置的屬性useCache來判斷是否使用快取(resultHandler一般使用的預設值,很少會null)。

if (ms.isUseCache() && resultHandler == null) {

確保方法沒有Out型別的引數,mybatis不支援儲存過程的快取,所以如果是儲存過程,這裡就會報錯。

ensureNoOutParams(ms, parameterObject, boundSql);

沒有問題後,就會從cache中根據key來取值:

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

在上面的程式碼中tcm.putObject(cache, key, list);這句程式碼是快取了結果。但是實際上直到sqlsession關閉,MyBatis才以序列化的形式儲存到了一個Map(預設的快取配置)中。

三、Cache使用時的注意事項

1. 只能在【只有單表操作】的表上使用快取

不只是要保證這個表在整個系統中只有單表操作,而且和該表有關的全部操作必須全部在一個namespace下。

2. 在可以保證查詢遠遠大於insert,update,delete操作的情況下使用快取

這一點不需要多說,所有人都應該清楚。記住,這一點需要保證在1的前提下才可以! 
 

四、避免使用二級快取

可能會有很多人不理解這裡,二級快取帶來的好處遠遠比不上他所隱藏的危害。

  1. 快取是以namespace為單位的,不同namespace下的操作互不影響。

  2. insert,update,delete操作會清空所在namespace下的全部快取。

  3. 通常使用MyBatis Generator生成的程式碼中,都是各個表獨立的,每個表都有自己的namespace

為什麼避免使用二級快取

在符合【Cache使用時的注意事項】的要求時,並沒有什麼危害。

其他情況就會有很多危害了。

針對一個表的某些操作不在他獨立的namespace下進行。

例如在UserMapper.xml中有大多數針對user表的操作。但是在一個XXXMapper.xml中,還有針對user單表的操作。

這會導致user在兩個名稱空間下的資料不一致。如果在UserMapper.xml中做了重新整理快取的操作,在XXXMapper.xml中快取仍然有效,如果有針對user的單表查詢,使用快取的結果可能會不正確。

更危險的情況是在XXXMapper.xml做了insert,update,delete操作時,會導致UserMapper.xml中的各種操作充滿未知和風險。

有關這樣單表的操作可能不常見。但是你也許想到了一種常見的情況。

多表操作一定不能使用快取

為什麼不能?

首先不管多表操作寫到那個namespace下,都會存在某個表不在這個namespace下的情況。

例如兩個表:roleuser_role,如果我想查詢出某個使用者的全部角色role,就一定會涉及到多表的操作。

<select id="selectUserRoles" resultType="UserRoleVO">
    select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>

像上面這個查詢,你會寫到那個xml中呢??

不管是寫到RoleMapper.xml還是UserRoleMapper.xml,或者是一個獨立的XxxMapper.xml中。如果使用了二級快取,都會導致上面這個查詢結果可能不正確。

如果你正好修改了這個使用者的角色,上面這個查詢使用快取的時候結果就是錯的。

這點應該很容易理解。

在我看來,就以MyBatis目前的快取方式來看是無解的。多表操作根本不能快取。

如果你讓他們都使用同一個namespace(通過<cache-ref>)來避免髒資料,那就失去了快取的意義。

看到這裡,實際上就是說,二級快取不能用。整篇文章介紹這麼多也沒什麼用了。

 

五、挽救二級快取?

想更高效率的使用二級快取是解決不了了。

但是解決多表操作避免髒資料還是有法解決的。解決思路就是通過攔截器判斷執行的sql涉及到那些表(可以用jsqlparser解析),然後把相關表的快取自動清空。但是這種方式對快取的使用效率是很低的。