一文搞懂MyBatis一級(jí)緩存和二級(jí)緩存
前言
在本篇文章中,將結(jié)合示例與源碼,對(duì)MyBatis中的一級(jí)緩存和二級(jí)緩存進(jìn)行說明。
MyBatis版本:3.5.6
正文
一. 一級(jí)緩存機(jī)制展示
在MyBatis中如果多次執(zhí)行完全相同的SQL語(yǔ)句時(shí),MyBatis提供了一級(jí)緩存機(jī)制用于提高查詢效率。一級(jí)緩存是默認(rèn)開啟的,如果想要手動(dòng)配置,需要在MyBatis配置文件中加入如下配置。
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>其中localCacheScope可以配置為SESSION(默認(rèn)) 或者STATEMENT,含義如下所示。
| 屬性值 | 含義 |
|---|---|
| SESSION | 一級(jí)緩存在一個(gè)會(huì)話中生效。即在一個(gè)會(huì)話中的所有查詢語(yǔ)句,均會(huì)共享同一份一級(jí)緩存,不同會(huì)話中的一級(jí)緩存不共享。 |
| STATEMENT | 一級(jí)緩存僅針對(duì)當(dāng)前執(zhí)行的SQL語(yǔ)句生效。當(dāng)前執(zhí)行的SQL語(yǔ)句執(zhí)行完畢后,對(duì)應(yīng)的一級(jí)緩存會(huì)被清空。 |
下面以一個(gè)例子對(duì)MyBatis的一級(jí)緩存機(jī)制進(jìn)行演示和說明。首先開啟日志打印,然后關(guān)閉二級(jí)緩存,并將一級(jí)緩存作用范圍設(shè)置為SESSION,配置如下。
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
<setting name="cacheEnabled" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
</settings>映射接口如下所示。
public interface BookMapper {
Book selectBookById(int id);
}映射文件如下所示。
<?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="com.mybatis.learn.dao.BookMapper">
<resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
<result property="bookName" column="b_name"/>
<result property="bookPrice" column="b_price"/>
</resultMap>
<select id="selectBookById" resultMap="bookResultMap">
SELECT
b.id,
b.b_name,
b.b_price
FROM book b
WHERE b.id=#{id}
</select>
</mapper>MyBatis的執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
SqlSession sqlSession = sqlSessionFactory.openSession(false);
BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
System.out.println(bookMapper.selectBookById(1));
System.out.println(bookMapper.selectBookById(1));
System.out.println(bookMapper.selectBookById(1));
}
}在執(zhí)行代碼中,連續(xù)執(zhí)行了三次查詢操作,看一下日志打印,如下所示。

可以知道,只有第一次查詢時(shí)和數(shù)據(jù)庫(kù)進(jìn)行了交互,后面兩次查詢均是從一級(jí)緩存中查詢的數(shù)據(jù)?,F(xiàn)在往映射接口和映射文件中加入更改數(shù)據(jù)的邏輯,如下所示。
public interface BookMapper {
Book selectBookById(int id);
// 根據(jù)id更改圖書價(jià)格
void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);
}<?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="com.mybatis.learn.dao.BookMapper">
<resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
<result property="bookName" column="b_name"/>
<result property="bookPrice" column="b_price"/>
</resultMap>
<select id="selectBookById" resultMap="bookResultMap">
SELECT
b.id,
b.b_name,
b.b_price
FROM book b
WHERE b.id=#{id}
</select>
<update id="updateBookPriceById">
UPDATE book SET b_price=#{bookPrice}
WHERE id=#{id}
</update>
</mapper>執(zhí)行的操作為先執(zhí)行一次查詢操作,然后執(zhí)行一次更新操作并提交事務(wù),最后再執(zhí)行一次查詢操作,執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
SqlSession sqlSession = sqlSessionFactory.openSession(false);
BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
System.out.println(bookMapper.selectBookById(1));
System.out.println("Change database.");
bookMapper.updateBookPriceById(1, 22.5f);
sqlSession.commit();
System.out.println(bookMapper.selectBookById(1));
}
}執(zhí)行結(jié)果如下所示。

通過上述結(jié)果可以知道,在執(zhí)行更新操作之后,再執(zhí)行查詢操作時(shí),是直接從數(shù)據(jù)庫(kù)查詢的數(shù)據(jù),并未使用一級(jí)緩存,即在一個(gè)會(huì)話中,對(duì)數(shù)據(jù)庫(kù)的增,刪,改操作,均會(huì)使一級(jí)緩存失效。
現(xiàn)在在執(zhí)行代碼中創(chuàng)建兩個(gè)會(huì)話,先讓會(huì)話1執(zhí)行一次查詢操作,然后讓會(huì)話2執(zhí)行一次更新操作并提交事務(wù),最后讓會(huì)話1再執(zhí)行一次相同的查詢。執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
System.out.println(bookMapper1.selectBookById(1));
System.out.println("Change database.");
bookMapper2.updateBookPriceById(1, 22.5f);
sqlSession2.commit();
System.out.println(bookMapper1.selectBookById(1));
}
}執(zhí)行結(jié)果如下所示。

上述結(jié)果表明,會(huì)話1的第一次查詢是直接查詢的數(shù)據(jù)庫(kù),然后會(huì)話2執(zhí)行了一次更新操作并提交了事務(wù),此時(shí)數(shù)據(jù)庫(kù)中id為1的圖書的價(jià)格已經(jīng)變更為了22.5,緊接著會(huì)話1又做了一次查詢,但查詢結(jié)果中的圖書價(jià)格為20.5,說明會(huì)話1的第二次查詢是從緩存獲取的查詢結(jié)果。所以在這里可以知道,MyBatis中每個(gè)會(huì)話均會(huì)維護(hù)一份一級(jí)緩存,不同會(huì)話之間的一級(jí)緩存各不影響。
在本小節(jié)最后,對(duì)MyBatis的一級(jí)緩存機(jī)制做一個(gè)總結(jié),如下所示。
- MyBatis的一級(jí)緩存默認(rèn)開啟,且默認(rèn)作用范圍為SESSION,即一級(jí)緩存在一個(gè)會(huì)話中生效,也可以通過配置將作用范圍設(shè)置為STATEMENT,讓一級(jí)緩存僅針對(duì)當(dāng)前執(zhí)行的SQL語(yǔ)句生效;
- 在同一個(gè)會(huì)話中,執(zhí)行增,刪,改操作會(huì)使本會(huì)話中的一級(jí)緩存失效;
- 不同會(huì)話持有不同的一級(jí)緩存,本會(huì)話內(nèi)的操作不會(huì)影響其它會(huì)話內(nèi)的一級(jí)緩存。
二. 一級(jí)緩存源碼分析
本小節(jié)將對(duì)一級(jí)緩存對(duì)應(yīng)的MyBatis源碼進(jìn)行討論。
已知,禁用二級(jí)緩存的情況下,執(zhí)行查詢操作時(shí),調(diào)用鏈如下所示。

