Mybatis 緩存特性的使用及源碼分析

前言

主題是Mybatis一級(jí)和二級(jí)緩存的應(yīng)用及源碼分析。希望在本場(chǎng)chat結(jié)束后,能夠幫助讀者朋友明白以下三點(diǎn)。

Mybatis是什么。

Mybatis一級(jí)和二級(jí)緩存如何配置使用。

Mybatis一級(jí)和二級(jí)緩存的工作流程及源碼分析。

本次分析中涉及到的代碼和數(shù)據(jù)庫表均放在Github上,地址:

https://github.com/kailuncen/mybatis-cache-demo

目錄

為達(dá)到以上三個(gè)目的,本文按照以下順序展開。

Mybatis的基礎(chǔ)概念。

一級(jí)緩存介紹及相關(guān)配置。

一級(jí)緩存工作流程及源碼分析。

一級(jí)緩存總結(jié)。

二級(jí)緩存介紹及相關(guān)配置。

二級(jí)緩存源碼分析。

二級(jí)緩存總結(jié)。

全文總結(jié)。

Mybatis的基礎(chǔ)概念

本章節(jié)會(huì)對(duì)Mybatis進(jìn)行大體的介紹,分為官方定義和核心組件介紹。

首先是Mybatis官方定義,如下所示。

MyBatis是支持定制化SQL、存儲(chǔ)過程以及高級(jí)映射的優(yōu)秀的持久層框架。MyBatis避免了幾乎所有的JDBC代碼和手動(dòng)設(shè)置參數(shù)以及獲取結(jié)果集。MyBatis可以對(duì)配置和原生Map使用簡單的XML或注解,將接口和Java 的POJOs(Plain Old Java Objects,普通的 Java對(duì)象)映射成數(shù)據(jù)庫中的記錄。

其次是Mybatis的幾個(gè)核心概念。

SqlSession : 代表和數(shù)據(jù)庫的一次會(huì)話,向用戶提供了操作數(shù)據(jù)庫的方法。

MappedStatement: 代表要發(fā)往數(shù)據(jù)庫執(zhí)行的指令,可以理解為是Sql的抽象表示。

Executor: 具體用來和數(shù)據(jù)庫交互的執(zhí)行器,接受MappedStatement作為參數(shù)。

映射接口: 在接口中會(huì)要執(zhí)行的Sql用一個(gè)方法來表示,具體的Sql寫在映射文件中。

映射文件: 可以理解為是Mybatis編寫Sql的地方,通常來說每一張單表都會(huì)對(duì)應(yīng)著一個(gè)映射文件,在該文件中會(huì)定義Sql語句入?yún)⒑统鰠⒌男问健?/p>

下圖就是一個(gè)針對(duì)Student表操作的接口文件StudentMapper,在StudentMapper中,我們可以若干方法,這個(gè)方法背后就是代表著要執(zhí)行的Sql的意義。

通常也可以把涉及多表查詢的方法定義在StudentMapper中,如果查詢的主體仍然是Student表的信息。也可以將涉及多表查詢的語句單獨(dú)抽出一個(gè)獨(dú)立的接口文件。

在定義完接口文件后,我們會(huì)開發(fā)一個(gè)Sql映射文件,主要由mapper元素和select|insert|update|delete元素構(gòu)成,如下圖所示。

mapper元素代表這個(gè)文件是一個(gè)映射文件,使用namespace和具體的映射接口綁定起來,namespace的值就是這個(gè)接口的全限定類名。select|insert|update|delete代表的是Sql語句,映射接口中定義的每一個(gè)方法也會(huì)和映射文件中的語句通過id的方式綁定起來,方法名就是語句的id,同時(shí)會(huì)定義語句的入?yún)⒑统鰠ⅲ糜谕瓿珊蚃ava對(duì)象之間的轉(zhuǎn)換。

在Mybatis初始化的時(shí)候,每一個(gè)語句都會(huì)使用對(duì)應(yīng)的MappedStatement代表,使用namespace+語句本身的id來代表這個(gè)語句。如下代碼所示,使用mapper.StudentMapper.getStudentById代表其對(duì)應(yīng)的Sql。

