Mybatis的緩存——一級(jí)緩存和源碼分析
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”
優(yōu)質(zhì)文章,第一時(shí)間送達(dá)
? 作者?|??isdxh
來(lái)源 |? urlify.cn/Ynq6ry
66套java從入門到精通實(shí)戰(zhàn)課程分享
什么是緩存?
緩存就是存在內(nèi)存中的數(shù)據(jù),而內(nèi)存讀取都是非常快的 ,通常我們會(huì)把更新變動(dòng)不太頻繁且查詢頻繁的數(shù)據(jù),在第一次從數(shù)據(jù)庫(kù)查詢出后,存放在緩存中,這樣就可以避免之后多次的與數(shù)據(jù)庫(kù)進(jìn)行交互,從而提升響應(yīng)速度。
mybatis 也提供了對(duì)緩存的支持,分為:
一級(jí)緩存
二級(jí)緩存

一級(jí)緩存:
每個(gè)sqlSeesion對(duì)象都有一個(gè)一級(jí)緩存,我們?cè)诓僮鲾?shù)據(jù)庫(kù)時(shí)需要構(gòu)造sqlSeesion對(duì)象,在對(duì)象中有一個(gè)HashMap用于存儲(chǔ)緩存數(shù)據(jù)。不同的sqlSession之間的緩存數(shù)據(jù)區(qū)域(HashMap)是互不影響的。二級(jí)緩存:
二級(jí)緩存是mapper級(jí)別(或稱為namespace級(jí)別)的緩存,多個(gè)sqlSession去操作同一個(gè)Mapper的sql語(yǔ)句,多個(gè)SqlSession可以共用二級(jí)緩存,二級(jí)緩存是跨sqlSession的。
一級(jí)緩存
首先我們來(lái)開一級(jí)緩存,一級(jí)緩存是默認(rèn)開啟的,所以我們可以很方便來(lái)體驗(yàn)一下一級(jí)緩存。
測(cè)試一、
準(zhǔn)備一張表,有兩個(gè)字段id和username
在測(cè)試類中:
public?class?TestCache?{
????private?SqlSession?sqlSession;
????private?UserMapper?mapper;
????@Before
????public?void?before()?throws?IOException?{
????????InputStream?resourceAsStream?=?Resources.getResourceAsStream("sqlMapConfig.xml");
????????SqlSessionFactory?build?=?new?SqlSessionFactoryBuilder().build(resourceAsStream);
????????sqlSession?=?build.openSession();
????????mapper?=?sqlSession.getMapper(UserMapper.class);
????}
????@Test
????public?void?testFirst(){
????????//第一次查詢————首先去一級(jí)緩存中查詢
????????User?user1?=?mapper.findById(1);
????????System.out.println("======"+user1);
??//第二次查詢
????????User?user2?=?mapper.findById(1);
????????System.out.println("======"+user2);
????????
????????System.out.println(user1==user2);
????}
}
我們用同一個(gè)sqlSession分別根據(jù)id來(lái)查詢用戶,id都為1,之后再比較它們的地址值。來(lái)看一下結(jié)果:
23:16:25,818?DEBUG?findById:159?-?==>??Preparing:?select?*?from?user?where?id=??
23:16:25,862?DEBUG?findById:159?-?==>?Parameters:?1(Integer)
23:16:25,894?DEBUG?findById:159?-?<==??????Total:?1
======User{id=1,?username='lucy'}
======User{id=1,?username='lucy'}
true
我們發(fā)現(xiàn)只打印了一條SQL,同時(shí)它們的地址值一致。
說(shuō)明第一次查詢,緩存中沒(méi)有,然后從數(shù)據(jù)庫(kù)中查詢——執(zhí)行SQL,然后存入緩存,第二次查詢時(shí)發(fā)現(xiàn)緩存中有了,所以直接從緩存中取出,不再執(zhí)行SQL了。
我們剛才提到,一級(jí)緩存的數(shù)據(jù)結(jié)構(gòu)是一個(gè)hashmap,也就是說(shuō)有key有value。
value就是我們查詢出的結(jié)果,key是由多個(gè)值組成的:
statementid?:namespace.id組成
params:查詢時(shí)傳入的參數(shù)
boundsql:mybatis底層的對(duì)象,它封裝著我們要執(zhí)行的sql
rowbounds:分頁(yè)對(duì)象
...還有一些會(huì)在源碼分析中道明
測(cè)試二、
我們現(xiàn)在修改一下,我們?cè)诓樵兊谝淮谓Y(jié)果后,修改一下數(shù)據(jù)庫(kù)的值,然后再進(jìn)行第二次查詢,我們來(lái)看一下查詢結(jié)果。id=1 的username為lucy
@Test
????public?void?testFirst(){
????????//第一次查詢
????????User?user1?=?mapper.findById(1);
????????System.out.println("======"+user1);
????????//修改id為1的username
????????User?updateUser?=?new?User();
????????updateUser.setId(1);
????????updateUser.setUsername("李思");
????????mapper.updateUser(updateUser);
????????//手動(dòng)提交事務(wù)
????????sqlSession.commit();
????????//第二次查詢
????????User?user2?=?mapper.findById(1);
????????System.out.println("======"+user2);
????????System.out.println(user1==user2);
????}