在BaseExecutor中有兩個(gè)重載的query() 方法,下面先看第一個(gè)query() 方法的實(shí)現(xiàn),如下所示。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler) throws SQLException {
// 獲取Sql語(yǔ)句
BoundSql boundSql = ms.getBoundSql(parameter);
// 生成CacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 調(diào)用重載的query()方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}在上述query() 方法中,先會(huì)在MappedStatement中獲取SQL語(yǔ)句,然后生成CacheKey,這個(gè)CacheKey實(shí)際就是本會(huì)話一級(jí)緩存中緩存的唯一標(biāo)識(shí),CacheKey類圖如下所示。

CacheKey中的multiplier,hashcode,checksum,count和updateList字段用于判斷CacheKey之間是否相等,這些字段會(huì)在CacheKey的構(gòu)造函數(shù)中進(jìn)行初始化,如下所示。
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLIER;
this.count = 0;
this.updateList = new ArrayList<>();
}同時(shí)hashcode,checksum,count和updateList字段會(huì)在CacheKey的update() 方法中被更新,如下所示。
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);
}主要邏輯就是基于update() 方法的入?yún)⒂?jì)算并更新hashcode,checksum和count的值,然后再將入?yún)⑻砑拥?strong>updateList集合中。同時(shí),在CacheKey重寫的equals() 方法中,只有當(dāng)hashcode相等,checksum相等,count相等,以及updateList集合中的元素也全都相等時(shí),才算做兩個(gè)CacheKey是相等。
回到上述的BaseExecutor中的query() 方法,在其中會(huì)調(diào)用createCacheKey() 方法生成CacheKey,其部分源碼如下所示。
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject,
RowBounds rowBounds, BoundSql boundSql) {
// ......
// 創(chuàng)建CacheKey
CacheKey cacheKey = new CacheKey();
// 基于MappedStatement的id更新CacheKey
cacheKey.update(ms.getId());
// 基于RowBounds的offset更新CacheKey
cacheKey.update(rowBounds.getOffset());
// 基于RowBounds的limit更新CacheKey
cacheKey.update(rowBounds.getLimit());
// 基于Sql語(yǔ)句更新CacheKey
cacheKey.update(boundSql.getSql());
// ......
// 基于查詢參數(shù)更新CacheKey
cacheKey.update(value);
// ......
// 基于Environment的id更新CacheKey
cacheKey.update(configuration.getEnvironment().getId());
return cacheKey;
}所以可以得出結(jié)論,判斷CacheKey是否相等的依據(jù)就是MappedStatement id + RowBounds offset + RowBounds limit + SQL + Parameter + Environment id相等。
獲取到CacheKey后,會(huì)調(diào)用BaseExecutor中重載的query() 方法,如下所示。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// queryStack是BaseExecutor的成員變量
// queryStack主要用于遞歸調(diào)用query()方法時(shí)防止一級(jí)緩存被清空
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 先從一級(jí)緩存中根據(jù)CacheKey命中查詢結(jié)果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 處理存儲(chǔ)過程相關(guān)邏輯
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 未命中,則直接查數(shù)據(jù)庫(kù)
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (BaseExecutor.DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
deferredLoads.clear();
// 如果一級(jí)緩存作用范圍是STATEMENT時(shí),每次query()執(zhí)行完畢就需要清空一級(jí)緩存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
}
return list;
}上述query() 方法中,會(huì)先根據(jù)CacheKey去緩存中命中查詢結(jié)果,如果命中到查詢結(jié)果并且映射文件中CURD標(biāo)簽上的statementType為CALLABLE,則會(huì)先在handleLocallyCachedOutputParameters() 方法中處理存儲(chǔ)過程相關(guān)邏輯然后再將命中的查詢結(jié)果返回,如果未命中到查詢結(jié)果,則會(huì)直接查詢數(shù)據(jù)庫(kù)。
上述query() 方法中還使用到了BaseExecutor的queryStack字段,主要防止一級(jí)緩存作用范圍是STATEMENT并且還存在遞歸調(diào)用query() 方法時(shí),在遞歸尚未終止時(shí)就將一級(jí)緩存刪除,如果不存在遞歸調(diào)用,那么一級(jí)緩存作用范圍是STATEMENT時(shí),每次查詢結(jié)束后,都會(huì)清空緩存。
下面看一下BaseExecutor中的一級(jí)緩存localCache,其實(shí)際是PerpetualCache,類圖如下所示。

所以PerpetualCache的內(nèi)部主要是基于一個(gè)Map(實(shí)際為HashMap)用于數(shù)據(jù)存儲(chǔ)。
現(xiàn)在回到上面的BaseExecutor的query() 方法中,如果沒有在一級(jí)緩存中命中查詢結(jié)果,則會(huì)直接查詢數(shù)據(jù)庫(kù),queryFromDatabase() 方法如下所示。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 調(diào)用doQuery()進(jìn)行查詢操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 將查詢結(jié)果添加到一級(jí)緩存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
// 返回查詢結(jié)果
return list;
}queryFromDatabase() 方法中和一級(jí)緩存相關(guān)的邏輯就是在查詢完數(shù)據(jù)庫(kù)后,會(huì)將查詢結(jié)果以CacheKey作為唯一標(biāo)識(shí)緩存到一級(jí)緩存中。
MyBatis中如果是執(zhí)行增,改和刪操作,并且在禁用二級(jí)緩存的情況下,均會(huì)調(diào)用到BaseExecutor的update() 方法,如下所示。
@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.");
}
// 執(zhí)行操作前先清空緩存
clearLocalCache();
return doUpdate(ms, parameter);
}所以MyBatis中的一級(jí)緩存在執(zhí)行了增,改和刪操作后,會(huì)被清空即失效。
最后,一級(jí)緩存的使用流程可以用下圖進(jìn)行概括。