SELECT id,name,age FROM student WHERE id = #{id}

在Mybatis執(zhí)行時(shí),會(huì)進(jìn)入對(duì)應(yīng)接口的方法,通過類名加上方法名的組合生成id,找到需要的MappedStatement,交給執(zhí)行器使用。 至此,Mybatis的基礎(chǔ)概念介紹完畢。

一級(jí)緩存

一級(jí)緩存介紹

在系統(tǒng)代碼的運(yùn)行中,我們可能會(huì)在一個(gè)數(shù)據(jù)庫會(huì)話中,執(zhí)行多次查詢條件完全相同的Sql,鑒于日常應(yīng)用的大部分場(chǎng)景都是讀多寫少,這重復(fù)的查詢會(huì)帶來一定的網(wǎng)絡(luò)開銷,同時(shí)select查詢的量比較大的話,對(duì)數(shù)據(jù)庫的性能是有比較大的影響的。

如果是Mysql數(shù)據(jù)庫的話,在服務(wù)端和Jdbc端都開啟預(yù)編譯支持的話,可以在本地JVM端緩存Statement,可以在Mysql服務(wù)端直接執(zhí)行Sql,省去編譯Sql的步驟,但也無法避免和數(shù)據(jù)庫之間的重復(fù)交互。關(guān)于Jdbc和Mysql預(yù)編譯緩存的事情,可以看我的這篇博客JDBC和Mysql那些事。

https://my.oschina.net/kailuncen/blog/905395

Mybatis提供了一級(jí)緩存的方案來優(yōu)化在數(shù)據(jù)庫會(huì)話間重復(fù)查詢的問題。實(shí)現(xiàn)的方式是每一個(gè)SqlSession中都持有了自己的緩存,一種是SESSION級(jí)別,即在一個(gè)Mybatis會(huì)話中執(zhí)行的所有語句,都會(huì)共享這一個(gè)緩存。一種是STATEMENT級(jí)別,可以理解為緩存只對(duì)當(dāng)前執(zhí)行的這一個(gè)statement有效。如果用一張圖來代表一級(jí)查詢的查詢過程的話,可以用下圖表示。

每一個(gè)SqlSession中持有了自己的Executor,每一個(gè)Executor中有一個(gè)Local Cache。當(dāng)用戶發(fā)起查詢時(shí),Mybatis會(huì)根據(jù)當(dāng)前執(zhí)行的MappedStatement生成一個(gè)key,去Local Cache中查詢,如果緩存命中的話,返回。如果緩存沒有命中的話,則寫入Local Cache,最后返回結(jié)果給用戶。

一級(jí)緩存配置

上文介紹了一級(jí)緩存的實(shí)現(xiàn)方式,解決了什么問題。在這個(gè)章節(jié),我們學(xué)習(xí)如何使用Mybatis的一級(jí)緩存。只需要在Mybatis的配置文件中,添加如下語句,就可以使用一級(jí)緩存。共有兩個(gè)選項(xiàng),SESSION或者STATEMENT,默認(rèn)是SESSION級(jí)別。

一級(jí)緩存實(shí)驗(yàn)

配置完畢后,通過實(shí)驗(yàn)的方式了解Mybatis一級(jí)緩存的效果。每一個(gè)單元測(cè)試后都請(qǐng)恢復(fù)被修改的數(shù)據(jù)。

首先是創(chuàng)建了一個(gè)示例表student,為其創(chuàng)建了對(duì)應(yīng)的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;

在以下實(shí)驗(yàn)中,id為1的學(xué)生名稱是凱倫。

實(shí)驗(yàn)1

開啟一級(jí)緩存,范圍為會(huì)話級(jí)別,調(diào)用三次getStudentById,代碼如下所示:

public void getStudentById() throws Exception {

SqlSession sqlSession = factory.openSession(true); // 自動(dòng)提交事務(wù)

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

System.out.println(studentMapper.getStudentById(1));

System.out.println(studentMapper.getStudentById(1));

System.out.println(studentMapper.getStudentById(1));

}

執(zhí)行結(jié)果:

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

實(shí)驗(yàn)2

