MyBatis緩存的使用以源碼分析
機(jī)緣巧合看到2018美團(tuán)技術(shù)年貨中介紹了一些MyBatis關(guān)于緩存的文章,正好上篇文章對MyBatis的使用以及源碼進(jìn)行了詳盡的分析,對于緩存的設(shè)計以及使用一筆帶過;本文將對MyBatis的緩存進(jìn)行補(bǔ)充說明。
一級緩存
一級緩存介紹
在應(yīng)用運(yùn)行的過程中,一次數(shù)據(jù)庫連接會話可能會多次執(zhí)行相同的SQL;而MyBatis針對此場景做了優(yōu)化,優(yōu)化的方案便是一級緩存;當(dāng)查詢相同的SQL,會優(yōu)先命中一級緩存,避免了對數(shù)據(jù)庫的多次訪問,提高了性能。執(zhí)行過程如圖所示:

(上圖來自:美團(tuán)技術(shù)年貨)
還記得上篇文章中介紹的SqlSession提供對數(shù)據(jù)庫的操作,而具體的職責(zé)是由Executor完成的,那么緩存的是在哪里完成的呢?

當(dāng)用戶執(zhí)行查詢時,MyBatis根據(jù)當(dāng)前語句生成的MapperStatement,在localCache(BaseExecutor的成員變量)中查詢,如果命中則直接返回,否則查詢數(shù)據(jù)庫,并將結(jié)果緩存,最后將數(shù)據(jù)返回給用戶。
一級緩存有兩種使用選項,SESSION 或者 STATEMENT,默認(rèn)的級別是SESSION;SESSION表示再一次數(shù)據(jù)庫會話中執(zhí)行的所有語句共享一個緩存;而STATEMENT則可以理解為緩存只對當(dāng)前語句有效。配置方式,在MyBatis配置文件中添加如下:
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>
一級緩存實(shí)驗(yàn)一
我們用以下代碼分別對SESSION,STATEMENT做演示
//演示代碼
@Test
public void test1() {
SqlSession sqlSession = sessionFactory.openSession(true);
CustomerDao customerDao;
try {
customerDao = sqlSession.getMapper(CustomerDao.class);
LOGGER.debug(customerDao.selectByPrimaryKey(1L));
LOGGER.debug(customerDao.selectByPrimaryKey(1L));
LOGGER.debug(customerDao.selectByPrimaryKey(1L));
} finally {
sqlSession.close();
}
}
SESSION級別演示效果
DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.
我們發(fā)現(xiàn)只有第一次查詢從數(shù)據(jù)庫取得結(jié)果,后面的都使用了一級緩存
STATEMENT級別演示效果
DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='ce.sun', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.
當(dāng)我們將一級緩存的選項調(diào)整為STATEMENT時,發(fā)現(xiàn)三次都是從數(shù)據(jù)庫取得結(jié)果。
一級緩存實(shí)驗(yàn)二
使用SESSION一級緩存,當(dāng)我們在一次會話中對改數(shù)據(jù)進(jìn)行修改,會對緩存進(jìn)行清除么?
//演示代碼
@Test
public void test2() {
SqlSession sqlSession = sessionFactory.openSession(true);
CustomerDao customerDao;
try {
customerDao = sqlSession.getMapper(CustomerDao.class);
Customer customer = customerDao.selectByPrimaryKey(1L);
LOGGER.debug(customer);
customer.setName("wang.er");
customerDao.updateByPrimaryKey(customer);
LOGGER.debug(customerDao.selectByPrimaryKey(1L));
} finally {
sqlSession.close();
}
}
實(shí)驗(yàn)結(jié)果如下:
DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='wang.er', phone='null'}
DEBUG [main] - ==> Preparing: update customer set optimistic = ?, name = ?, phone = ? where id = ?
DEBUG [main] - ==> Parameters: null, wang.er(String), null, 1(Long)
DEBUG [main] - <== Updates: 1
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
DEBUG [main] - Customer{id=1, optimistic=null, name='wang.er', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.
一級緩存失效,重新從數(shù)據(jù)庫獲取數(shù)據(jù)。
一級緩存實(shí)驗(yàn)三
驗(yàn)證一級緩存SESSION配置,是只在會話內(nèi)部共享;我們開啟兩個會話,一個會話查詢兩次,另一個會話在兩次查詢期間對查詢數(shù)據(jù)進(jìn)行修改,會有什么樣的結(jié)果呢?
@Test
public void test3() {
SqlSession sqlSession1 = sessionFactory.openSession(true);
SqlSession sqlSession2 = sessionFactory.openSession(true);
try {
CustomerDao customerDao1 = sqlSession1.getMapper(CustomerDao.class);
CustomerDao customerDao2 = sqlSession2.getMapper(CustomerDao.class);
LOGGER.info("會話一:" + customerDao1.selectByPrimaryKey(1L));
LOGGER.info("會話一:" + customerDao1.selectByPrimaryKey(1L));
Customer customer = new Customer();
customer.setId(1L);
customer.setName("wang.er");
customerDao2.updateByPrimaryKey(customer);
LOGGER.info("會話二:" + customerDao2.selectByPrimaryKey(1L));
LOGGER.info("會話一:" + customerDao1.selectByPrimaryKey(1L));
} finally {
sqlSession1.close();
sqlSession2.close();
}
}
演示結(jié)果
DEBUG [main] - Created connection 525968792.
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
INFO [main] - 會話一:Customer{id=1, optimistic=null, name='sun.ce', phone='null'}
INFO [main] - 會話一:Customer{id=1, optimistic=null, name='sun.ce', phone='null'}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1431467659.
DEBUG [main] - ==> Preparing: update customer set optimistic = ?, name = ?, phone = ? where id = ?
DEBUG [main] - ==> Parameters: null, wang.er(String), null, 1(Long)
DEBUG [main] - <== Updates: 1
DEBUG [main] - ==> Preparing: select id, optimistic, name, phone from customer where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
DEBUG [main] - <== Total: 1
INFO [main] - 會話二:Customer{id=1, optimistic=null, name='wang.er', phone='null'}
INFO [main] - 會話一:Customer{id=1, optimistic=null, name='sun.ce', phone='null'}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1f59a598]
DEBUG [main] - Returned connection 525968792 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5552768b]
DEBUG [main] - Returned connection 1431467659 to pool.
我們發(fā)現(xiàn),會話一的結(jié)果,沒有因?yàn)闀挾男薷亩淖?,還是使用了一級緩存的結(jié)果;發(fā)生了數(shù)據(jù)臟讀。
一級緩存源碼分析