三. 二級(jí)緩存機(jī)制展示
MyBatis的一級(jí)緩存僅在一個(gè)會(huì)話中被共享,會(huì)話之間的一級(jí)緩存互不影響,而MyBatis的二級(jí)緩存可以被多個(gè)會(huì)話共享,本小節(jié)將結(jié)合例子,對(duì)MyBatis中的二級(jí)緩存的使用機(jī)制進(jìn)行分析。要使用二級(jí)緩存,需要對(duì)MyBatis配置文件進(jìn)行更改以開啟二級(jí)緩存,如下所示。
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
<setting name="cacheEnabled" value="true"/>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>上述配置文件中還將一級(jí)緩存的作用范圍設(shè)置為了STATEMENT,目的是為了在例子中屏蔽一級(jí)緩存對(duì)查詢結(jié)果的干擾。映射接口如下所示。
public interface BookMapper {
Book selectBookById(int id);
void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);
}要使用二級(jí)緩存,還需要在映射文件中加入二級(jí)緩存相關(guān)的設(shè)置,如下所示。
<?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="com.mybatis.learn.dao.BookMapper">
<!-- 二級(jí)緩存相關(guān)設(shè)置 -->
<cache eviction="LRU"
type="org.apache.ibatis.cache.impl.PerpetualCache"
flushInterval="600000"
size="1024"
readOnly="true"
blocking="false"/>
<resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
<result property="bookName" column="b_name"/>
<result property="bookPrice" column="b_price"/>
</resultMap>
<select id="selectBookById" resultMap="bookResultMap">
SELECT
b.id,
b.b_name,
b.b_price
FROM book b
WHERE b.id=#{id}
</select>
<update id="updateBookPriceById">
UPDATE book SET b_price=#{bookPrice}
WHERE id=#{id}
</update>
</mapper>二級(jí)緩存相關(guān)設(shè)置的每一項(xiàng)的含義,會(huì)在本小節(jié)末尾進(jìn)行說明。
1. 場(chǎng)景一
場(chǎng)景一:創(chuàng)建兩個(gè)會(huì)話,會(huì)話1以相同SQL語(yǔ)句連續(xù)執(zhí)行兩次查詢,會(huì)話2以相同SQL語(yǔ)句執(zhí)行一次查詢。執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
System.out.println(bookMapper1.selectBookById(1));
System.out.println(bookMapper1.selectBookById(1));
System.out.println(bookMapper2.selectBookById(1));
}
}執(zhí)行結(jié)果如下所示。

MyBatis中的二級(jí)緩存開啟時(shí),每次查詢會(huì)先去二級(jí)緩存中命中查詢結(jié)果,未命中時(shí)才會(huì)使用一級(jí)緩存以及直接去查詢數(shù)據(jù)庫(kù)。上述結(jié)果截圖表明,場(chǎng)景一中,SQL語(yǔ)句相同時(shí),無論是同一會(huì)話的連續(xù)兩次查詢還是另一會(huì)話的一次查詢,均是查詢的數(shù)據(jù)庫(kù),仿佛二級(jí)緩存沒有生效,實(shí)際上,將查詢結(jié)果緩存到二級(jí)緩存中需要事務(wù)提交,場(chǎng)景一中并沒有事務(wù)提交,所以二級(jí)緩存中是沒有內(nèi)容的,最終導(dǎo)致三次查詢均是直接查詢的數(shù)據(jù)庫(kù)。此外,如果是增刪改操作,只要沒有事務(wù)提交,那么就不會(huì)影響二級(jí)緩存。
2. 場(chǎng)景二
場(chǎng)景二:創(chuàng)建兩個(gè)會(huì)話,會(huì)話1執(zhí)行一次查詢并提交事務(wù),然后會(huì)話1以相同SQL語(yǔ)句再執(zhí)行一次查詢,接著會(huì)話2以相同SQL語(yǔ)句執(zhí)行一次查詢。執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
SqlSession sqlSession1 = sqlSessionFactory.openSession(false);
SqlSession sqlSession2 = sqlSessionFactory.openSession(false);
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
System.out.println(bookMapper1.selectBookById(1));
sqlSession1.commit();
System.out.println(bookMapper1.selectBookById(1));
System.out.println(bookMapper2.selectBookById(1));
}
}執(zhí)行結(jié)果如下所示。

場(chǎng)景二中第一次查詢后提交了事務(wù),此時(shí)將查詢結(jié)果緩存到了二級(jí)緩存,所以后續(xù)的查詢?nèi)吭诙?jí)緩存中命中了查詢結(jié)果。
3. 場(chǎng)景三
場(chǎng)景三:創(chuàng)建兩個(gè)會(huì)話,會(huì)話1執(zhí)行一次查詢并提交事務(wù),然后會(huì)話2執(zhí)行一次更新并提交事務(wù),接著會(huì)話1再執(zhí)行一次相同的查詢。執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
// 將事務(wù)隔離級(jí)別設(shè)置為讀已提交
SqlSession sqlSession1 = sqlSessionFactory.openSession(
TransactionIsolationLevel.READ_COMMITTED);
SqlSession sqlSession2 = sqlSessionFactory.openSession(
TransactionIsolationLevel.READ_COMMITTED);
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
BookMapper bookMapper2 = sqlSession2.getMapper(BookMapper.class);
System.out.println(bookMapper1.selectBookById(1));
sqlSession1.commit();
System.out.println("Change database.");
bookMapper2.updateBookPriceById(1, 20.5f);
sqlSession2.commit();
System.out.println(bookMapper1.selectBookById(1));
}
}執(zhí)行結(jié)果如下所示。