在這次的試驗(yàn)中,我們?cè)黾恿藢?duì)數(shù)據(jù)庫的修改操作,驗(yàn)證在一次數(shù)據(jù)庫會(huì)話中,對(duì)數(shù)據(jù)庫發(fā)生了修改操作,一級(jí)緩存是否會(huì)失效。

@Test

public void addStudent() throws Exception {

SqlSession sqlSession = factory.openSession(true); // 自動(dòng)提交事務(wù)

StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);

System.out.println(studentMapper.getStudentById(1));

System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "個(gè)學(xué)生");

System.out.println(studentMapper.getStudentById(1));

sqlSession.close();

}

執(zhí)行結(jié)果:

我們可以看到,在修改操作后執(zhí)行的相同查詢,查詢了數(shù)據(jù)庫,一級(jí)緩存失效。

實(shí)驗(yàn)3

開啟兩個(gè)SqlSession,在sqlSession1中查詢數(shù)據(jù),使一級(jí)緩存生效,在sqlSession2中更新數(shù)據(jù)庫,驗(yàn)證一級(jí)緩存只在數(shù)據(jù)庫會(huì)話內(nèi)部共享。

@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讀取數(shù)據(jù): " + studentMapper.getStudentById(1));

System.out.println("studentMapper讀取數(shù)據(jù): " + studentMapper.getStudentById(1));

System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "個(gè)學(xué)生的數(shù)據(jù)");

System.out.println("studentMapper讀取數(shù)據(jù): " + studentMapper.getStudentById(1));

System.out.println("studentMapper2讀取數(shù)據(jù): " + studentMapper2.getStudentById(1));

}

我們可以看到,sqlSession2更新了id為1的學(xué)生的姓名,從凱倫改為了小岑,但session1之后的查詢中,id為1的學(xué)生的名字還是凱倫,出現(xiàn)了臟數(shù)據(jù),也證明了我們之前就得到的結(jié)論,一級(jí)緩存只存在于只在數(shù)據(jù)庫會(huì)話內(nèi)部共享。

一級(jí)緩存工作流程&源碼分析

這一章節(jié)主要從一級(jí)緩存的工作流程和源碼層面對(duì)一級(jí)緩存進(jìn)行學(xué)習(xí)。

工作流程

根據(jù)一級(jí)緩存的工作流程,我們繪制出一級(jí)緩存執(zhí)行的時(shí)序圖,如下圖所示。

主要步驟如下:

1. 對(duì)于某個(gè)Select Statement,根據(jù)該Statement生成key。

2. 判斷在Local Cache中,該key是否用對(duì)應(yīng)的數(shù)據(jù)存在。

3. 如果命中,則跳過查詢數(shù)據(jù)庫,繼續(xù)往下走。

4. 如果沒命中

4.1 ?去數(shù)據(jù)庫中查詢數(shù)據(jù),得到查詢結(jié)果;

4.2 ?將key和查詢到的結(jié)果作為key和value,放入Local Cache中。

4.3. 將查詢結(jié)果返回;

5. 判斷緩存級(jí)別是否為STATEMENT級(jí)別,如果是的話,清空本地緩存。

源碼分析

了解具體的工作流程后,我們隊(duì)Mybatis查詢相關(guān)的核心類和一級(jí)緩存的源碼進(jìn)行走讀。這對(duì)于之后學(xué)習(xí)二級(jí)緩存時(shí)也有幫助。 SqlSession: 對(duì)外提供了用戶和數(shù)據(jù)庫之間交互需要的所有方法,隱藏了底層的細(xì)節(jié)。它的一個(gè)默認(rèn)實(shí)現(xiàn)類是DefaultSqlSession。

Executor: SqlSession向用戶提供操作數(shù)據(jù)庫的方法,但和數(shù)據(jù)庫操作有關(guān)的職責(zé)都會(huì)委托給Executor。

如下圖所示,Executor有若干個(gè)實(shí)現(xiàn)類,為Executor賦予了不同的能力,大家可以根據(jù)類名,自行私下學(xué)習(xí)每個(gè)類的基本作用。

