聊聊MyBatis緩存機制

原文:https://tech.meituan.com/mybatis_cache.html

前言

MyBatis是常見的Java數據庫訪問層框架。在日常工作中,開發人員多數情況下是使用MyBatis的默認緩存配置,但是MyBatis緩存機制有一些不足之處,在使用中容易引起臟數據,形成一些潛在的隱患。個人在業務開發中也處理過一些由于MyBatis緩存引發的開發問題,帶著個人的興趣,希望從應用及源碼的角度為讀者梳理MyBatis緩存機制。
本次分析中涉及到的代碼和數據庫表均放在GitHub上,地址: mybatis-cache-demo

目錄

本文按照以下順序展開。

  • 一級緩存介紹及相關配置。
  • 一級緩存工作流程及源碼分析。
  • 一級緩存總結。
  • 二級緩存介紹及相關配置。
  • 二級緩存源碼分析。
  • 二級緩存總結。
  • 全文總結。

一級緩存

一級緩存介紹

在應用運行過程中,我們有可能在一次數據庫會話中,執行多次查詢條件完全相同的SQL,MyBatis提供了一級緩存的方案優化這部分場景,如果是相同的SQL語句,會優先命中一級緩存,避免直接對數據庫進行查詢,提高性能。具體執行過程如下圖所示。


image

每個SqlSession中持有了Executor,每個Executor中有一個LocalCache。當用戶發起查詢時,MyBatis根據當前執行的語句生成MappedStatement,在Local Cache進行查詢,如果緩存命中的話,直接返回結果給用戶,如果緩存沒有命中的話,查詢數據庫,結果寫入Local Cache,最后返回結果給用戶。具體實現類的類關系圖如下圖所示。


image

一級緩存配置

我們來看看如何使用MyBatis一級緩存。開發者只需在MyBatis的配置文件中,添加如下語句,就可以使用一級緩存。共有兩個選項,SESSION或者STATEMENT,默認是SESSION級別,即在一個MyBatis會話中執行的所有語句,都會共享這一個緩存。一種是STATEMENT級別,可以理解為緩存只對當前執行的這一個Statement有效。

<setting name="localCacheScope" value="SESSION"/>

一級緩存實驗

接下來通過實驗,了解MyBatis一級緩存的效果,每個單元測試后都請恢復被修改的數據。
首先是創建示例表student,創建對應的POJO類和增改的方法,具體可以在entity包和mapper包中查看。

CREATE TABLE `student` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `age` tinyint(3) unsigned DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

在以下實驗中,id為1的學生名稱是凱倫。

實驗1

開啟一級緩存,范圍為會話級別,調用三次getStudentById,代碼如下所示:

public void getStudentById() throws Exception {
        SqlSession sqlSession = factory.openSession(true); // 自動提交事務
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        System.out.println(studentMapper.getStudentById(1));
        System.out.println(studentMapper.getStudentById(1));
        System.out.println(studentMapper.getStudentById(1));
    }

執行結果:


image

我們可以看到,只有第一次真正查詢了數據庫,后續的查詢使用了一級緩存。

實驗2

增加了對數據庫的修改操作,驗證在一次數據庫會話中,如果對數據庫發生了修改操作,一級緩存是否會失效。

@Test
public void addStudent() throws Exception {
        SqlSession sqlSession = factory.openSession(true); // 自動提交事務
        StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
        System.out.println(studentMapper.getStudentById(1));
        System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "個學生");
        System.out.println(studentMapper.getStudentById(1));
        sqlSession.close();
}

執行結果:


image

我們可以看到,在修改操作后執行的相同查詢,查詢了數據庫,一級緩存失效

實驗3

開啟兩個SqlSession,在sqlSession1中查詢數據,使一級緩存生效,在sqlSession2中更新數據庫,驗證一級緩存只在數據庫會話內部共享。

@Test
public void testLocalCacheScope() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "個學生的數據");
        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}

image

sqlSession2更新了id為1的學生的姓名,從凱倫改為了小岑,但session1之后的查詢中,id為1的學生的名字還是凱倫,出現了臟數據,也證明了之前的設想,一級緩存只在數據庫會話內部共享。