場(chǎng)景三的執(zhí)行結(jié)果表明,執(zhí)行更新操作并且提交事務(wù)后,會(huì)清空二級(jí)緩存,執(zhí)行新增和刪除操作也是同理。
4. 場(chǎng)景四
場(chǎng)景四:創(chuàng)建兩個(gè)會(huì)話,創(chuàng)建兩張表,會(huì)話1首先執(zhí)行一次多表查詢并提交事務(wù),然后會(huì)話2執(zhí)行一次更新操作以更新表2的數(shù)據(jù)并提交事務(wù),接著會(huì)話1再執(zhí)行一次相同的多表查詢。創(chuàng)表語(yǔ)句如下所示。
CREATE TABLE book(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
b_name VARCHAR(255) NOT NULL,
b_price FLOAT NOT NULL,
bs_id INT(11) NOT NULL,
FOREIGN KEY book(bs_id) REFERENCES bookstore(id)
);
CREATE TABLE bookstore(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
bs_name VARCHAR(255) NOT NULL
)往book表和bookstore表中添加如下數(shù)據(jù)。
INSERT INTO book (b_name, b_price, bs_id) VALUES ("Math", 20.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("English", 21.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("Water Margin", 30.5, 2);
INSERT INTO bookstore (bs_name) VALUES ("XinHua");
INSERT INTO bookstore (bs_name) VALUES ("SanYou")創(chuàng)建BookStore類,如下所示。
@Data
public class BookStore {
private String id;
private String bookStoreName;
}創(chuàng)建BookDetail類,如下所示。
@Data
public class BookDetail {
private long id;
private String bookName;
private float bookPrice;
private BookStore bookStore;
}BookMapper映射接口添加selectBookDetailById() 方法,如下所示。
public interface BookMapper {
Book selectBookById(int id);
void updateBookPriceById(@Param("id") int id, @Param("bookPrice") float bookPrice);
BookDetail selectBookDetailById(int id);
}BookMapper.xml映射文件如下所示。
<?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="com.mybatis.learn.dao.BookMapper">
<cache eviction="LRU"
type="org.apache.ibatis.cache.impl.PerpetualCache"
flushInterval="600000"
size="1024"
readOnly="true"
blocking="false"/>
<resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
<result property="bookName" column="b_name"/>
<result property="bookPrice" column="b_price"/>
</resultMap>
<resultMap id="bookDetailResultMap" type="com.mybatis.learn.entity.BookDetail">
<id property="id" column="id"/>
<result property="bookName" column="b_name"/>
<result property="bookPrice" column="b_price"/>
<association property="bookStore">
<id property="id" column="id"/>
<result property="bookStoreName" column="bs_name"/>
</association>
</resultMap>
<select id="selectBookById" resultMap="bookResultMap">
SELECT
b.id,
b.b_name,
b.b_price
FROM book b
WHERE b.id=#{id}
</select>
<update id="updateBookPriceById">
UPDATE book SET b_price=#{bookPrice}
WHERE id=#{id}
</update>
<select id="selectBookDetailById" resultMap="bookDetailResultMap">
SELECT
b.id,
b.b_name,
b.b_price,
bs.id,
bs.bs_name
FROM book b, bookstore bs
WHERE b.id=#{id}
AND b.bs_id = bs.id
</select>
</mapper>還需要添加BookStoreMapper映射接口,如下所示。
public interface BookStoreMapper {
void updateBookPriceById(@Param("id") int id, @Param("bookStoreName") String bookStoreName);
}還需要添加BookStoreMapper.xml映射文件,如下所示。
<?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="com.mybatis.learn.dao.BookStoreMapper">
<cache eviction="LRU"
type="org.apache.ibatis.cache.impl.PerpetualCache"
flushInterval="600000"
size="1024"
readOnly="true"
blocking="false"/>
<update id="updateBookPriceById">
UPDATE bookstore SET bs_name=#{bookStoreName}
WHERE id=#{id}
</update>
</mapper>進(jìn)行完上述更改之后,進(jìn)行場(chǎng)景四的測(cè)試,執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
// 將事務(wù)隔離級(jí)別設(shè)置為讀已提交
SqlSession sqlSession1 = sqlSessionFactory.openSession(
TransactionIsolationLevel.READ_COMMITTED);
SqlSession sqlSession2 = sqlSessionFactory.openSession(
TransactionIsolationLevel.READ_COMMITTED);
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class);
System.out.println(bookMapper1.selectBookDetailById(1));
sqlSession1.commit();
System.out.println("Change database.");
bookStoreMapper.updateBookStoreById(1, "ShuXiang");
sqlSession2.commit();
System.out.println(bookMapper1.selectBookDetailById(1));
}
}執(zhí)行結(jié)果如下所示。

會(huì)話1第一次執(zhí)行多表查詢并提交事務(wù)時(shí),將查詢結(jié)果緩存到了二級(jí)緩存中,然后會(huì)話2對(duì)bookstore表執(zhí)行了更新操作并提交了事務(wù),但是最后會(huì)話1第二次執(zhí)行相同的多表查詢時(shí),卻從二級(jí)緩存中命中了查詢結(jié)果,最終導(dǎo)致查詢出來了臟數(shù)據(jù)。
實(shí)際上,二級(jí)緩存的作用范圍是同一命名空間下的多個(gè)會(huì)話共享,這里的命名空間就是映射文件的namespace,可以理解為每一個(gè)映射文件持有一份二級(jí)緩存,所有會(huì)話在這個(gè)映射文件中的所有操作,都會(huì)共享這個(gè)二級(jí)緩存。所以場(chǎng)景四的例子中,會(huì)話2對(duì)bookstore表執(zhí)行更新操作并提交事務(wù)時(shí),清空的是BookStoreMapper.xml持有的二級(jí)緩存,BookMapper.xml持有的二級(jí)緩存沒有感知到bookstore表的數(shù)據(jù)發(fā)生了變化,最終導(dǎo)致會(huì)話1第二次執(zhí)行相同的多表查詢時(shí)從二級(jí)緩存中命中了臟數(shù)據(jù)。
5. 場(chǎng)景五
場(chǎng)景五:執(zhí)行的操作和場(chǎng)景四一致,但是在BookStoreMapper.xml文件中進(jìn)行如下更改。
<?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="com.mybatis.learn.dao.BookStoreMapper">
<cache-ref namespace="com.mybatis.learn.dao.BookMapper"/>
<update id="updateBookStoreById">
UPDATE bookstore SET bs_name=#{bookStoreName}
WHERE id=#{id}
</update>
</mapper>執(zhí)行代碼如下所示。
public class MybatisTest {
public static void main(String[] args) throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
// 將事務(wù)隔離級(jí)別設(shè)置為讀已提交
SqlSession sqlSession1 = sqlSessionFactory.openSession(
TransactionIsolationLevel.READ_COMMITTED);
SqlSession sqlSession2 = sqlSessionFactory.openSession(
TransactionIsolationLevel.READ_COMMITTED);
BookMapper bookMapper1 = sqlSession1.getMapper(BookMapper.class);
BookStoreMapper bookStoreMapper = sqlSession2.getMapper(BookStoreMapper.class);
System.out.println(bookMapper1.selectBookDetailById(1));
sqlSession1.commit();
System.out.println("Change database.");
bookStoreMapper.updateBookStoreById(1, "ShuXiang");
sqlSession2.commit();
System.out.println(bookMapper1.selectBookDetailById(1));
}
}執(zhí)行結(jié)果如下所示。