從上圖中我們能夠發(fā)現(xiàn),緩存的重點(diǎn)關(guān)注對象是Executor類,我們再來一起回顧一下Executor的繼承關(guān)系圖。

我們先來看看Executor的抽象實(shí)現(xiàn)類BaseExecutor
//創(chuàng)建CacheKey
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLEx
ception {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
具體CacheKey生成規(guī)則如下:
//CacheKey生成規(guī)則
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,以及參數(shù)生成了CacheKey;我們緊接著看生成CacheKey之后的操作;
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
//如果緩存結(jié)果不為空則處理緩存結(jié)果
if (list != null) {
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//否則從數(shù)據(jù)庫查詢
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
我們看到進(jìn)入query(ms, parameter, rowBounds, resultHandler, key, boundSql)方法后,會先取緩存結(jié)果,不存在則從數(shù)據(jù)庫中查詢。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);
List list;
try {
list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
this.localCache.removeObject(key);
}
//將數(shù)據(jù)庫中查詢的結(jié)果緩存起來
this.localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
this.localOutputParameterCache.putObject(key, parameter);
}
return list;
}
驗(yàn)證完查詢的緩存流程,我們再來驗(yàn)證之前實(shí)驗(yàn)二中的更新清空緩存,代碼如下:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (this.closed) {
throw new ExecutorException("Executor was closed.");
} else {
//每次更新之前會清空緩存
this.clearLocalCache();
return this.doUpdate(ms, parameter);
}
}
我們發(fā)現(xiàn)每次更新之前,會先清空緩存,隨后在進(jìn)行更新操作。我們再來仔細(xì)看看Cache的具體實(shí)現(xiàn);它是Executor的一個成員變量
public abstract class BaseExecutor implements Executor {
....
protected PerpetualCache localCache;
}
PerpetualCache 具體代碼如下:
public class PerpetualCache implements Cache {
private Map<Object, Object> cache = new HashMap();
public int getSize() {
return this.cache.size();
}
//添加緩存對象
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
//獲取緩存對象
public Object getObject(Object key) {
return this.cache.get(key);
}
//移除緩存對象
public Object removeObject(Object key) {
return this.cache.remove(key);
}
//清空緩存
public void clear() {
this.cache.clear();
}
}
一級緩存總結(jié)
- 一級緩存的生命周期跟SqlSession一樣
- 一級緩存的實(shí)現(xiàn)比較簡單通過HashMap
- 一級緩存的最大范圍是SESSION,但是在分布式或者多個SqlSession的情況下,有可能會出現(xiàn)數(shù)據(jù)臟讀, 建議使用STATEMENT
二級緩存
二級緩存介紹
我們通過一級緩存了解到,一級緩存的最大范圍是SESSION內(nèi)部,那么在多個SqlSeesion中,如何實(shí)現(xiàn)緩存呢?這就需要二級緩存了,回想Executor有兩個子類,BaseExecutor跟CachingExecutor;二級緩存就是通過CachingExecutor實(shí)現(xiàn)的,如下是二級緩存的工作原理。