在一級(jí)緩存章節(jié),我們主要學(xué)習(xí)BaseExecutor。

BaseExecutor: BaseExecutor是一個(gè)實(shí)現(xiàn)了Executor接口的抽象類,定義若干抽象方法,在執(zhí)行的時(shí)候,把具體的操作委托給子類進(jìn)行執(zhí)行。

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

protected abstract List doFlushStatements(boolean isRollback) throws SQLException;

protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;

protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;

在一級(jí)緩存的介紹中,我們提到對(duì)Local Cache的查詢和寫入是在Executor內(nèi)部完成的。在閱讀BaseExecutor的代碼后,我們也發(fā)現(xiàn)Local Cache就是它內(nèi)部的一個(gè)成員變量,如下代碼所示。

public abstract class BaseExecutor implements Executor {

protected ConcurrentLinkedQueue deferredLoads;

protected PerpetualCache localCache;

Cache: Mybatis中的Cache接口,提供了和緩存相關(guān)的最基本的操作,有若干個(gè)實(shí)現(xiàn)類,使用裝飾器模式互相組裝,提供豐富的操控緩存的能力。

BaseExecutor成員變量之一的PerpetualCache,就是對(duì)Cache接口最基本的實(shí)現(xiàn),其實(shí)現(xiàn)非常的簡內(nèi)部持有了hashmap,對(duì)一級(jí)緩存的操作其實(shí)就是對(duì)這個(gè)hashmap的操作。如下代碼所示。

public class PerpetualCache implements Cache {

private String id;

private Map cache = new HashMap();

在閱讀相關(guān)核心類代碼后,從源代碼層面對(duì)一級(jí)緩存工作中涉及到的相關(guān)代碼,出于篇幅的考慮,對(duì)源碼做適當(dāng)刪減,讀者朋友可以結(jié)合本文,后續(xù)進(jìn)行更詳細(xì)的學(xué)習(xí)。

為了執(zhí)行和數(shù)據(jù)庫的交互,首先會(huì)通過DefaultSqlSessionFactory開啟一個(gè)SqlSession,在創(chuàng)建SqlSession的過程中,會(huì)通過Configuration類創(chuàng)建一個(gè)全新的Executor,作為DefaultSqlSession構(gòu)造函數(shù)的參數(shù),代碼如下所示。

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {

............

final Executor executor = configuration.newExecutor(tx, execType);

return new DefaultSqlSession(configuration, executor, autoCommit);

}

如果用戶不進(jìn)行制定的話,Configuration在創(chuàng)建Executor時(shí),默認(rèn)創(chuàng)建的類型就是SimpleExecutor,它是一個(gè)簡單的執(zhí)行類,只是單純執(zhí)行Sql。以下是具體用來創(chuàng)建的代碼。

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

}

// 尤其可以注意這里,如果二級(jí)緩存開關(guān)開啟的話,是使用CahingExecutor裝飾BaseExecutor的子類

if (cacheEnabled) {

executor = new CachingExecutor(executor);

}

executor = (Executor) interceptorChain.pluginAll(executor);

return executor;

}

在SqlSession創(chuàng)建完畢后,根據(jù)Statment的不同類型,會(huì)進(jìn)入SqlSession的不同方法中,如果是Select語句的話,最后會(huì)執(zhí)行到SqlSession的selectList,代碼如下所示。

@Override

public List selectList(String statement, Object parameter, RowBounds rowBounds) {

MappedStatement ms = configuration.getMappedStatement(statement);

return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);

}

在上文的代碼中,SqlSession把具體的查詢職責(zé)委托給了Executor。如果只開啟了一級(jí)緩存的話,首先會(huì)進(jìn)入BaseExecutor的query方法。代碼如下所示。

@Override

public List 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);

}

在上述代碼中,會(huì)先根據(jù)傳入的參數(shù)生成CacheKey,進(jìn)入該方法查看CacheKey是如何生成的,代碼如下所示。

CacheKey cacheKey = new CacheKey();

cacheKey.update(ms.getId());

cacheKey.update(rowBounds.getOffset());

cacheKey.update(rowBounds.getLimit());

cacheKey.update(boundSql.getSql());