在BookStoreMapper.xml中使用<cache-ref>標(biāo)簽引用了命名空間為com.mybatis.learn.dao.BookMapper的映射文件使用的二級(jí)緩存,因此相當(dāng)于BookMapper.xml映射文件與BookStoreMapper.xml映射文件持有同一份二級(jí)緩存,會(huì)話2在BookStoreMapper.xml映射文件中執(zhí)行更新操作并提交事務(wù)后,會(huì)導(dǎo)致二級(jí)緩存被清空,從而會(huì)話1第二次執(zhí)行相同的多表查詢時(shí)會(huì)從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)。
現(xiàn)在對(duì)MyBatis的二級(jí)緩存機(jī)制進(jìn)行一個(gè)總結(jié),如下所示。
- MyBatis中的二級(jí)緩存默認(rèn)開啟,可以在MyBatis配置文件中的<settings>中添加<setting name="cacheEnabled" value="false"/>將二級(jí)緩存關(guān)閉;
- MyBatis中的二級(jí)緩存作用范圍是同一命名空間下的多個(gè)會(huì)話共享,這里的命名空間就是映射文件的namespace,即不同會(huì)話使用同一映射文件中的SQL語(yǔ)句對(duì)數(shù)據(jù)庫(kù)執(zhí)行操作并提交事務(wù)后,均會(huì)影響這個(gè)映射文件持有的二級(jí)緩存;
- MyBatis中執(zhí)行查詢操作后,需要提交事務(wù)才能將查詢結(jié)果緩存到二級(jí)緩存中;
- MyBatis中執(zhí)行增,刪或改操作并提交事務(wù)后,會(huì)清空對(duì)應(yīng)的二級(jí)緩存;
- MyBatis中需要在映射文件中添加<cache>標(biāo)簽來為映射文件配置二級(jí)緩存,也可以在映射文件中添加<cache-ref>標(biāo)簽來引用其它映射文件的二級(jí)緩存以達(dá)到多個(gè)映射文件持有同一份二級(jí)緩存的效果。
最后,對(duì)<cache>標(biāo)簽和<cache-ref>標(biāo)簽進(jìn)行說明。
<cache>標(biāo)簽如下所示。
| 屬性 | 含義 | 默認(rèn)值 |
|---|---|---|
| eviction | 緩存淘汰策略。LRU表示最近使用頻次最少的優(yōu)先被淘汰;FIFO表示先被緩存的會(huì)先被淘汰;SOFT表示基于軟引用規(guī)則來淘汰;WEAK表示基于弱引用規(guī)則來淘汰 | LRU |
| flushInterval | 緩存刷新間隔。單位毫秒 | 空,表示永不過期 |
| type | 緩存的類型 | PerpetualCache |
| size | 最多緩存的對(duì)象個(gè)數(shù) | 1024 |
| blocking | 緩存未命中時(shí)是否阻塞 | false |
| readOnly | 緩存中的對(duì)象是否只讀。配置為true時(shí),表示緩存對(duì)象只讀,命中緩存時(shí)會(huì)直接將緩存的對(duì)象返回,性能更快,但是線程不安全;配置為false時(shí),表示緩存對(duì)象可讀寫,命中緩存時(shí)會(huì)將緩存的對(duì)象克隆然后返回克隆的對(duì)象,性能更慢,但是線程安全 | false |
<cache-ref>標(biāo)簽如下所示。
| 屬性 | 含義 |
|---|---|
| namespace | 其它映射文件的命名空間,設(shè)置之后則當(dāng)前映射文件將和其它映射文件將持有同一份二級(jí)緩存 |
四. 二級(jí)緩存的創(chuàng)建
在詳解MyBatis加載映射文件和動(dòng)態(tài)代理中已經(jīng)知道,XMLMapperBuilder的configurationElement() 方法會(huì)解析映射文件的內(nèi)容并豐富到Configuration中,但在詳解MyBatis加載映射文件和動(dòng)態(tài)代理中并未對(duì)解析映射文件的<cache>標(biāo)簽和<cache-ref>標(biāo)簽進(jìn)行說明,因此本小節(jié)將對(duì)這部分內(nèi)容進(jìn)行補(bǔ)充。
configurationElement() 方法如下所示。
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 解析<cache-ref>標(biāo)簽
cacheRefElement(context.evalNode("cache-ref"));
// 解析<cache>標(biāo)簽
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '"
+ resource + "'. Cause: " + e, e);
}
}在configurationElement() 方法中會(huì)先解析<cache-ref>標(biāo)簽,然后再解析<cache>標(biāo)簽,因此在這里先進(jìn)行一個(gè)推測(cè):如果映射文件中同時(shí)存在<cache-ref>和<cache>標(biāo)簽,那么<cache>標(biāo)簽配置的二級(jí)緩存會(huì)覆蓋<cache-ref>引用的二級(jí)緩存。
下面先分析<cache>標(biāo)簽的解析,cacheElement() 方法如下所示。
private void cacheElement(XNode context) {
if (context != null) {
// 獲取<cache>標(biāo)簽的type屬性值
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 獲取<cache>標(biāo)簽的eviction屬性值
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 獲取<cache>標(biāo)簽的flushInterval屬性值
Long flushInterval = context.getLongAttribute("flushInterval");
// 獲取<cache>標(biāo)簽的size屬性值
Integer size = context.getIntAttribute("size");
// 獲取<cache>標(biāo)簽的readOnly屬性值并取反
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// 獲取<cache>標(biāo)簽的blocking屬性值
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}單步跟蹤cacheElement() 方法,每個(gè)屬性解析出來的內(nèi)容可以參照下圖。

Cache的實(shí)際創(chuàng)建是在MapperBuilderAssistant的useNewCache() 方法中,實(shí)現(xiàn)如下所示。
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
configuration.addCache(cache);
currentCache = cache;
return cache;
}在MapperBuilderAssistant的useNewCache() 方法中會(huì)先創(chuàng)建CacheBuilder,然后調(diào)用CacheBuilder的build() 方法構(gòu)建Cache。CacheBuilder類圖如下所示。