一級緩存工作流程&源碼分析

那么,一級緩存的工作流程是怎樣的呢?我們從源碼層面來學習一下。

工作流程

一級緩存執行的時序圖,如下圖所示。


image
源碼分析

接下來將對MyBatis查詢相關的核心類和一級緩存的源碼進行走讀。這對后面學習二級緩存也有幫助。
SqlSession: 對外提供了用戶和數據庫之間交互需要的所有方法,隱藏了底層的細節。默認實現類是DefaultSqlSession。

image

Executor: SqlSession向用戶提供操作數據庫的方法,但和數據庫操作有關的職責都會委托給Executor。

image

如下圖所示,Executor有若干個實現類,為Executor賦予了不同的能力,大家可以根據類名,自行學習每個類的基本作用。


image

在一級緩存的源碼分析中,主要學習BaseExecutor的內部實現。
BaseExecutor: BaseExecutor是一個實現了Executor接口的抽象類,定義若干抽象方法,在執行的時候,把具體的操作委托給子類進行執行。

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;

在一級緩存的介紹中提到對Local Cache的查詢和寫入是在Executor內部完成的。在閱讀BaseExecutor的代碼后發現Local Cache是BaseExecutor內部的一個成員變量,如下代碼所示。

public abstract class BaseExecutor implements Executor {
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;

Cache: MyBatis中的Cache接口,提供了和緩存相關的最基本的操作,如下圖所示。

image

有若干個實現類,使用裝飾器模式互相組裝,提供豐富的操控緩存的能力,部分實現類如下圖所示。


image

BaseExecutor成員變量之一的PerpetualCache,是對Cache接口最基本的實現,其實現非常簡單,內部持有HashMap,對一級緩存的操作實則是對HashMap的操作。如下代碼所示。

public class PerpetualCache implements Cache {
  private String id;
  private Map<Object, Object> cache = new HashMap<Object, Object>();

在閱讀相關核心類代碼后,從源代碼層面對一級緩存工作中涉及到的相關代碼,出于篇幅的考慮,對源碼做適當刪減,讀者朋友可以結合本文,后續進行更詳細的學習。
為執行和數據庫的交互,首先需要初始化SqlSession,通過DefaultSqlSessionFactory開啟SqlSession:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    ............
    final Executor executor = configuration.newExecutor(tx, execType);     
    return new DefaultSqlSession(configuration, executor, autoCommit);
}

在初始化SqlSesion時,會使用Configuration類創建一個全新的Executor,作為DefaultSqlSession構造函數的參數,創建Executor代碼如下所示:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    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);
    }
    // 尤其可以注意這里,如果二級緩存開關開啟的話,是使用CahingExecutor裝飾BaseExecutor的子類
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);                      
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

SqlSession創建完畢后,根據Statment的不同類型,會進入SqlSession的不同方法中,如果是Select語句的話,最后會執行到SqlSession的selectList,代碼如下所示:

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

SqlSession把具體的查詢職責委托給了Executor。如果只開啟了一級緩存的話,首先會進入BaseExecutor的query方法。代碼如下所示:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

在上述代碼中,會先根據傳入的參數生成CacheKey,進入該方法查看CacheKey是如何生成的,代碼如下所示:

CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中帶的參數
cacheKey.update(value);

在上述的代碼中,將MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的參數傳入了CacheKey這個類,最終構成CacheKey。以下是這個類的內部結構:

private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;

private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;

public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLYER;
    this.count = 0;
    this.updateList = new ArrayList<Object>();
}

首先是成員變量和構造函數,有一個初始的hachcode和乘數,同時維護了一個內部的updatelist。在CacheKey的update方法中,會進行一個hashcode和checksum的計算,同時把傳入的參數添加進updatelist中。如下代碼所示。

public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object); 
    count++;
    checksum += baseHashCode;
    baseHashCode *= count;
    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
}

同時重寫了CacheKey的equals方法,代碼如下所示:

@Override
public boolean equals(Object object) {
    .............
    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
}

