Mybatis二級緩存原理

gugumomo65 8年前發布 | 42K 次閱讀 MyBatis MyBatis3 持久層框架

記錄是一種精神,是加深理解最好的方式之一。

最近看了下Mybatis的源碼,分析了二級緩存的實現方式,在這里把他記下來。雖然這不復雜,對這方面的博客也有很多,寫的也很好。但我堅信看懂了是其一,能夠教別人或者描述清楚記下來才能真正的掌握。

這篇文章能夠幫你

  • 學會對Mybatis配置二級緩存
  • 學會Mybatis二級緩存的實現方式
  • 學會整合外部緩存框架(如: Ehcache )
  • 學會自定義二級緩存

1. Mybatis內部二級緩存的配置

要使用Mybatis的二級緩存,需要對Mybatis進行配置,配置分三步

  • Mybatis全局配置中啟用二級緩存配置
    <setting name="cacheEnabled" value="true"/>
  • 在對應的Mapper.xml中配置cache節點
    <mapper namespace="userMapper">
      <cache />
      <result ... />
      <select ... />
    </mapper>
  • 在對應的select查詢節點中添加useCache=true

    <select id="findUserById" parameterType="int" resultMap="user" useCache="true">
      select * from users where id=#{id};
    </select>
  • 高級配置

    a. 為每一個Mapper分配一個Cache緩存對象(使用<cache>節點配置)

    b. 多個Mapper共用一個Cache緩存對象(使用<cache-ref>節點配置)

只要簡單的三步配置即可開啟Mybatis的二級緩存了。在使用mybatis查詢時候("userMapper.findUserById"),不同會話(Sqlsession)在查詢時候,只會查詢數據庫一次,第二次會從二級緩存中讀取。

@Before
public void before() {
    String mybatisConfigFile = "MybatisConfig/Mybatis-conf.xml";
    InputStream stream = TestMybatis.class.getClassLoader().getResourceAsStream(mybatisConfigFile);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream); //構建sqlSession的工廠
}
@Test
public void test() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    User i = sqlSession.selectOne("userMapper.findUserById", 1);
    System.out.println(i);
    sqlSession.close();
    sqlSession = sqlSessionFactory.openSession();
    User x = sqlSession.selectOne("userMapper.findUserById", 1); // 讀取二級緩存數據
    System.out.println(x);
    sqlSession.close();
}

2. Mybatis內部二級緩存的設計及工作模式

首先我們要知道,mybatis的二級緩存是通過CacheExecutor實現的。CacheExecutor其實是Executor的代理對象。所有的查詢操作,在CacheExecutor中都會先匹配緩存中是否存在,不存在則查詢數據庫。

3. 內部二級緩存的實現詳解

竟然知道Mybatis二級緩存是通過CacheExecotur實現的,那看下Mybatis中創建Executor的過程

// 創建執行器(Configuration.newExecutor)
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
   //確保ExecutorType不為空(defaultExecutorType有可能為空)
   executorType = executorType == null ? defaultExecutorType : executorType;
   executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
   Executor executor;
   if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
   } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
   } else {
      executor = new SimpleExecutor(this, transaction);
   }
   if (cacheEnabled) { //重點在這里,如果啟用全局代理對象,返回Executor的Cache包裝類對象
      executor = new CachingExecutor(executor);
   }
   executor = (Executor) interceptorChain.pluginAll(executor);
   return executor;
}

重點在cacheEnabled這個參數。如果你看了我的文章[ Mybatis配置文件解析過程詳解 ],就應該知道了怎么設置cacheEnabled。對,就是此文章第一點說的開啟Mybatis的全局配置項。我們繼續看下CachingExecutor具體怎么實現的。

public class CachingExecutor implements Executor {
    private Executor delegate;
    public CachingExecutor(Executor delegate) {
        this.delegate = delegate;
        delegate.setExecutorWrapper(this);
    }
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        flushCacheIfRequired(ms); //是否需要更緩存
        return delegate.update(ms, parameterObject);  //更新數據
    }
    ......
}