CacheBuilder的構(gòu)造函數(shù)如下所示。
public CacheBuilder(String id) {
this.id = id;
this.decorators = new ArrayList<>();
}所以可以知道,CacheBuilder的id字段實(shí)際就是當(dāng)前映射文件的namespace,其實(shí)到這里已經(jīng)大致可以猜到,CacheBuilder構(gòu)建出來的二級(jí)緩存Cache在Configuration中的唯一標(biāo)識(shí)就是映射文件的namespace。此外,CacheBuilder中的implementation是PerpetualCache的Class對(duì)象,decorators集合中包含有LruCache的Class對(duì)象。下面看一下CacheBuilder的build() 方法,如下所示。
public Cache build() {
setDefaultImplementations();
// 創(chuàng)建PerpetualCache,作為基礎(chǔ)Cache對(duì)象
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) {
// 為基礎(chǔ)Cache對(duì)象添加緩存淘汰策略相關(guān)的裝飾器
for (Class<? extends Cache> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 繼續(xù)添加裝飾器
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}CacheBuilder的build() 方法首先會(huì)創(chuàng)建PerpetualCache對(duì)象,作為基礎(chǔ)緩存對(duì)象,然后還會(huì)為基礎(chǔ)緩存對(duì)象根據(jù)緩存淘汰策略添加對(duì)應(yīng)的裝飾器,比如<cache>標(biāo)簽中eviction屬性值為LRU,那么對(duì)應(yīng)的裝飾器為LruCache,根據(jù)eviction屬性值的不同,對(duì)應(yīng)的裝飾器就不同,下圖是MyBatis為緩存淘汰策略提供的所有裝飾器。

CacheBuilder的build() 方法中,為PerpetualCache添加完緩存淘汰策略添裝飾器后,還會(huì)繼續(xù)添加標(biāo)準(zhǔn)裝飾器,MyBatis中定義的標(biāo)準(zhǔn)裝飾器有ScheduledCache,SerializedCache,LoggingCache,SynchronizedCache和BlockingCache,含義如下表所示。
| 裝飾器 | 含義 |
|---|---|
| ScheduledCache | 提供緩存定時(shí)刷新功能,<cache>標(biāo)簽設(shè)置了flushInterval屬性值時(shí)會(huì)添加該裝飾器 |
| SerializedCache | 提供緩存序列化功能,<cache>標(biāo)簽的readOnly屬性設(shè)置為false時(shí)會(huì)添加該裝飾器 |
| LoggingCache | 提供日志功能,默認(rèn)會(huì)添加該裝飾器 |
| SynchronizedCache | 提供同步功能,默認(rèn)會(huì)添加該裝飾器 |
| BlockingCache | 提供阻塞功能,<cache>標(biāo)簽的blocking屬性設(shè)置為true時(shí)會(huì)添加該裝飾器 |
如下是一個(gè)<cache>標(biāo)簽的示例。
<cache eviction="LRU"
type="org.apache.ibatis.cache.impl.PerpetualCache"
flushInterval="600000"
size="1024"
readOnly="false"
blocking="true"/>那么生成的二級(jí)緩存對(duì)象如下所示。

整個(gè)裝飾鏈如下圖所示。

現(xiàn)在回到MapperBuilderAssistant的useNewCache() 方法,構(gòu)建好二級(jí)緩存對(duì)象之后,會(huì)將其添加到Configuration中,Configuration的addCache() 方法如下所示。
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}這里就印證了前面的猜想,即二級(jí)緩存Cache在Configuration中的唯一標(biāo)識(shí)就是映射文件的namespace。
現(xiàn)在再分析一下XMLMapperBuilder中的configurationElement() 方法對(duì)<cache-ref>標(biāo)簽的解析。cacheRefElement() 方法如下所示。
private void cacheRefElement(XNode context) {
if (context != null) {
// 在Configuration的cacheRefMap中將當(dāng)前映射文件命名空間與引用的映射文件命名空間建立映射關(guān)系
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// CacheRefResolver會(huì)將引用的映射文件的二級(jí)緩存從Configuration中獲取出來并賦值給MapperBuilderAssistant的currentCache
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}cacheRefElement() 方法會(huì)首先在Configuration的cacheRefMap中將當(dāng)前映射文件命名空間與引用的映射文件命名空間建立映射關(guān)系,然后會(huì)通過CacheRefResolver將引用的映射文件的二級(jí)緩存從Configuration中獲取出來并賦值給MapperBuilderAssistant的currentCache,currentCache這個(gè)字段后續(xù)會(huì)在MapperBuilderAssistant構(gòu)建MappedStatement時(shí)傳遞給MappedStatement,以及如果映射文件中還存在<cache>標(biāo)簽,那么MapperBuilderAssistant會(huì)將<cache>標(biāo)簽配置的二級(jí)緩存重新賦值給currentCache以覆蓋<cache-ref>標(biāo)簽引用的二級(jí)緩存,所以映射文件中同時(shí)有<cache-ref>標(biāo)簽和<cache>標(biāo)簽時(shí),只有<cache>標(biāo)簽配置的二級(jí)緩存會(huì)生效。
五. 二級(jí)緩存的源碼分析
本小節(jié)將對(duì)二級(jí)緩存對(duì)應(yīng)的MyBatis源碼進(jìn)行討論。MyBatis中開啟二級(jí)緩存之后,執(zhí)行查詢操作時(shí),調(diào)用鏈如下所示。

在CachingExecutor中有兩個(gè)重載的query() 方法,下面先看第一個(gè)query() 方法,如下所示。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 獲取Sql語(yǔ)句
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 創(chuàng)建CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}繼續(xù)看重載的query() 方法,如下所示。
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 從MappedStatement中將二級(jí)緩存獲取出來
Cache cache = ms.getCache();
if (cache != null) {
// 清空二級(jí)緩存(如果需要的話)
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
// 處理存儲(chǔ)過程相關(guān)邏輯
ensureNoOutParams(ms, boundSql);
// 從二級(jí)緩存中根據(jù)CacheKey命中查詢結(jié)果
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 未命中緩存,則查數(shù)據(jù)庫(kù)
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// 將從數(shù)據(jù)庫(kù)查詢到的結(jié)果緩存到二級(jí)緩存中
tcm.putObject(cache, key, list);
}
// 返回查詢結(jié)果
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}上述query() 方法整體執(zhí)行流程比較簡(jiǎn)單,概括下來就是:
- 先從緩存中命中查詢結(jié)果;
- 命中到查詢結(jié)果則返回;
- 未命中到查詢結(jié)果則直接查詢數(shù)據(jù)庫(kù)并把查詢結(jié)果緩存到二級(jí)緩存中。
但是從二級(jí)緩存中根據(jù)CacheKey命中查詢結(jié)果時(shí),并沒有直接通過Cache的getObject() 方法,而是通過tcm的getObject() 方法,合理進(jìn)行推測(cè)的話,應(yīng)該就是tcm持有二級(jí)緩存的引用,當(dāng)需要從二級(jí)緩存中命中查詢結(jié)果時(shí),由tcm將請(qǐng)求轉(zhuǎn)發(fā)給二級(jí)緩存。
實(shí)際上,tcm為CachingExecutor持有的TransactionalCacheManager對(duì)象,從二級(jí)緩存中命中查詢結(jié)果這一請(qǐng)求之所以需要通過TransactionalCacheManager轉(zhuǎn)發(fā)給二級(jí)緩存,是因?yàn)樾枰柚?strong>TransactionalCacheManager實(shí)現(xiàn)只有當(dāng)事務(wù)提交時(shí),二級(jí)緩存才會(huì)被更新這一功能。聯(lián)想到第三小節(jié)中的場(chǎng)景一和場(chǎng)景二的示例,將查詢結(jié)果緩存到二級(jí)緩存中需要事務(wù)提交這一功能,其實(shí)就是借助TransactionalCacheManager實(shí)現(xiàn)的,所以下面對(duì)TransactionalCacheManager進(jìn)行一個(gè)說明。首先TransactionalCacheManager的類圖如下所示。