在提交事務(wù)的地方打一個(gè)斷點(diǎn),可以看到執(zhí)行了兩條sql,一個(gè)是查詢id為1,一個(gè)是修改id為1的username
最終結(jié)果:
23:50:15,933?DEBUG?findById:159?-?==>??Preparing:?select?*?from?user?where?id=??
23:50:15,976?DEBUG?findById:159?-?==>?Parameters:?1(Integer)
23:50:16,002?DEBUG?findById:159?-?<==??????Total:?1
======User{id=1,?username='lucy',?roleList=null,?orderList=null}
23:50:16,003?DEBUG?updateUser:159?-?==>??Preparing:?update?user?set?username=??where?id?=??
23:50:16,005?DEBUG?updateUser:159?-?==>?Parameters:?李思(String),?1(Integer)
23:50:16,016?DEBUG?updateUser:159?-?<==????Updates:?1
23:53:18,316?DEBUG?JdbcTransaction:70?-?Committing?JDBC?Connection?[com.mysql.jdbc.JDBC4Connection@421e361]
23:53:22,306?DEBUG?findById:159?-?==>??Preparing:?select?*?from?user?where?id=??
23:53:22,306?DEBUG?findById:159?-?==>?Parameters:?1(Integer)
23:53:22,307?DEBUG?findById:159?-?<==??????Total:?1
======User{id=1,?username='李思',?roleList=null,?orderList=null}
我們看到,最終打印了3條sql,再進(jìn)行修改后的第二次查詢也打印了。
說(shuō)明在第二次查詢時(shí)在緩存中找不到所對(duì)應(yīng)的key了。在進(jìn)行修改操作時(shí),會(huì)刷新緩存
我們也可以通過(guò)sqlSession.clearCache();手動(dòng)刷新一級(jí)緩存
總結(jié):
一級(jí)緩存的數(shù)據(jù)結(jié)構(gòu)時(shí)HashMap
不同的SqlSession的一級(jí)緩存互不影響
一級(jí)緩存的key是由多個(gè)值組成的,value就是其查詢結(jié)果
增刪改操作會(huì)刷新一級(jí)緩存
通過(guò)
sqlSession.clearCache()手動(dòng)刷新一級(jí)緩存
一級(jí)緩存源碼分析:
我們?cè)诜治鲆患?jí)緩存之前帶著一些疑問(wèn)來(lái)讀代碼
一級(jí)緩存是什么?真的是上面說(shuō)的HashMap嗎?
一級(jí)緩存什么時(shí)候被創(chuàng)建?
一級(jí)緩存的工作流程是怎么樣的?
1. 一級(jí)緩存到底是什么?
之前說(shuō)不同的SqlSession的一級(jí)緩存互不影響,所以我從SqlSession這個(gè)類入手
可以看到,org.apache.ibatis.session.SqlSession中有一個(gè)和緩存有關(guān)的方法——clearCache()刷新緩存的方法,點(diǎn)進(jìn)去,找到它的實(shí)現(xiàn)類DefaultSqlSession
@Override
??public?void?clearCache()?{
????executor.clearLocalCache();
??}
再次點(diǎn)進(jìn)去executor.clearLocalCache(),再次點(diǎn)進(jìn)去并找到其實(shí)現(xiàn)類BaseExecutor,
@Override
??public?void?clearLocalCache()?{
????if?(!closed)?{
??????localCache.clear();
??????localOutputParameterCache.clear();
????}
??}
進(jìn)入localCache.clear()方法。進(jìn)入到了org.apache.ibatis.cache.impl.PerpetualCache類中
package?org.apache.ibatis.cache.impl;
import?java.util.HashMap;
import?java.util.Map;
import?java.util.concurrent.locks.ReadWriteLock;
import?org.apache.ibatis.cache.Cache;
import?org.apache.ibatis.cache.CacheException;
/**
?*?@author?Clinton?Begin
?*/
public?class?PerpetualCache?implements?Cache?{
??private?final?String?id;
??private?Map我們看到了PerpetualCache類中有一個(gè)屬性private Map,很明顯它是一個(gè)HashMap,我們所調(diào)用的.clear()方法,實(shí)際上就是調(diào)用的Map的clear方法
得出結(jié)論:
一級(jí)緩存的數(shù)據(jù)結(jié)構(gòu)確實(shí)是HashMap
2. 一級(jí)緩存什么時(shí)候被創(chuàng)建?
我們進(jìn)入到org.apache.ibatis.executor.Executor中
看到一個(gè)方法CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)?,見名思意是一個(gè)創(chuàng)建CacheKey的方法
找到它的實(shí)現(xiàn)類和方法org.apache.ibatis.executor.BaseExecuto.createCacheKey