二級緩存開啟后,同一個namespace下的所有SqlSession共享一個Cache;此時Sql的查詢流程是
二級緩存 > 一級緩存 > 數(shù)據(jù)庫
通過在配置文件中,新增如下配置可以打開二級緩存
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
XML 文件中新增<Cache/>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xin.sunce.mybatis.dao.StudentDao">
<cache/>
<select id="getStudentById" parameterType="int" resultType="xin.sunce.mybatis.entity.Student">
SELECT id,name,age FROM student WHERE id = #{id}
</select>
</mapper>
二級緩存實(shí)驗(yàn)一
測試二級緩存效果,不提交事務(wù), sqlSession1 查詢完數(shù)據(jù)后, sqlSession2 相同的查詢是否會從緩存中獲取數(shù)據(jù)。
//演示代碼
@Test
public void test1() {
SqlSession sqlSession1 = sessionFactory.openSession(true);
SqlSession sqlSession2 = sessionFactory.openSession(true);
try {
StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
LOGGER.info(studentDao1.getStudentById(1));
LOGGER.info(studentDao2.getStudentById(1));
} finally {
sqlSession1.close();
sqlSession2.close();
}
}
實(shí)驗(yàn)結(jié)果:
DEBUG [main] - Created connection 1552999801.
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - xin.sunce.mybatis.entity.Student@152aa092
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1324578393.
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - xin.sunce.mybatis.entity.Student@37858383
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5c90e579]
DEBUG [main] - Returned connection 1552999801 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4ef37659]
DEBUG [main] - Returned connection 1324578393 to pool.
我們發(fā)現(xiàn)沒有命中緩存,會話二重新從數(shù)據(jù)庫中讀取
二級緩存實(shí)驗(yàn)二
測試二級緩存效果,提交事務(wù), sqlSession1 查詢完數(shù)據(jù)后, sqlSession2 相同的查詢是否會從緩存中獲取數(shù)據(jù)。
//演示代碼
@Test
public void test2() {
SqlSession sqlSession1 = sessionFactory.openSession(true);
SqlSession sqlSession2 = sessionFactory.openSession(true);
try {
StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
LOGGER.info(studentDao1.getStudentById(1));
sqlSession1.commit();
LOGGER.info(studentDao2.getStudentById(1));
} finally {
sqlSession1.close();
sqlSession2.close();
}
}
實(shí)驗(yàn)結(jié)果
DEBUG [main] - Created connection 1552999801.
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - xin.sunce.mybatis.entity.Student@152aa092
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
INFO [main] - xin.sunce.mybatis.entity.Student@62e7f11d
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5c90e579]
DEBUG [main] - Returned connection 1552999801 to pool.
會話一事務(wù)提交以后,會話二查詢結(jié)果命中緩存
二級緩存實(shí)驗(yàn)三
其他會話更新,會不會清空緩存
//演示代碼:
@Test
public void test3() {
SqlSession sqlSession1 = sessionFactory.openSession(true);
SqlSession sqlSession2 = sessionFactory.openSession(true);
SqlSession sqlSession3 = sessionFactory.openSession(true);
try {
StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
StudentDao studentDao3 = sqlSession3.getMapper(StudentDao.class);
LOGGER.info("會話一:" + studentDao1.getStudentById(1));
sqlSession1.commit();
LOGGER.info("會話二,studentDao3更新之前:" + studentDao2.getStudentById(1));
studentDao3.updateStudentName("測測", 1);
sqlSession3.commit();
LOGGER.info("會話二,studentDao3更新之后:" + studentDao2.getStudentById(1));
} finally {
sqlSession1.close();
sqlSession2.close();
sqlSession3.close();
}
}
演示結(jié)果:
DEBUG [main] - Created connection 1479909053.
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - 會話一:Student{id=1, name='試試', age=16}
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
INFO [main] - 會話二,studentDao3更新之前:Student{id=1, name='試試', age=16}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1061448687.
DEBUG [main] - ==> Preparing: UPDATE student SET name = ? WHERE id = ?
DEBUG [main] - ==> Parameters: 測測(String), 1(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.3333333333333333
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1533330615.
DEBUG [main] - ==> Preparing: SELECT id,name,age FROM student WHERE id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - 會話二,studentDao3更新之后:Student{id=1, name='測測', age=16}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@58359ebd]
DEBUG [main] - Returned connection 1479909053 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5b64c4b7]
DEBUG [main] - Returned connection 1533330615 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3f446bef]
DEBUG [main] - Returned connection 1061448687 to pool.
sqlSession3事務(wù)提交之后,sqlSession2沒有走Cache,而是通過數(shù)據(jù)庫查詢到結(jié)果。
二級緩存實(shí)驗(yàn)四
驗(yàn)證緩存范圍namespace,我們通常會為一個表建立一個namespace;一個namespace無法感知到另一個namespace的數(shù)據(jù)變化;關(guān)聯(lián)查詢學(xué)生所在的班級名稱,單獨(dú)修改班級名稱,緩存結(jié)果會怎樣呢?
//演示代碼
@Test
public void test4() {
SqlSession sqlSession1 = sessionFactory.openSession(true);
SqlSession sqlSession2 = sessionFactory.openSession(true);
SqlSession sqlSession3 = sessionFactory.openSession(true);
try {
StudentDao studentDao1 = sqlSession1.getMapper(StudentDao.class);
StudentDao studentDao2 = sqlSession2.getMapper(StudentDao.class);
ClassDao classDao = sqlSession3.getMapper(ClassDao.class);
LOGGER.info("會話一:" + studentDao1.getStudentByIdWithClassInfo(1));
sqlSession1.commit();
LOGGER.info("會話二,classDao更新之前:" + studentDao2.getStudentByIdWithClassInfo(1));
classDao.updateClassName("測試一班", 1);
sqlSession3.commit();
LOGGER.info("會話二,classDao更新之后:" + studentDao2.getStudentByIdWithClassInfo(1));
} finally {
sqlSession1.close();
sqlSession2.close();
sqlSession3.close();
}
}
演示結(jié)果:
DEBUG [main] - Created connection 758119607.
DEBUG [main] - ==> Preparing: SELECT s.id,s.name,s.age,class.name as className FROM classroom c JOIN student s ON c.student_id = s.id JOIN class ON c.class_id = class.id WHERE s.id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - 會話一:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
INFO [main] - 會話二,classDao更新之前:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1987169128.
DEBUG [main] - ==> Preparing: UPDATE class SET name = ? WHERE id = ?
DEBUG [main] - ==> Parameters: 測試一班(String), 1(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.6666666666666666
INFO [main] - 會話二,classDao更新之后:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2d2ffcb7]
DEBUG [main] - Returned connection 758119607 to pool.
DEBUG [main] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7671cb68]
DEBUG [main] - Returned connection 1987169128 to pool.
我們發(fā)現(xiàn)班級名稱已經(jīng)被修改為測試一班,然而會話二的結(jié)果仍為一班;驗(yàn)證了二級緩存的范圍為namespace
二級緩存實(shí)驗(yàn)五
我們注釋掉ClassDao.xml中的<cache/> ,開啟<cache-ref namespace="xin.sunce.mybatis.dao.StudentDao"/>,將class表與student表置于同一namesapce,再看看結(jié)果
測試結(jié)果:
DEBUG [main] - Created connection 758119607.
DEBUG [main] - ==> Preparing: SELECT s.id,s.name,s.age,class.name as className FROM classroom c JOIN student s ON c.student_id = s.id JOIN class ON c.class_id = class.id WHERE s.id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - 會話一:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.5
INFO [main] - 會話二,classDao更新之前:{name=測測, className=一班, id=1, age=16}
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1944978632.
DEBUG [main] - ==> Preparing: UPDATE class SET name = ? WHERE id = ?
DEBUG [main] - ==> Parameters: 特殊一班(String), 1(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - Cache Hit Ratio [xin.sunce.mybatis.dao.StudentDao]: 0.3333333333333333
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1804379080.
DEBUG [main] - ==> Preparing: SELECT s.id,s.name,s.age,class.name as className FROM classroom c JOIN student s ON c.student_id = s.id JOIN class ON c.class_id = class.id WHERE s.id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
INFO [main] - 會話二,classDao更新之后:{name=測測, className=特殊一班, id=1, age=16}
發(fā)現(xiàn)結(jié)果沒有走緩存,兩個表共享一個namespace;不過這樣做的后果是,緩存的粒度變粗了,多個 Mapper namespace 下的所有操作都會對緩存使用造成影響。
二級緩存源碼分析

如果讀過上篇文章的朋友可能記得,從Configuration獲取Executor時:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? this.defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Object 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);
}
// 當(dāng)配置XML文件中cacheEnabled=true時,返回的的執(zhí)行器則為CachingExecutor
if (this.cacheEnabled) {
executor = new CachingExecutor((Executor)executor);
}
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
return executor;
}
開啟緩存以后返回的Executor為CachingExecutor;而CachingExecutor本質(zhì)即為裝飾器,是對BaseExecutor一次包裝,從構(gòu)造函數(shù)public CachingExecutor(Executor delegate)即可看出;那么跟BaseExecutor一樣,我們還是從query方法展開源碼的閱讀。
public class CachingExecutor implements Executor {
//事務(wù)緩存管理器
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
private final Executor delegate;
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) {
//判斷是否要清除緩存
this.flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
this.ensureNoOutParams(ms, boundSql);
List<E> list = (List)this.tcm.getObject(cache, key);
if (list == null) {
list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
this.tcm.putObject(cache, key, list);
}
return list;
}
}
return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
....
//在默認(rèn)的設(shè)置中SELECT語句不會刷新緩存,insert/update/delte 會刷新緩存。進(jìn)入該方法。
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
this.tcm.clear(cache);
}
}
CachingExecutor對緩存的管理都是由TransactionalCacheManager完成的,我們來看看TransactionalCacheManager的具體代碼
public void clear(Cache cache) {
this.getTransactionalCache(cache).clear();
}
public Object getObject(Cache cache, CacheKey key) {
return this.getTransactionalCache(cache).getObject(key);
}
public void putObject(Cache cache, CacheKey key, Object value) {
this.getTransactionalCache(cache).putObject(key, value);
}
Cache 究竟是如何工作的呢?我們可以通過DEBUG,來看看