//后面是update了sql中帶的參數(shù)

cacheKey.update(value);

在上述的代碼中,我們可以看到它將MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的參數(shù)傳入了CacheKey這個(gè)類,最終生成了CacheKey。我們看一下這個(gè)類的結(jié)構(gòu)。

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 updateList;

public CacheKey() {

this.hashcode = DEFAULT_HASHCODE;

this.multiplier = DEFAULT_MULTIPLYER;

this.count = 0;

this.updateList = new ArrayList();

}

首先是它的成員變量和構(gòu)造函數(shù),有一個(gè)初始的hachcode和乘數(shù),同時(shí)維護(hù)了一個(gè)內(nèi)部的updatelist。在CacheKey的update方法中,會(huì)進(jìn)行一個(gè)hashcode和checksum的計(jì)算,同時(shí)把傳入的參數(shù)添加進(jìn)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相等的呢,在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中的元素一一對(duì)應(yīng)相等,那么就可以認(rèn)為是CacheKey相等。只要兩條Sql的下列五個(gè)值相同,即可以認(rèn)為是相同的Sql。

Statement Id + Offset + Limmit + Sql + Params

BaseExecutor的query方法繼續(xù)往下走,代碼如下所示。

list = resultHandler == null ? (List) localCache.getObject(key) : null;

if (list != null) {

// 這個(gè)主要是處理存儲(chǔ)過程用的。

handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

} else {

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

如果查不到的話,就從數(shù)據(jù)庫查,在queryFromDatabase中,會(huì)對(duì)localcache進(jìn)行寫入。

在query方法執(zhí)行的最后,會(huì)判斷一級(jí)緩存級(jí)別是否是STATEMENT級(jí)別,如果是的話,就清空緩存,這也就是STATEMENT級(jí)別的一級(jí)緩存無法共享localCache的原因。代碼如下所示。

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {

clearLocalCache();

}

在源碼分析的最后,我們確認(rèn)一下,如果是insert/delete/update方法,緩存就會(huì)刷新的原因。

SqlSession的insert方法和delete方法,都會(huì)統(tǒng)一走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執(zhí)行。BaseExecutor的執(zhí)行方法如下所示。

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

}

每次執(zhí)行update前都會(huì)清空localCache。

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

總結(jié)

Mybatis一級(jí)緩存的生命周期和SqlSession一致。

Mybatis的緩存是一個(gè)粗粒度的緩存,沒有更新緩存和緩存過期的概念,同時(shí)只是使用了默認(rèn)的hashmap,也沒有做容量上的限定。

Mybatis的一級(jí)緩存最大范圍是SqlSession內(nèi)部,有多個(gè)SqlSession或者分布式的環(huán)境下,有操作數(shù)據(jù)庫寫的話,會(huì)引起臟數(shù)據(jù),建議是把一級(jí)緩存的默認(rèn)級(jí)別設(shè)定為Statement,即不使用一級(jí)緩存。

二級(jí)緩存

二級(jí)緩存介紹

在上文中提到的一級(jí)緩存中,其最大的共享范圍就是一個(gè)SqlSession內(nèi)部,那么如何讓多個(gè)SqlSession之間也可以共享緩存呢,答案是二級(jí)緩存。 當(dāng)開啟二級(jí)緩存后,會(huì)使用CachingExecutor裝飾Executor,在進(jìn)入后續(xù)執(zhí)行前,先在CachingExecutor進(jìn)行二級(jí)緩存的查詢,具體的工作流程如下所示。

在二級(jí)緩存的使用中,一個(gè)namespace下的所有操作語句,都影響著同一個(gè)Cache,即二級(jí)緩存是被多個(gè)SqlSession共享著的,是一個(gè)全局的變量。 當(dāng)開啟緩存后,數(shù)據(jù)的查詢執(zhí)行的流程就是 二級(jí)緩存 -> 一級(jí)緩存 -> 數(shù)據(jù)庫。

二級(jí)緩存配置

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

1 在Mybatis的配置文件中開啟二級(jí)緩存。

2 在Mybatis的映射XML中配置cache或者 cache-ref 。