我們分析一下創(chuàng)建CacheKey的這塊代碼:
public?CacheKey?createCacheKey(MappedStatement?ms,?Object?parameterObject,?RowBounds?rowBounds,?BoundSql?boundSql)?{
????if?(closed)?{
??????throw?new?ExecutorException("Executor?was?closed.");
????}
????//初始化CacheKey
????CacheKey?cacheKey?=?new?CacheKey();
????//存入statementId
????cacheKey.update(ms.getId());
????//分別存入分頁(yè)需要的Offset和Limit
????cacheKey.update(rowBounds.getOffset());
????cacheKey.update(rowBounds.getLimit());
????//把從BoundSql中封裝的sql取出并存入到cacheKey對(duì)象中
????cacheKey.update(boundSql.getSql());
????//下面這一塊就是封裝參數(shù)
????List?parameterMappings?=?boundSql.getParameterMappings();
????TypeHandlerRegistry?typeHandlerRegistry?=?ms.getConfiguration().getTypeHandlerRegistry();
????for?(ParameterMapping?parameterMapping?:?parameterMappings)?{
??????if?(parameterMapping.getMode()?!=?ParameterMode.OUT)?{
????????Object?value;
????????String?propertyName?=?parameterMapping.getProperty();
????????if?(boundSql.hasAdditionalParameter(propertyName))?{
??????????value?=?boundSql.getAdditionalParameter(propertyName);
????????}?else?if?(parameterObject?==?null)?{
??????????value?=?null;
????????}?else?if?(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass()))?{
??????????value?=?parameterObject;
????????}?else?{
??????????MetaObject?metaObject?=?configuration.newMetaObject(parameterObject);
??????????value?=?metaObject.getValue(propertyName);
????????}
????????cacheKey.update(value);
??????}
????}
????//從configuration對(duì)象中(也就是載入配置文件后存放的對(duì)象)把EnvironmentId存入
????????/**
?????*?????"development">
?????*?????????"development">?//就是這個(gè)id
?????*?????????????
?????*?????????????type="JDBC">
?????*?????????????
?????*?????????????type="POOLED">
?????*?????????????????"driver"?value="${jdbc.driver}"/>
?????*?????????????????"url"?value="${jdbc.url}"/>
?????*?????????????????"username"?value="${jdbc.username}"/>
?????*?????????????????"password"?value="${jdbc.password}"/>
?????*?????????????
?????*?????????
?????*?????
?????*/
????if?(configuration.getEnvironment()?!=?null)?{
??????//?issue?#176
??????cacheKey.update(configuration.getEnvironment().getId());
????}
????//返回
????return?cacheKey;
??}
我們?cè)冱c(diǎn)進(jìn)去cacheKey.update()方法看一看
/**
?*?@author?Clinton?Begin
?*/
public?class?CacheKey?implements?Cloneable,?Serializable?{
??private?static?final?long?serialVersionUID?=?1146682552656046210L;
??public?static?final?CacheKey?NULL_CACHE_KEY?=?new?NullCacheKey();
??private?static?final?int?DEFAULT_MULTIPLYER?=?37;
??private?static?final?int?DEFAULT_HASHCODE?=?17;
??private?final?int?multiplier;
??private?int?hashcode;
??private?long?checksum;
??private?int?count;
??//值存入的地方
??private?transient?List?updateList;
??//省略部分方法......
??//省略部分方法......
??public?void?update(Object?object)?{
????int?baseHashCode?=?object?==?null???1?:?ArrayUtil.hashCode(object);?
????count++;
????checksum?+=?baseHashCode;
????baseHashCode?*=?count;
????hashcode?=?multiplier?*?hashcode?+?baseHashCode;
????//看到把值傳入到了一個(gè)list中
????updateList.add(object);
??}
?
??//省略部分方法......
}
我們知道了那些數(shù)據(jù)是在CacheKey對(duì)象中如何存儲(chǔ)的了。下面我們返回createCacheKey()方法。
Ctrl+鼠標(biāo)左鍵 點(diǎn)擊方法名,查詢有哪些地方調(diào)用了此方法
我們進(jìn)入BaseExecutor,可以看到一個(gè)query()方法:
這里我們很清楚的看到,在執(zhí)行query()方法前,CacheKey方法被創(chuàng)建了
3. 一級(jí)緩存的執(zhí)行流程
我們可以看到,創(chuàng)建CacheKey后調(diào)用了query()方法,我們?cè)俅吸c(diǎn)進(jìn)去:

在執(zhí)行SQL前如何在一級(jí)緩存中找不到Key,那么將會(huì)執(zhí)行sql,我們來(lái)看一下執(zhí)行sql前后會(huì)做些什么,進(jìn)入
list?=?queryFromDatabase(ms,?parameter,?rowBounds,?resultHandler,?key,?boundSql);

分析一下:
private??List?queryFromDatabase(MappedStatement?ms,?Object?parameter,?RowBounds?rowBounds,?ResultHandler?resultHandler,?CacheKey?key,?BoundSql?boundSql)?throws?SQLException?{
????List?list;
????//1.?把key存入緩存,value放一個(gè)占位符
?localCache.putObject(key,?EXECUTION_PLACEHOLDER);
????try?{
??????//2.?與數(shù)據(jù)庫(kù)交互
??????list?=?doQuery(ms,?parameter,?rowBounds,?resultHandler,?boundSql);
????}?finally?{
??????//3.?如果第2步出了什么異常,把第1步存入的key刪除
??????localCache.removeObject(key);
????}
??????//4.?把結(jié)果存入緩存
????localCache.putObject(key,?list);
????if?(ms.getStatementType()?==?StatementType.CALLABLE)?{
??????localOutputParameterCache.putObject(key,?parameter);
????}
????return?list;
??}
至此,我們思路就非常的清晰了。
結(jié)論:
在執(zhí)行sql前,會(huì)首先根據(jù)CacheKey查詢緩存中有沒(méi)有,如果有,就處理緩存中的參數(shù),如果沒(méi)有,就執(zhí)行sql,執(zhí)行sql后把結(jié)果存入緩存。
一級(jí)緩存源碼分析結(jié)論:
一級(jí)緩存的數(shù)據(jù)結(jié)構(gòu)是一個(gè)
HashMap,它的value就是查詢結(jié)果,它的key是CacheKey,CacheKey中有一個(gè)list屬性,statementId,params,rowbounds,sql等參數(shù)都存入到了這個(gè)list中一級(jí)緩存在調(diào)用
query()方法前被創(chuàng)建。并傳入到query()方法中會(huì)首先根據(jù)
CacheKey查詢緩存中有沒(méi)有,如果有,就處理緩存中的參數(shù),如果沒(méi)有,就執(zhí)行sql,執(zhí)行sql后把結(jié)果存入緩存。
粉絲福利:實(shí)戰(zhàn)springboot+CAS單點(diǎn)登錄系統(tǒng)視頻教程免費(fèi)領(lǐng)取
???
?長(zhǎng)按上方微信二維碼?2 秒 即可獲取資料
感謝點(diǎn)贊支持下哈?