除去hashcode,checksum和count的比較外,只要updatelist中的元素一一對應相等,那么就可以認為是CacheKey相等。只要兩條SQL的下列五個值相同,即可以認為是相同的SQL。

Statement Id + Offset + Limmit + Sql + Params

BaseExecutor的query方法繼續往下走,代碼如下所示:

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

如果查不到的話,就從數據庫查,在queryFromDatabase中,會對localcache進行寫入。
在query方法執行的最后,會判斷一級緩存級別是否是STATEMENT級別,如果是的話,就清空緩存,這也就是STATEMENT級別的一級緩存無法共享localCache的原因。代碼如下所示:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
}

在源碼分析的最后,我們確認一下,如果是insert/delete/update方法,緩存就會刷新的原因。
SqlSession的insert方法和delete方法,都會統一走update的流程,代碼如下所示:

@Override
public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }
   @Override
  public int delete(String statement) {
    return update(statement, null);
}

update方法也是委托給了Executor執行。BaseExecutor的執行方法如下所示。

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

每次執行update前都會清空localCache。

至此,一級緩存的工作流程講解以及源碼分析完畢。

總結

  1. MyBatis一級緩存的生命周期和SqlSession一致。
  2. MyBatis一級緩存內部設計簡單,只是一個沒有容量限定的HashMap,在緩存的功能性上有所欠缺。
  3. MyBatis的一級緩存最大范圍是SqlSession內部,有多個SqlSession或者分布式的環境下,數據庫寫操作會引起臟數據,建議設定緩存級別為Statement。

二級緩存

二級緩存介紹

在上文中提到的一級緩存中,其最大的共享范圍就是一個SqlSession內部,如果多個SqlSession之間需要共享緩存,則需要使用到二級緩存。開啟二級緩存后,會使用CachingExecutor裝飾Executor,進入一級緩存的查詢流程前,先在CachingExecutor進行二級緩存的查詢,具體的工作流程如下所示。


image

二級緩存開啟后,同一個namespace下的所有操作語句,都影響著同一個Cache,即二級緩存被多個SqlSession共享,是一個全局的變量。
當開啟緩存后,數據的查詢執行的流程就是 二級緩存 -> 一級緩存 -> 數據庫。

二級緩存配置

要正確的使用二級緩存,需完成如下配置的。

  1. 在MyBatis的配置文件中開啟二級緩存。

    <setting name="cacheEnabled" value="true"/>
    
    
  2. 在MyBatis的映射XML中配置cache或者 cache-ref 。

cache標簽用于聲明這個namespace使用二級緩存,并且可以自定義配置。

<cache/>

  • type:cache使用的類型,默認是PerpetualCache,這在一級緩存中提到過。
  • eviction: 定義回收的策略,常見的有FIFO,LRU。
  • flushInterval: 配置一定時間自動刷新緩存,單位是毫秒。
  • size: 最多緩存對象的個數。
  • readOnly: 是否只讀,若配置可讀寫,則需要對應的實體類能夠序列化。
  • blocking: 若緩存中找不到對應的key,是否會一直blocking,直到有對應的數據進入緩存。

cache-ref代表引用別的命名空間的Cache配置,兩個命名空間的操作使用的是同一個Cache。

<cache-ref namespace="mapper.StudentMapper"/>

二級緩存實驗

接下來我們通過實驗,了解MyBatis二級緩存在使用上的一些特點。
在本實驗中,id為1的學生名稱初始化為點點。

實驗1

測試二級緩存效果,不提交事務,sqlSession1查詢完數據后,sqlSession2相同的查詢是否會從緩存中獲取數據。

@Test
public void testCacheWithoutCommitOrClose() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}

執行結果:


image

我們可以看到,當sqlsession沒有調用commit()方法時,二級緩存并沒有起到作用。

實驗2

測試二級緩存效果,當提交事務時,sqlSession1查詢完數據后,sqlSession2相同的查詢是否會從緩存中獲取數據。

@Test
public void testCacheWithCommitOrClose() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}

image

從圖上可知,sqlsession2的查詢,使用了緩存,緩存的命中率是0.5。

實驗3

