一文搞懂MyBatis一級緩存和二級緩存
前言
在本篇文章中,將結合示例與源碼,對MyBatis中的一級緩存和二級緩存進行說明。
MyBatis版本:3.5.6
正文
一. 一級緩存機制展示
在MyBatis中如果多次執(zhí)行完全相同的SQL語句時,MyBatis提供了一級緩存機制用于提高查詢效率。一級緩存是默認開啟的,如果想要手動配置,需要在MyBatis配置文件中加入如下配置。
<settings>
<setting name="localCacheScope" value="SESSION"/>
</settings>其中localCacheScope可以配置為SESSION(默認) 或者STATEMENT,含義如下所示。
| 屬性值 | 含義 |
|---|---|
| SESSION | 一級緩存在一個會話中生效。即在一個會話中的所有查詢語句,均會共享同一份一級緩存,不同會話中的一級緩存不共享。 |
| STATEMENT | 一級緩存僅針對當前執(zhí)行的SQL語句生效。當前執(zhí)行的SQL語句執(zhí)行完畢后,對應的一級緩存會被清空。 |
下面以一個例子對MyBatis的一級緩存機制進行演示和說明。首先開啟日志打印,然后關閉二級緩存,并將一級緩存作用范圍設置為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ù)據(jù)庫進行了交互,后面兩次查詢均是從一級緩存中查詢的數(shù)據(jù)?,F(xiàn)在往映射接口和映射文件中加入更改數(shù)據(jù)的邏輯,如下所示。
public interface BookMapper {
Book selectBookById(int id);
// 根據(jù)id更改圖書價格
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í)行一次更新操作并提交事務,最后再執(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í)行結果如下所示。

通過上述結果可以知道,在執(zhí)行更新操作之后,再執(zhí)行查詢操作時,是直接從數(shù)據(jù)庫查詢的數(shù)據(jù),并未使用一級緩存,即在一個會話中,對數(shù)據(jù)庫的增,刪,改操作,均會使一級緩存失效。
現(xiàn)在在執(zhí)行代碼中創(chuàng)建兩個會話,先讓會話1執(zhí)行一次查詢操作,然后讓會話2執(zhí)行一次更新操作并提交事務,最后讓會話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í)行結果如下所示。

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

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

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

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

三. 二級緩存機制展示
MyBatis的一級緩存僅在一個會話中被共享,會話之間的一級緩存互不影響,而MyBatis的二級緩存可以被多個會話共享,本小節(jié)將結合例子,對MyBatis中的二級緩存的使用機制進行分析。要使用二級緩存,需要對MyBatis配置文件進行更改以開啟二級緩存,如下所示。
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
<setting name="cacheEnabled" value="true"/>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>上述配置文件中還將一級緩存的作用范圍設置為了STATEMENT,目的是為了在例子中屏蔽一級緩存對查詢結果的干擾。映射接口如下所示。
public interface BookMapper {
Book selectBookById(int id);
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">
<!-- 二級緩存相關設置 -->
<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>二級緩存相關設置的每一項的含義,會在本小節(jié)末尾進行說明。
1. 場景一
場景一:創(chuàng)建兩個會話,會話1以相同SQL語句連續(xù)執(zhí)行兩次查詢,會話2以相同SQL語句執(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í)行結果如下所示。

MyBatis中的二級緩存開啟時,每次查詢會先去二級緩存中命中查詢結果,未命中時才會使用一級緩存以及直接去查詢數(shù)據(jù)庫。上述結果截圖表明,場景一中,SQL語句相同時,無論是同一會話的連續(xù)兩次查詢還是另一會話的一次查詢,均是查詢的數(shù)據(jù)庫,仿佛二級緩存沒有生效,實際上,將查詢結果緩存到二級緩存中需要事務提交,場景一中并沒有事務提交,所以二級緩存中是沒有內(nèi)容的,最終導致三次查詢均是直接查詢的數(shù)據(jù)庫。此外,如果是增刪改操作,只要沒有事務提交,那么就不會影響二級緩存。
2. 場景二
場景二:創(chuàng)建兩個會話,會話1執(zhí)行一次查詢并提交事務,然后會話1以相同SQL語句再執(zhí)行一次查詢,接著會話2以相同SQL語句執(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í)行結果如下所示。

場景二中第一次查詢后提交了事務,此時將查詢結果緩存到了二級緩存,所以后續(xù)的查詢?nèi)吭诙壘彺嬷忻辛瞬樵兘Y果。
3. 場景三
場景三:創(chuàng)建兩個會話,會話1執(zhí)行一次查詢并提交事務,然后會話2執(zhí)行一次更新并提交事務,接著會話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(
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í)行結果如下所示。