TransactionalCacheManager中持有一個(gè)Map,該Map的鍵為Cache,值為TransactionalCache,即一個(gè)二級(jí)緩存對(duì)應(yīng)一個(gè)TransactionalCache。繼續(xù)看TransactionalCacheManager的getObject() 方法,如下所示。
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
private TransactionalCache getTransactionalCache(Cache cache) {
return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
}通過上述代碼可以知道,一個(gè)二級(jí)緩存對(duì)應(yīng)一個(gè)TransactionalCache,且TransactionalCache中持有這個(gè)二級(jí)緩存的引用,當(dāng)調(diào)用TransactionalCacheManager的getObject() 方法時(shí),TransactionalCacheManager會(huì)將調(diào)用請(qǐng)求轉(zhuǎn)發(fā)給TransactionalCache,下面分析一下TransactionalCache,類圖如下所示。

繼續(xù)看TransactionalCache的getObject() 方法,如下所示。
@Override
public Object getObject(Object key) {
// 在二級(jí)緩存中命中查詢結(jié)果
Object object = delegate.getObject(key);
if (object == null) {
// 未命中則將CacheKey添加到entriesMissedInCache中
// 用于統(tǒng)計(jì)命中率
entriesMissedInCache.add(key);
}
if (clearOnCommit) {
return null;
} else {
return object;
}
}到這里就可以知道了,在CachingExecutor中通過CacheKey命中查詢結(jié)果時(shí),步驟如下。
- CachingExecutor將請(qǐng)求發(fā)送給TransactionalCacheManager;
- TransactionalCacheManager將請(qǐng)求轉(zhuǎn)發(fā)給二級(jí)緩存對(duì)應(yīng)的TransactionalCache;
- 最后再由TransactionalCache將請(qǐng)求最終傳遞到二級(jí)緩存。
在上述getObject() 方法中,如果clearOnCommit為true,則無論是否在二級(jí)緩存中命中查詢結(jié)果,均返回null,那么clearOnCommit在什么地方會(huì)被置為true呢,其實(shí)就是在CachingExecutor的flushCacheIfRequired() 方法中,這個(gè)方法在上面分析的query() 方法中會(huì)被調(diào)用到,看一下flushCacheIfRequired() 的實(shí)現(xiàn),如下所示。
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}調(diào)用TransactionalCacheManager的clear() 方法時(shí),最終會(huì)調(diào)用到TransactionalCache的clear() 方法,如下所示。
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}現(xiàn)在繼續(xù)分析為什么將查詢結(jié)果緩存到二級(jí)緩存中需要事務(wù)提交。從數(shù)據(jù)庫(kù)中查詢出來結(jié)果后,CachingExecutor會(huì)調(diào)用TransactionalCacheManager的putObject() 方法試圖將查詢結(jié)果緩存到二級(jí)緩存中,我們已經(jīng)知道,如果事務(wù)不提交,那么查詢結(jié)果是無法被緩存到二級(jí)緩存中,那么在事務(wù)提交之前,查詢結(jié)果肯定被暫存到了某個(gè)地方,為了搞清楚這部分邏輯,先看一下TransactionalCacheManager的putObject() 方法,如下所示。
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}繼續(xù)看TransactionalCache的putObject() 方法,如下所示。
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}到這里就搞明白了,在事務(wù)提交之前,查詢結(jié)果會(huì)被暫存到TransactionalCache的entriesToAddOnCommit中。
下面繼續(xù)分析事務(wù)提交時(shí)如何將entriesToAddOnCommit中暫存的查詢結(jié)果刷新到二級(jí)緩存中,DefaultSqlSession的commit() 方法如下所示。
@Override
public void commit() {
commit(false);
}
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException(
"Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}在DefaultSqlSession的commit() 方法中會(huì)調(diào)用到CachingExecutor的commit() 方法,如下所示。
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
// 調(diào)用TransactionalCacheManager的commit()方法
tcm.commit();
}在CachingExecutor的commit() 方法中,會(huì)調(diào)用TransactionalCacheManager的commit() 方法,如下所示。
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
// 調(diào)用TransactionalCache的commit()方法
txCache.commit();
}
}繼續(xù)看TransactionalCache的commit() 方法,如下所示。
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
// 將entriesToAddOnCommit中暫存的查詢結(jié)果全部緩存到二級(jí)緩存中
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}至此可以知道,當(dāng)調(diào)用SqlSession的commit() 方法時(shí),會(huì)一路傳遞到TransactionalCache的commit() 方法,最終調(diào)用TransactionalCache的flushPendingEntries() 方法將暫存的查詢結(jié)果全部刷到二級(jí)緩存中。
當(dāng)執(zhí)行增,刪,改操作并提交事務(wù)時(shí),二級(jí)緩存會(huì)被清空,這是因?yàn)?strong>增,刪,改操作最終會(huì)調(diào)用到CachingExecutor的update() 方法,而update() 方法中又會(huì)調(diào)用flushCacheIfRequired() 方法,已經(jīng)知道在flushCacheIfRequired() 方法中如果所執(zhí)行的方法對(duì)應(yīng)的MappedStatement的flushCacheRequired字段為true的話,則會(huì)最終將TransactionalCache中的clearOnCommit字段置為true,隨即在事務(wù)提交的時(shí)候,會(huì)將二級(jí)緩存清空。而加載映射文件時(shí),解析CURD標(biāo)簽為MappedStatement時(shí)有如下一行代碼。
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);即如果沒有在CURD標(biāo)簽中顯式的設(shè)置flushCache屬性,則會(huì)給flushCache字段一個(gè)默認(rèn)值,且默認(rèn)值為非查詢標(biāo)簽下默認(rèn)為true,所以到這里就可以知道,如果是增,刪,改操作,那么TransactionalCache中的clearOnCommit字段會(huì)被置為true,從而在提交事務(wù)時(shí)會(huì)在TransactionalCache的commit() 方法中將二級(jí)緩存清空。
到這里,二級(jí)緩存的源碼分析結(jié)束。二級(jí)緩存的使用流程可以用下圖進(jìn)行概括,如下所示。