測試update操作是否會刷新該namespace下的二級緩存。

@Test
public void testCacheWithUpdate() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        SqlSession sqlSession3 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentById(1));
        sqlSession1.commit();
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));

        studentMapper3.updateStudentName("方方",1);
        sqlSession3.commit();
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentById(1));
}

image

我們可以看到,在sqlSession3更新數據庫,并提交事務后,sqlsession2的StudentMapper namespace下的查詢走了數據庫,沒有走Cache。

實驗4

驗證MyBatis的二級緩存不適應用于映射文件中存在多表查詢的情況。
通常我們會為每個單表創建單獨的映射文件,由于MyBatis的二級緩存是基于namespace的,多表查詢語句所在的namspace無法感應到其他namespace中的語句對多表查詢中涉及的表進行的修改,引發臟數據問題。

@Test
public void testCacheWithDiffererntNamespace() throws Exception {
        SqlSession sqlSession1 = factory.openSession(true); 
        SqlSession sqlSession2 = factory.openSession(true); 
        SqlSession sqlSession3 = factory.openSession(true); 

        StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
        StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
        ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);

        System.out.println("studentMapper讀取數據: " + studentMapper.getStudentByIdWithClassInfo(1));
        sqlSession1.close();
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentByIdWithClassInfo(1));

        classMapper.updateClassName("特色一班",1);
        sqlSession3.commit();
        System.out.println("studentMapper2讀取數據: " + studentMapper2.getStudentByIdWithClassInfo(1));
}

執行結果:


image

在這個實驗中,我們引入了兩張新的表,一張class,一張classroom。class中保存了班級的id和班級名,classroom中保存了班級id和學生id。我們在StudentMapper中增加了一個查詢方法getStudentByIdWithClassInfo,用于查詢學生所在的班級,涉及到多表查詢。在ClassMapper中添加了updateClassName,根據班級id更新班級名的操作。
當sqlsession1的studentmapper查詢數據后,二級緩存生效。保存在StudentMapper的namespace下的cache中。當sqlSession3的classMapper的updateClassName方法對class表進行更新時,updateClassName不屬于StudentMapper的namespace,所以StudentMapper下的cache沒有感應到變化,沒有刷新緩存。當StudentMapper中同樣的查詢再次發起時,從緩存中讀取了臟數據。

實驗5

為了解決實驗4的問題呢,可以使用Cache ref,讓ClassMapper引用StudenMapper命名空間,這樣兩個映射文件對應的Sql操作都使用的是同一塊緩存了。
執行結果:


image

不過這樣做的后果是,緩存的粒度變粗了,多個Mapper namespace下的所有操作都會對緩存使用造成影響。

二級緩存源碼分析

MyBatis二級緩存的工作流程和前文提到的一級緩存類似,只是在一級緩存處理前,用CachingExecutor裝飾了BaseExecutor的子類,在委托具體職責給delegate之前,實現了二級緩存的查詢和寫入功能,具體類關系圖如下圖所示。


image
源碼分析

源碼分析從CachingExecutor的query方法展開,源代碼走讀過程中涉及到的知識點較多,不能一一詳細講解,讀者朋友可以自行查詢相關資料來學習。
CachingExecutor的query方法,首先會從MappedStatement中獲得在配置初始化時賦予的Cache。

Cache cache = ms.getCache();

本質上是裝飾器模式的使用,具體的裝飾鏈是

SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache。

image

以下是具體這些Cache實現類的介紹,他們的組合為Cache賦予了不同的能力。

  • SynchronizedCache: 同步Cache,實現比較簡單,直接使用synchronized修飾方法。
  • LoggingCache: 日志功能,裝飾類,用于記錄緩存的命中率,如果開啟了DEBUG模式,則會輸出命中率日志。
  • SerializedCache: 序列化功能,將值序列化后存到緩存中。該功能用于緩存返回一份實例的Copy,用于保存線程安全。
  • LruCache: 采用了Lru算法的Cache實現,移除最近最少使用的key/value。
  • PerpetualCache: 作為為最基礎的緩存類,底層實現比較簡單,直接使用了HashMap。

