一文搞懂MyBatis一級(jí)緩存和二級(jí)緩存
前言
在本篇文章中,將結(jié)合示例與源碼,對(duì)MyBatis
中的一級(jí)緩存
和二級(jí)緩存
進(jìn)行說明。
MyBatis版本:3.5.6
正文
一. 一級(jí)緩存機(jī)制展示
在MyBatis中如果多次執(zhí)行完全相同的SQL語句時(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ì)話中的所有查詢語句,均會(huì)共享同一份一級(jí)緩存,不同會(huì)話中的一級(jí)緩存不共享。 |
STATEMENT | 一級(jí)緩存僅針對(duì)當(dāng)前執(zhí)行的SQL語句生效。當(dāng)前執(zhí)行的SQL語句執(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語句生效;
- 在同一個(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語句 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語句,然后生成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語句更新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語句連續(xù)執(zhí)行兩次查詢,會(huì)話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í)行結(jié)果如下所示。
MyBatis中的二級(jí)緩存開啟時(shí),每次查詢會(huì)先去二級(jí)緩存中命中查詢結(jié)果,未命中時(shí)才會(huì)使用一級(jí)緩存以及直接去查詢數(shù)據(jù)庫(kù)。上述結(jié)果截圖表明,場(chǎng)景一中,SQL語句相同時(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語句再執(zhí)行一次查詢,接著會(huì)話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í)行結(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)表語句如下所示。
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語句對(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語句 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語句生效;
- 在同一個(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語句對(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-08spring boot + jpa + kotlin入門實(shí)例詳解
這篇文章主要介紹了spring boot + jpa + kotlin入門實(shí)例詳解 ,需要的朋友可以參考下2017-07-07java使用selenium自動(dòng)化WebDriver等待的示例代碼
顯式等待和隱式等待是WebDriver中兩種常用的等待方式,它們都可以用來等待特定的條件滿足后再繼續(xù)執(zhí)行代碼,本文給大家介紹java使用selenium自動(dòng)化WebDriver等待,感興趣的朋友一起看看吧2023-09-09SpringBoot實(shí)現(xiàn)項(xiàng)目文件上傳的方法詳解
這篇文章主要為大家詳細(xì)介紹了SpringBoot中實(shí)現(xiàn)項(xiàng)目文件上傳的相關(guān)資料,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,感興趣的小伙伴可以了解一下2022-11-11java?Export大量數(shù)據(jù)導(dǎo)出和打包
這篇文章主要為大家介紹了java?Export大量數(shù)據(jù)的導(dǎo)出和打包實(shí)現(xiàn)過程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06Java 異步回調(diào)機(jī)制實(shí)例分析
這篇文章主要介紹了Java 異步回調(diào)機(jī)制實(shí)例解析的相關(guān)資料,需要的朋友可以參考下2017-02-02fasterxml 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