總結(jié)
關(guān)于MyBatis的一級(jí)緩存,總結(jié)如下。
- MyBatis的一級(jí)緩存默認(rèn)開啟,且默認(rèn)作用范圍為SESSION,即一級(jí)緩存在一個(gè)會(huì)話中生效,也可以通過配置將作用范圍設(shè)置為STATEMENT,讓一級(jí)緩存僅針對(duì)當(dāng)前執(zhí)行的SQL語(yǔ)句生效;
- 在同一個(gè)會(huì)話中,執(zhí)行增,刪,改操作會(huì)使本會(huì)話中的一級(jí)緩存失效;
- 不同會(huì)話持有不同的一級(jí)緩存,本會(huì)話內(nèi)的操作不會(huì)影響其它會(huì)話內(nèi)的一級(jí)緩存。
關(guān)于MyBatis的二級(jí)緩存,總結(jié)如下。
- MyBatis中的二級(jí)緩存默認(rèn)開啟,可以在MyBatis配置文件中的<settings>中添加<setting name="cacheEnabled" value="false"/>將二級(jí)緩存關(guān)閉;
- MyBatis中的二級(jí)緩存作用范圍是同一命名空間下的多個(gè)會(huì)話共享,這里的命名空間就是映射文件的namespace,即不同會(huì)話使用同一映射文件中的SQL語(yǔ)句對(duì)數(shù)據(jù)庫(kù)執(zhí)行操作并提交事務(wù)后,均會(huì)影響這個(gè)映射文件持有的二級(jí)緩存;
- MyBatis中執(zhí)行查詢操作后,需要提交事務(wù)才能將查詢結(jié)果緩存到二級(jí)緩存中;
- MyBatis中執(zhí)行增,刪或改操作并提交事務(wù)后,會(huì)清空對(duì)應(yīng)的二級(jí)緩存;
- MyBatis中需要在映射文件中添加<cache>標(biāo)簽來為映射文件配置二級(jí)緩存,也可以在映射文件中添加<cache-ref>標(biāo)簽來引用其它映射文件的二級(jí)緩存以達(dá)到多個(gè)映射文件持有同一份二級(jí)緩存的效果。
到此這篇關(guān)于一文搞懂MyBatis一級(jí)緩存和二級(jí)緩存的文章就介紹到這了,更多相關(guān)MyBatis一級(jí)緩存和二級(jí)緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中的服務(wù)發(fā)現(xiàn)與負(fù)載均衡及Eureka與Ribbon的應(yīng)用小結(jié)
這篇文章主要介紹了Java中的服務(wù)發(fā)現(xiàn)與負(fù)載均衡:Eureka與Ribbon的應(yīng)用,通過使用Eureka和Ribbon,我們可以在Java項(xiàng)目中實(shí)現(xiàn)高效的服務(wù)發(fā)現(xiàn)和負(fù)載均衡,需要的朋友可以參考下2024-08-08
spring boot + jpa + kotlin入門實(shí)例詳解
這篇文章主要介紹了spring boot + jpa + kotlin入門實(shí)例詳解 ,需要的朋友可以參考下2017-07-07
java使用selenium自動(dòng)化WebDriver等待的示例代碼
顯式等待和隱式等待是WebDriver中兩種常用的等待方式,它們都可以用來等待特定的條件滿足后再繼續(xù)執(zhí)行代碼,本文給大家介紹java使用selenium自動(dòng)化WebDriver等待,感興趣的朋友一起看看吧2023-09-09
SpringBoot實(shí)現(xiàn)項(xiàng)目文件上傳的方法詳解
這篇文章主要為大家詳細(xì)介紹了SpringBoot中實(shí)現(xiàn)項(xiàng)目文件上傳的相關(guān)資料,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解一下2022-11-11
java?Export大量數(shù)據(jù)導(dǎo)出和打包
這篇文章主要為大家介紹了java?Export大量數(shù)據(jù)的導(dǎo)出和打包實(shí)現(xiàn)過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
Java 異步回調(diào)機(jī)制實(shí)例分析
這篇文章主要介紹了Java 異步回調(diào)機(jī)制實(shí)例解析的相關(guān)資料,需要的朋友可以參考下2017-02-02
fasterxml jackson反序列化時(shí)對(duì)于非靜態(tài)內(nèi)部類報(bào)錯(cuò)問題及解決
這篇文章主要介紹了fasterxml jackson反序列化時(shí)對(duì)于非靜態(tài)內(nèi)部類報(bào)錯(cuò)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08