然后是判斷是否需要刷新緩存,代碼如下所示:

flushCacheIfRequired(ms);

在默認的設置中SELECT語句不會刷新緩存,insert/update/delte會刷新緩存。進入該方法。代碼如下所示:

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);
    }
}

MyBatis的CachingExecutor持有了TransactionalCacheManager,即上述代碼中的tcm。
TransactionalCacheManager中持有了一個Map,代碼如下所示:

private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

這個Map保存了Cache和用TransactionalCache包裝后的Cache的映射關系。
TransactionalCache實現了Cache接口,CachingExecutor會默認使用他包裝初始生成的Cache,作用是如果事務提交,對緩存的操作才會生效,如果事務回滾或者不提交事務,則不對緩存產生影響。
在TransactionalCache的clear,有以下兩句。清空了需要在提交時加入緩存的列表,同時設定提交時清空緩存,代碼如下所示:

@Override
public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
}

CachingExecutor繼續往下走,ensureNoOutParams主要是用來處理存儲過程的,暫時不用考慮。

if (ms.isUseCache() && resultHandler == null) {
    ensureNoOutParams(ms, parameterObject, boundSql);

之后會嘗試從tcm中獲取緩存的列表。

List<E> list = (List<E>) tcm.getObject(cache, key);

在getObject方法中,會把獲取值的職責一路傳遞,最終到PerpetualCache。如果沒有查到,會把key加入Miss集合,這個主要是為了統計命中率。

Object object = delegate.getObject(key);
if (object == null) {
    entriesMissedInCache.add(key);
}

CachingExecutor繼續往下走,如果查詢到數據,則調用tcm.putObject方法,往緩存中放入值。

if (list == null) {
    list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    tcm.putObject(cache, key, list); // issue #578 and #116
}

tcm的put方法也不是直接操作緩存,只是在把這次的數據和key放入待提交的Map中。

@Override
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

從以上的代碼分析中,我們可以明白,如果不調用commit方法的話,由于TranscationalCache的作用,并不會對二級緩存造成直接的影響。因此我們看看Sqlsession的commit方法中做了什么。代碼如下所示:

@Override
public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));

因為我們使用了CachingExecutor,首先會進入CachingExecutor實現的commit方法。

@Override
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
}

會把具體commit的職責委托給包裝的Executor。主要是看下tcm.commit(),tcm最終又會調用到TrancationalCache。

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
}

看到這里的clearOnCommit就想起剛才TrancationalCache的clear方法設置的標志位,真正的清理Cache是放到這里來進行的。具體清理的職責委托給了包裝的Cache類。之后進入flushPendingEntries方法。代碼如下所示:

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    ................
}

在flushPendingEntries中,將待提交的Map進行循環處理,委托給包裝的Cache類,進行putObject的操作。
后續的查詢操作會重復執行這套流程。如果是insert|update|delete的話,會統一進入CachingExecutor的update方法,其中調用了這個函數,代碼如下所示:

private void flushCacheIfRequired(MappedStatement ms)

在二級緩存執行流程后就會進入一級緩存的執行流程,因此不再贅述。

總結

  1. MyBatis的二級緩存相對于一級緩存來說,實現了SqlSession之間緩存數據的共享,同時粒度更加的細,能夠到namespace級別,通過Cache接口實現類不同的組合,對Cache的可控性也更強。
  2. MyBatis在多表查詢時,極大可能會出現臟數據,有設計上的缺陷,安全使用二級緩存的條件比較苛刻。
  3. 在分布式環境下,由于默認的MyBatis Cache實現都是基于本地的,分布式環境下必然會出現讀取到臟數據,需要使用集中式緩存將MyBatis的Cache接口實現,有一定的開發成本,直接使用Redis,Memcached等分布式緩存可能成本更低,安全性也更高。

全文總結

本文對介紹了MyBatis一二級緩存的基本概念,并從應用及源碼的角度對MyBatis的緩存機制進行了分析。最后對MyBatis緩存機制做了一定的總結,個人建議MyBatis緩存特性在生產環境中進行關閉,單純作為一個ORM框架使用可能更為合適。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容