TransactionalCacheManager管理的是一個Cache的裝飾鏈,裝飾鏈的執(zhí)行過程是SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache ->
PerpetualCache 最終一直到PerpetualCache;以上就是處理緩存的流程。
有的人可能會好奇<cache/> <cache-ref/> 如何處理的?回顧上篇文章,SqlSessionFactoryBuilder 在構(gòu)建Factory可以選擇讀取XML文件的方式,順著這個思路去找,你應(yīng)該可以找到你想要的答案。
二級緩存總結(jié)
- MyBatis的二級緩存相對于一級緩存來說,實(shí)現(xiàn)了 SqlSession 之間緩存數(shù)據(jù)的共享,同時粒度更加的細(xì),能夠到namespace 級別,通過Cache接口實(shí)現(xiàn)類不同的組合,對Cache的可控性也更強(qiáng)。
- MyBatis在多表查詢時,極大可能會出現(xiàn)臟數(shù)據(jù),有設(shè)計上的缺陷,安全使用二級緩存的條件比較苛刻。
- 在分布式環(huán)境下,由于默認(rèn)的MyBatis Cache實(shí)現(xiàn)都是基于本地的,分布式環(huán)境下必然會出現(xiàn)讀取到臟數(shù)據(jù),需要使用集中式緩存將MyBatis的Cache接口實(shí)現(xiàn),有一定的開發(fā)成本,直接使用Redis、Memcached等分布式緩存可能成本更低,安全性也更高。