cache標(biāo)簽用于聲明這個(gè)namespace使用二級(jí)緩存,并且可以自定義配置。

type: cache使用的類型,默認(rèn)是PerpetualCache,這在一級(jí)緩存中提到過。

eviction: 定義回收的策略,常見的有FIFO,LRU。

flushInterval: 配置一定時(shí)間自動(dòng)刷新緩存,單位是毫秒

size: 最多緩存對(duì)象的個(gè)數(shù)

readOnly: 是否只讀,若配置可讀寫,則需要對(duì)應(yīng)的實(shí)體類能夠序列化。

blocking: 若緩存中找不到對(duì)應(yīng)的key,是否會(huì)一直blocking,直到有對(duì)應(yīng)的數(shù)據(jù)進(jìn)入緩存。

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

二級(jí)緩存實(shí)驗(yàn)

在本章節(jié),通過實(shí)驗(yàn),了解Mybatis二級(jí)緩存在使用上的一些特點(diǎn)。

在本實(shí)驗(yàn)中,id為1的學(xué)生名稱初始化為點(diǎn)點(diǎn)。

實(shí)驗(yàn)1

測(cè)試二級(jí)緩存效果,不提交事務(wù),sqlSession1查詢完數(shù)據(jù)后,sqlSession2相同的查詢是否會(huì)從緩存中獲取數(shù)據(jù)。

@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讀取數(shù)據(jù): " + studentMapper.getStudentById(1));

System.out.println("studentMapper2讀取數(shù)據(jù): " + studentMapper2.getStudentById(1));

}

執(zhí)行結(jié)果:

我們可以看到,當(dāng)sqlsession沒有調(diào)用commit()方法時(shí),二級(jí)緩存并沒有起到作用。

實(shí)驗(yàn)2

測(cè)試二級(jí)緩存效果,當(dāng)提交事務(wù)時(shí),sqlSession1查詢完數(shù)據(jù)后,sqlSession2相同的查詢是否會(huì)從緩存中獲取數(shù)據(jù)。

@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讀取數(shù)據(jù): " + studentMapper.getStudentById(1));

sqlSession1.commit();

System.out.println("studentMapper2讀取數(shù)據(jù): " + studentMapper2.getStudentById(1));

}

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

實(shí)驗(yàn)3

測(cè)試update操作是否會(huì)刷新該namespace下的二級(jí)緩存。

@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讀取數(shù)據(jù): " + studentMapper.getStudentById(1));

sqlSession1.commit();

System.out.println("studentMapper2讀取數(shù)據(jù): " + studentMapper2.getStudentById(1));

studentMapper3.updateStudentName("方方",1);

sqlSession3.commit();

System.out.println("studentMapper2讀取數(shù)據(jù): " + studentMapper2.getStudentById(1));

}

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

實(shí)驗(yàn)4

驗(yàn)證Mybatis的二級(jí)緩存不適應(yīng)用于映射文件中存在多表查詢的情況。一般來說,我們會(huì)為每一個(gè)單表創(chuàng)建一個(gè)單獨(dú)的映射文件,如果存在涉及多個(gè)表的查詢的話,由于Mybatis的二級(jí)緩存是基于namespace的,多表查詢語句所在的namspace無法感應(yīng)到其他namespace中的語句對(duì)多表查詢中涉及的表進(jìn)行了修改,引發(fā)臟數(shù)據(jù)問題。

@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讀取數(shù)據(jù): " + studentMapper.getStudentByIdWithClassInfo(1));

sqlSession1.close();

System.out.println("studentMapper2讀取數(shù)據(jù): " + studentMapper2.getStudentByIdWithClassInfo(1));

classMapper.updateClassName("特色一班",1);

sqlSession3.commit();

System.out.println("studentMapper2讀取數(shù)據(jù): " + studentMapper2.getStudentByIdWithClassInfo(1));

}

執(zhí)行結(jié)果:

在這個(gè)實(shí)驗(yàn)中,我們引入了兩張新的表,一張class,一張classroom。class中保存了班級(jí)的id和班級(jí)名,classroom中保存了班級(jí)id和學(xué)生id。我們?cè)赟tudentMapper中增加了一個(gè)查詢方法getStudentByIdWithClassInfo,用于查詢學(xué)生所在的班級(jí),涉及到多表查詢。在ClassMapper中添加了updateClassName,根據(jù)班級(jí)id更新班級(jí)名的操作。

當(dāng)sqlsession1的studentmapper查詢數(shù)據(jù)后,二級(jí)緩存生效。保存在StudentMapper的namespace下的cache中。當(dāng)sqlSession3的classMapper的updateClassName方法對(duì)class表進(jìn)行更新時(shí),updateClassName不屬于StudentMapper的namespace,所以StudentMapper下的cache沒有感應(yīng)到變化,沒有刷新緩存。當(dāng)StudentMapper中同樣的查詢?cè)俅伟l(fā)起時(shí),從緩存中讀取了臟數(shù)據(jù)。

實(shí)驗(yàn)5

為了解決實(shí)驗(yàn)4的問題呢,可以使用Cache ref,讓ClassMapper引用StudenMapper命名空間,這樣兩個(gè)映射文件對(duì)應(yīng)的Sql操作都使用的是同一塊緩存了。

執(zhí)行結(jié)果:

不過這樣做的后果是,緩存的粒度變粗了,多個(gè)Mapper namespace下的所有操作都會(huì)對(duì)緩存使用造成影響,其實(shí)這個(gè)緩存存在的意義已經(jīng)不大了。

二級(jí)緩存源碼分析

Mybatis二級(jí)緩存的工作流程和前文提到的一級(jí)緩存類似,只是在一級(jí)緩存處理前,用CachingExecutor裝飾了BaseExecutor的子類,實(shí)現(xiàn)了緩存的查詢和寫入功能,所以二級(jí)緩存直接從源碼開始分析。

源碼分析

源碼分析從CachingExecutor的query方法展開,源代碼走讀過程中涉及到的知識(shí)點(diǎn)較多,不能一一詳細(xì)講解,可以在文后留言,我會(huì)在交流環(huán)節(jié)更詳細(xì)的表示出來。

CachingExecutor的query方法,首先會(huì)從MappedStatement中獲得在配置初始化時(shí)賦予的cache。

Cache cache = ms.getCache();

本質(zhì)上是裝飾器模式的使用,具體的執(zhí)行鏈?zhǔn)?/p>

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

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

SynchronizedCache: 同步Cache,實(shí)現(xiàn)比較簡單,直接使用synchronized修飾方法。

LoggingCache: 日志功能,裝飾類,用于記錄緩存的命中率,如果開啟了DEBUG模式,則會(huì)輸出命中率日志。

SerializedCache: 序列化功能,將值序列化后存到緩存中。該功能用于緩存返回一份實(shí)例的Copy,用于保存線程安全。

LruCache: 采用了Lru算法的Cache實(shí)現(xiàn),移除最近最少使用的key/value。

PerpetualCache: 作為為最基礎(chǔ)的緩存類,底層實(shí)現(xiàn)比較簡單,直接使用了HashMap。

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

flushCacheIfRequired(ms);

在默認(rèn)的設(shè)置中SELECT語句不會(huì)刷新緩存,insert/update/delte會(huì)刷新緩存。進(jìn)入該方法。代碼如下所示。

private void flushCacheIfRequired(MappedStatement ms) {

Cache cache = ms.getCache();

if (cache != null && ms.isFlushCacheRequired()) {

tcm.clear(cache);

}

}

Mybatis的CachingExecutor持有了TransactionalCacheManager,即上述代碼中的tcm。

TransactionalCacheManager中持有了一個(gè)Map,代碼如下所示。

private Map transactionalCaches = new HashMap();

這個(gè)Map保存了Cache和用TransactionalCache包裝后的Cache的映射關(guān)系。

TransactionalCache實(shí)現(xiàn)了Cache接口,CachingExecutor會(huì)默認(rèn)使用他包裝初始生成的Cache,作用是如果事務(wù)提交,對(duì)緩存的操作才會(huì)生效,如果事務(wù)回滾或者不提交事務(wù),則不對(duì)緩存產(chǎn)生影響。