很清晰,靜態代理模式。在CachingExecutor的所有操作都是通過調用內部的delegate對象執行的。緩存只應用于查詢,我們看下CachingExecutor的query方法。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    //創建緩存值
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    //獲取記錄
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

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
            if (ms.getStatementType() == StatementType.CALLABLE) {
                for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
                    if (parameterMapping.getMode() != ParameterMode.IN) {
                        throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
                    }
                }
            }
            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); // 結果保存到緩存中
            }
            return list;
        }
    }
    return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

如果MappedStatement中對應的Cache存在,并且對于的查詢開啟了二級緩存(useCache="true"),那么在CachingExecutor中會先從緩存中根據CacheKey獲取數據,如果緩存中不存在則從數據庫獲取。這里的代碼很簡單,很容易理解。

說到緩存,有效期和緩存策略不得不提。在Mybatis中二級緩存也實現了有效期的控制和緩存策略。Mybatis中是使用責任鏈模式實現的,具體可以看下mybatis的cache包

具體于配置如下:

<cache eviction="FIFO|LRU|SOFT|WEAK" flushInterval="300" size="100" />

對應具體實現源碼可以參考CacheBuilder類的源碼。

public Cache build() {
    if (implementation == null) { //緩存實現類
        implementation = PerpetualCache.class;
        if (decorators.size() == 0) {
            decorators.add(LruCache.class);
        }
    }
    Cache cache = newBaseCacheInstance(implementation, id);
    setCacheProperties(cache);
    if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache);
        }
        // 采用默認緩存包裝類
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        cache = new LoggingCache(cache);
    }
    return cache;
}

4. 一級緩存和二級緩存的使用順序

如果你的MyBatis使用了二級緩存,并且你的Mapper和select語句也配置使用了二級緩存,那么在執行select查詢的時候,MyBatis會先從二級緩存中取輸入,其次才是一級緩存,即MyBatis查詢數據的順序是:

二級緩存 ———> 一級緩存——> 數據庫

5. mybatis二級緩存和分頁插件同時使用產生的問題

問題:分頁插件開啟二級緩存后,分頁查詢時無論查詢哪一頁都返回第一頁的數據

在之前講解Mybatis的執行流程的時候提到,在開啟cache的前提下,Mybatis的executor會先從緩存里讀取數據,讀取不到才去數據庫查詢。問題就出在這里,sql自動生成插件和分頁插件執行的時機是在statementhandler里,而statementhandler是在executor之后執行的,無論sql自動生成插件和分頁插件都是通過改寫sql來實現的,executor在生成讀取cache的key(key由sql以及對應的參數值構成)時使用都是原始的sql,這樣當然就出問題了。

找到問題的原因后,解決起來就方便了。只要通過攔截器改寫executor里生成key的方法,在生成可以時使用自動生成的sql(對應sql自動生成插件)或加入分頁信息(對應分頁插件)就可以了。

參考: http://blog.csdn.net/hupanfeng/article/details/16950161

6. mybatis整合第三方緩存框架

我們以ehcache為例。對于ehcache我只會簡單的使用。這里我只是介紹Mybatis怎么使用ehcache,不對ehcache配置作說明。我們知道,在配置二級緩存時候,我們可以指定對應的實現類。這里需要mybatis-ehcache-1.0.3.jar這個jar包。在Mapper中我們只要配置如下即可。

<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

當然,項目中ehcache的配置還是需要的。

小結

對于Mybatis整合第三方的緩存,實現騎士很簡單,只要在配置的地方制定實現類即可。

Mybatis默認二級緩存的實現在集群或者分布式部署下是有問題的,Mybatis默認緩存只在當節點內有效,并且對緩存的失效操作無法同步的其他節點。需要整合第三方分布式緩存實現,如ehcache或者自定義實現。

 

來自:http://www.jianshu.com/p/5ff874fa696f

 

 本文由用戶 gugumomo65 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!