場景三的執(zhí)行結果表明,執(zhí)行更新操作并且提交事務后,會清空二級緩存,執(zhí)行新增和刪除操作也是同理。
4. 場景四
場景四:創(chuàng)建兩個會話,創(chuàng)建兩張表,會話1首先執(zhí)行一次多表查詢并提交事務,然后會話2執(zhí)行一次更新操作以更新表2的數(shù)據(jù)并提交事務,接著會話1再執(zhí)行一次相同的多表查詢。創(chuàng)表語句如下所示。
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>進行完上述更改之后,進行場景四的測試,執(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(
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í)行結果如下所示。

會話1第一次執(zhí)行多表查詢并提交事務時,將查詢結果緩存到了二級緩存中,然后會話2對bookstore表執(zhí)行了更新操作并提交了事務,但是最后會話1第二次執(zhí)行相同的多表查詢時,卻從二級緩存中命中了查詢結果,最終導致查詢出來了臟數(shù)據(jù)。
實際上,二級緩存的作用范圍是同一命名空間下的多個會話共享,這里的命名空間就是映射文件的namespace,可以理解為每一個映射文件持有一份二級緩存,所有會話在這個映射文件中的所有操作,都會共享這個二級緩存。所以場景四的例子中,會話2對bookstore表執(zhí)行更新操作并提交事務時,清空的是BookStoreMapper.xml持有的二級緩存,BookMapper.xml持有的二級緩存沒有感知到bookstore表的數(shù)據(jù)發(fā)生了變化,最終導致會話1第二次執(zhí)行相同的多表查詢時從二級緩存中命中了臟數(shù)據(jù)。
5. 場景五
場景五:執(zhí)行的操作和場景四一致,但是在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-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));
// 將事務隔離級別設置為讀已提交
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í)行結果如下所示。

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

Cache的實際創(chuàng)建是在MapperBuilderAssistant的useNewCache() 方法中,實現(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() 方法中會先創(chuàng)建CacheBuilder,然后調用CacheBuilder的build() 方法構建Cache。CacheBuilder類圖如下所示。

CacheBuilder的構造函數(shù)如下所示。
public CacheBuilder(String id) {
this.id = id;
this.decorators = new ArrayList<>();
}所以可以知道,CacheBuilder的id字段實際就是當前映射文件的namespace,其實到這里已經(jīng)大致可以猜到,CacheBuilder構建出來的二級緩存Cache在Configuration中的唯一標識就是映射文件的namespace。此外,CacheBuilder中的implementation是PerpetualCache的Class對象,decorators集合中包含有LruCache的Class對象。下面看一下CacheBuilder的build() 方法,如下所示。
public Cache build() {
setDefaultImplementations();
// 創(chuàng)建PerpetualCache,作為基礎Cache對象
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
if (PerpetualCache.class.equals(cache.getClass())) {
// 為基礎Cache對象添加緩存淘汰策略相關的裝飾器
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() 方法首先會創(chuàng)建PerpetualCache對象,作為基礎緩存對象,然后還會為基礎緩存對象根據(jù)緩存淘汰策略添加對應的裝飾器,比如<cache>標簽中eviction屬性值為LRU,那么對應的裝飾器為LruCache,根據(jù)eviction屬性值的不同,對應的裝飾器就不同,下圖是MyBatis為緩存淘汰策略提供的所有裝飾器。

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

整個裝飾鏈如下圖所示。

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

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

TransactionalCacheManager中持有一個Map,該Map的鍵為Cache,值為TransactionalCache,即一個二級緩存對應一個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);
}通過上述代碼可以知道,一個二級緩存對應一個TransactionalCache,且TransactionalCache中持有這個二級緩存的引用,當調用TransactionalCacheManager的getObject() 方法時,TransactionalCacheManager會將調用請求轉發(fā)給TransactionalCache,下面分析一下TransactionalCache,類圖如下所示。

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

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