在TransactionalCache的clear,有以下兩句。清空了需要在提交時(shí)加入緩存的列表,同時(shí)設(shè)定提交時(shí)清空緩存,代碼如下所示。

@Override

public void clear() {

clearOnCommit = true;

entriesToAddOnCommit.clear();

}

CachingExecutor繼續(xù)往下走,ensureNoOutParams主要是用來處理存儲(chǔ)過程的,暫時(shí)不用考慮。

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

ensureNoOutParams(ms, parameterObject, boundSql);

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

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

在getObject方法中,會(huì)把獲取值的職責(zé)一路向后傳,最終到PerpetualCache。如果沒有查到,會(huì)把key加入Miss集合,這個(gè)主要是為了統(tǒng)計(jì)命中率。

Object object = delegate.getObject(key);

if (object == null) {

entriesMissedInCache.add(key);

}

CachingExecutor繼續(xù)往下走,如果查詢到數(shù)據(jù),則調(diào)用tcm.putObject方法,往緩存中放入值。

f (list == null) {

list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);

tcm.putObject(cache, key, list); // issue #578 and #116

}

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

@Override

public void putObject(Object key, Object object) {

entriesToAddOnCommit.put(key, object);

}

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

@Override

public void commit(boolean force) {

try {

executor.commit(isCommitOrRollbackRequired(force));

因?yàn)槲覀兪褂昧薈achingExecutor,首先會(huì)進(jìn)入CachingExecutor實(shí)現(xiàn)的commit方法。

@Override

public void commit(boolean required) throws SQLException {

delegate.commit(required);

tcm.commit();

}

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

public void commit() {

if (clearOnCommit) {

delegate.clear();

}

flushPendingEntries();

reset();

}

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

private void flushPendingEntries() {

for (Map.Entry entry : entriesToAddOnCommit.entrySet()) {

delegate.putObject(entry.getKey(), entry.getValue());

}

................

}

在flushPendingEntries中,就把待提交的Map循環(huán)后,委托給包裝的Cache類,進(jìn)行putObject的操作。 后續(xù)的查詢操作會(huì)重復(fù)執(zhí)行這套流程。如果是insert|update|delete的話,會(huì)統(tǒng)一進(jìn)入CachingExecutor的update方法,其中調(diào)用了這個(gè)函數(shù),代碼如下所示,因此不再贅述。

private void flushCacheIfRequired(MappedStatement ms)

總結(jié)

Mybatis的二級(jí)緩存相對(duì)于一級(jí)緩存來說,實(shí)現(xiàn)了SqlSession之間緩存數(shù)據(jù)的共享,同時(shí)粒度更加的細(xì),能夠到Mapper級(jí)別,通過Cache接口實(shí)現(xiàn)類不同的組合,對(duì)Cache的可控性也更強(qiáng)。

Mybatis在多表查詢時(shí),極大可能會(huì)出現(xiàn)臟數(shù)據(jù),有設(shè)計(jì)上的缺陷,安全使用的條件比較苛刻。

在分布式環(huán)境下,由于默認(rèn)的Mybatis Cache實(shí)現(xiàn)都是基于本地的,分布式環(huán)境下必然會(huì)出現(xiàn)讀取到臟數(shù)據(jù),需要使用集中式緩存將Mybatis的Cache接口實(shí)現(xiàn),有一定的開發(fā)成本,不如直接用Redis,Memcache實(shí)現(xiàn)業(yè)務(wù)上的緩存就好了。

全文總結(jié)

本文介紹了Mybatis的基礎(chǔ)概念,Mybatis一二級(jí)緩存的使用及源碼分析,并對(duì)于一二級(jí)緩存進(jìn)行了一定程度上的總結(jié)。 最終的結(jié)論是Mybatis的緩存機(jī)制設(shè)計(jì)的不是很完善,在使用上容易引起臟數(shù)據(jù)問題,個(gè)人建議不要使用Mybatis緩存,在業(yè)務(wù)層面上使用其他機(jī)制實(shí)現(xiàn)需要的緩存功能,讓Mybatis老老實(shí)實(shí)做它的ORM框架就好了哈哈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容