1. 驗(yàn)證二級(jí)緩存
在上一篇帖子中的 User 和 Department 實(shí)體類依然要用,這里就不再贅述了,要啟用二級(jí)緩存,需要在 Mapper.xml 文件中指定 cache 標(biāo)簽,如下:
UserMapper.xml
select * from user
/select?>
Department.xml
select * from department;
/select?>
在 Department.xml 中的 cache 標(biāo)簽指定了 readOnly 屬性,因?yàn)樵撆渲孟鄬?duì)比較重要,所以我們?cè)谶@里把它講解一下:
readOnly 默認(rèn)為 false,這種情況下通過(guò)二級(jí)緩存查詢出來(lái)的數(shù)據(jù)會(huì)進(jìn)行一次 序列化深拷貝。在這里大家需要回想一下介紹一級(jí)緩存時(shí)舉的例子:一級(jí)緩存查詢出來(lái)返回的是 該對(duì)象的引用,若我們對(duì)它修改,再查詢 時(shí)觸發(fā)一級(jí)緩存獲得的便是 被修改過(guò)的數(shù)據(jù)。但是,二級(jí)緩存的序列化機(jī)制則不同,它獲取到的是 緩存深拷貝的對(duì)象,這樣對(duì)二級(jí)緩存進(jìn)行修改操作不影響后續(xù)查詢結(jié)果。
如果將該屬性配置為 true 的話,那么它就會(huì)變得和一級(jí)緩存一樣,返回的是對(duì)象的引用,這樣做的好處是 避免了深拷貝的開(kāi)銷。
為什么會(huì)有這種機(jī)制呢?
因?yàn)槎?jí)緩存是 Mapper級(jí)別 的,不能保證其他 SqlSession 不對(duì)二級(jí)緩存進(jìn)行修改,所以這也是一種保護(hù)機(jī)制。
我們驗(yàn)證一下這個(gè)例子,Department 和 User 的查詢都執(zhí)行了兩遍(注意 事務(wù)提交之后 才能使二級(jí)緩存生效):
public static void main(String[] args) {
InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 開(kāi)啟二級(jí)緩存需要在同一個(gè)SqlSessionFactory下,二級(jí)緩存存在于 SqlSessionFactory 生命周期,如此才能命中二級(jí)緩存
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(xml);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
System.out.println("----------department第一次查詢 ↓------------");
List departments1 = departmentMapper1.findAll();
System.out.println("----------user第一次查詢 ↓------------");
List users1 = userMapper1.findAll();
// 提交事務(wù),使二級(jí)緩存生效
sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
System.out.println("----------department第二次查詢 ↓------------");
List departments2 = departmentMapper2.findAll();
System.out.println("----------user第二次查詢 ↓------------");
List users2 = userMapper2.findAll();
sqlSession1.close();
sqlSession2.close();
}
Department 和 User 的同一條查詢語(yǔ)句都執(zhí)行了兩遍,因?yàn)?Department 指定了 readOnly 為true,那么 兩次查詢返回的對(duì)象均為同一個(gè)引用,而 User 則反之,Debug 試一下:

cache 的其他屬性
| 屬性 | 描述 | 備注 |
|---|---|---|
| eviction | 緩存回收策略 | 默認(rèn) LRU |
| type | 二級(jí)緩存的實(shí)現(xiàn)類 | 默認(rèn)實(shí)現(xiàn) PerpetualCache |
| size | 緩存引用數(shù)量 | 默認(rèn)1024 |
| flushInterval | 定時(shí)清除時(shí)間間隔 | 默認(rèn)無(wú) |
| blocking | 阻塞獲取緩存數(shù)據(jù) | 若緩存中找不到對(duì)應(yīng)的 key ,是否會(huì)一直阻塞,直到有對(duì)應(yīng)的數(shù)據(jù)進(jìn)入緩存。默認(rèn) false |
接下來(lái)我們測(cè)試驗(yàn)證下二級(jí)緩存的生效:
SqlSession sqlSession1 = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper1 = sqlSession1.getMapper(DepartmentMapper.class);
System.out.println("----------department第一次查詢 ↓------------");
List departments1 = departmentMapper1.findAll();
// 使二級(jí)緩存生效
sqlSession1.commit();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
DepartmentMapper departmentMapper2 = sqlSession2.getMapper(DepartmentMapper.class);
System.out.println("----------department第二次查詢 ↓------------");
List departments2 = departmentMapper2.findAll();
第一次 Query,會(huì)去數(shù)據(jù)庫(kù)中查

第二次 Query,直接從二級(jí)緩存中取

2. 二級(jí)緩存的原理
二級(jí)緩存對(duì)象 Cache
在加載 Mapper 文件(org.apache.ibatis.builder.xml.XMLConfigBuilder#mappersElement 方法)時(shí),定義了加載 cache 標(biāo)簽的步驟(org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement 方法),代碼如下:
public class XMLMapperBuilder extends BaseBuilder {
// ...
private void configurationElement(XNode context) {
try {
// 若想要在多個(gè)命名空間中共享相同的緩存配置和實(shí)例,可以使用 cache-ref 元素來(lái)引用另一個(gè)緩存
cacheRefElement(context.evalNode("cache-ref"));
// 配置二級(jí)緩存
cacheElement(context.evalNode("cache"));
// ...
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
}
具體解析邏輯如下:
public class XMLMapperBuilder extends BaseBuilder {
// ...
private void cacheElement(XNode context) {
if (context != null) {
// 二級(jí)緩存實(shí)現(xiàn)類,默認(rèn) PerpetualCache,我們?cè)谝患?jí)緩存也提到過(guò)
String type = context.getStringAttribute("type", "PERPETUAL");
Class? extends Cache?> typeClass = typeAliasRegistry.resolveAlias(type);
// 緩存清除策略,默認(rèn) LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class? extends Cache?> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 定時(shí)清除間隔
Long flushInterval = context.getLongAttribute("flushInterval");
// 緩存引用數(shù)量
Integer size = context.getIntAttribute("size");
// readOnly上文我們提到過(guò),默認(rèn) false
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
// blocking 默認(rèn) false
boolean blocking = context.getBooleanAttribute("blocking", false);
Properties props = context.getChildrenAsProperties();
// 創(chuàng)建緩存對(duì)象
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
}
我們繼續(xù)看創(chuàng)建二級(jí)緩存對(duì)象的邏輯 org.apache.ibatis.builder.MapperBuilderAssistant#useNewCache,可以發(fā)現(xiàn),創(chuàng)建 Cache 對(duì)象使用了 建造者模式:

建造者 CacheBuilder 并沒(méi)有被組合在任意一種緩存的實(shí)現(xiàn)類中,而是根據(jù)如下代碼中 implementation(valueOrDefault(typeClass, PerpetualCache.class)) 邏輯指定了要?jiǎng)?chuàng)建的緩存類型,并在 build 方法中使用反射創(chuàng)建對(duì)應(yīng)實(shí)現(xiàn)類:
public class MapperBuilderAssistant extends BaseBuilder {
// ...
public Cache useNewCache(Class? extends Cache?> typeClass, Class? extends Cache?> evictionClass, Long flushInterval,
Integer size, boolean readWrite, boolean blocking, Properties props) {
// 建造者模式,將標(biāo)簽屬性賦值
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;
}
}
其中 addDecorator(valueOrDefault(evictionClass, LruCache.class)) 邏輯添加了 裝飾器,使用了 裝飾器模式,將 LruCache 類型的裝飾器添加到 decorators 中:
public class CacheBuilder {
private final List> decorators;
public CacheBuilder addDecorator(Class? extends Cache?> decorator) {
// 將 LruCache 裝飾器添加到 decorators
if (decorator != null) {
this.decorators.add(decorator);
}
return this;
}
// ...
}
在 CacheBuilder#build 方法中,如下為封裝裝飾器的邏輯:
public class CacheBuilder {
// ...
public Cache build() {
setDefaultImplementations();
// 反射創(chuàng)建 PerpetualCache
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);
// 封裝裝飾器的邏輯
if (PerpetualCache.class.equals(cache.getClass())) {
for (Class? extends Cache?> decorator : decorators) {
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
// 初始化基礎(chǔ)必要的裝飾器
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
cache = new LoggingCache(cache);
}
return cache;
}
private Cache setStandardDecorators(Cache cache) {
try {
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
metaCache.setValue("size", size);
}
// 定時(shí)清空二級(jí)緩存
if (clearInterval != null) {
cache = new ScheduledCache(cache);
((ScheduledCache) cache).setClearInterval(clearInterval);
}
// readOnly屬性相關(guān)的讀寫(xiě)緩存
if (readWrite) {
cache = new SerializedCache(cache);
}
// 日志緩存和同步緩存(借助 ReentrantLock 實(shí)現(xiàn))
cache = new LoggingCache(cache);
cache = new SynchronizedCache(cache);
// 阻塞屬性的緩存
if (blocking) {
cache = new BlockingCache(cache);
}
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
}
所有裝飾器都在 org.apache.ibatis.cache.decorators 包下,唯獨(dú) PerpetualCache 在org.apache.ibatis.cache.impl 包下:

PerpetualCache 中不包含 delegate 屬性表示裝飾器,說(shuō)明它將作為最基礎(chǔ)的實(shí)現(xiàn)類被其他裝飾器裝飾,而其他裝飾器中均含有 delegate 屬性來(lái)裝飾其他實(shí)現(xiàn)。
默認(rèn)創(chuàng)建的二級(jí)緩存類型如下:

類關(guān)系圖如下:

query 方法對(duì)二級(jí)緩存的應(yīng)用
org.apache.ibatis.executor.CachingExecutor#query 方法使用了二級(jí)緩存,如下代碼所示:
public class CachingExecutor implements Executor {
// 事務(wù)緩存管理器
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 先獲取二級(jí)緩存,該對(duì)象便是上文中創(chuàng)建的被裝飾器裝飾的 PerpetualCache
Cache cache = ms.getCache();
if (cache != null) {
// 判斷是否需要清除緩存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
// 從二級(jí)緩存中取
@SuppressWarnings("unchecked")
List list = (List) tcm.getObject(cache, key);
if (list == null) {
// 沒(méi)取到二級(jí)緩存,嘗試取一級(jí)緩存或去數(shù)據(jù)庫(kù)查詢
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
// “添加二級(jí)緩存”
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
// 沒(méi)有二級(jí)緩存的話,執(zhí)行的是我們?cè)谝患?jí)緩存中介紹的方法,要么取一級(jí)緩存,否則去數(shù)據(jù)庫(kù)查
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
// ...
}
上述邏輯比較清晰,我們?cè)谏衔闹刑岬竭^(guò),只有 事務(wù)提交的時(shí)候才會(huì)將二級(jí)緩存保存,但是其中有 tcm.putObject(cache, key, list); 邏輯,似乎在這里保存了二級(jí)緩存,而此時(shí)事務(wù)還未提交,這便需要我們一探究竟。它會(huì)執(zhí)行到 TransactionalCacheManager#putObject 方法:
public class TransactionalCacheManager {
private final Map transactionalCaches = new HashMap?>();
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
private TransactionalCache getTransactionalCache(Cache cache) {
return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
}
}
TransactionalCacheManager 事務(wù)緩存管理器會(huì)創(chuàng)建并管理 TransactionalCache 對(duì)象,TransactionalCache 同樣是 Cache 裝飾器,它將裝飾在 SynchronizedCache 上:
public class TransactionalCache implements Cache {
// 被裝飾對(duì)象,默認(rèn)是 SynchronizedCache
private final Cache delegate;
// 該元素將保存在事務(wù) commit 時(shí)被保存的鍵值對(duì)緩存
private final Map entriesToAddOnCommit;
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
// ...
}
putObject 執(zhí)行時(shí)便是向 entriesToAddOnCommit 添加元素,記錄二級(jí)緩存鍵值對(duì),并沒(méi)有真正添加到二級(jí)緩存 PerpetualCache 對(duì)象中。此外,entriesToAddOnCommit 的命名,也暗示了在事務(wù)提交時(shí)緩存才會(huì)被保存。那么接下來(lái),便需要看一下事務(wù)提交邏輯。
在上文測(cè)試二級(jí)緩存的代碼中,有 sqlSession1.commit(); 邏輯。在事務(wù)提交時(shí),它會(huì)走到 CachingExecutor#commit 方法,其中會(huì)調(diào)用到 TransactionalCacheManager#commit 方法,如下:
public class CachingExecutor implements Executor {
// ...
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
@Override
public void commit(boolean required) throws SQLException {
// ...
tcm.commit();
}
}
在該方法中,會(huì)遍歷所有的事務(wù)緩存 TransactionalCache,并逐一調(diào)用它們的 commit 方法,
public class TransactionalCacheManager {
private final Map transactionalCaches = new HashMap?>();
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
// ...
commit 方法會(huì)調(diào)用 delegate.commit 方法,而 delegate 為被裝飾對(duì)象,最后便會(huì)將二級(jí)緩存記錄:
public class TransactionalCache implements Cache {
private final Map entriesToAddOnCommit;
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
private void flushPendingEntries() {
// 事務(wù)提交,將 entriesToAddOnCommit 中所有待添加的二級(jí)緩存添加
for (Map.Entry entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
// ...
}
緩存失效
事務(wù)回滾是不是會(huì)使本次事務(wù)中相關(guān)的二級(jí)緩存失效呢?
public class TransactionalCache implements Cache {
public void rollback() {
unlockMissedEntries();
reset();
}
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
private void unlockMissedEntries() {
for (Object entry : entriesMissedInCache) {
try {
delegate.removeObject(entry);
} catch (Exception e) {
log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);
}
}
}
// ...
}
的確如此,它會(huì)將未被緩存的元素清除 reset(),也會(huì)把在本次事務(wù)中操作過(guò)的數(shù)據(jù)在二級(jí)緩存中移除 unlockMissedEntries()。
那數(shù)據(jù)發(fā)生新增、修改或刪除呢?同樣會(huì)清除緩存
public class CachingExecutor implements Executor {
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
// 默認(rèn) flushCacheRequired 為 true
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
它將調(diào)用 TransactionalCache#clear 方法,將待生效的 entriesToAddOnCommit 二級(jí)緩存清除,并標(biāo)記 clearOnCommit 為 true,在事務(wù)提交時(shí),二級(jí)緩存會(huì)執(zhí)行清除緩存的 clear 方法:
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
緩存生效范圍
到這里,我們已經(jīng)基本弄清楚二級(jí)緩存生效的原理了,那么接下來(lái)我們需要解釋“為什么二級(jí)緩存是 Mapper 級(jí)別的?”其實(shí)也非常簡(jiǎn)單,看如下代碼:
public class CachingExecutor implements Executor {
@Override
public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 先獲取二級(jí)緩存,該對(duì)象便是上文中創(chuàng)建的被裝飾器裝飾的 PerpetualCache
Cache cache = ms.getCache();
// ...
}
// ...
}
在執(zhí)行查詢時(shí),二級(jí)緩存 Cache 是在 MappedStatement 中獲取的,Mapper 中每個(gè) SQL 聲明都對(duì)應(yīng)唯一的 MappedStatement,當(dāng)同一條 SQL 被執(zhí)行時(shí),它們都會(huì)去取同樣的緩存,所以可以說(shuō)它是 Mapper 級(jí)別的,說(shuō)成 MappedStatement 級(jí)別更準(zhǔn)確,二級(jí)緩存支持多個(gè) SqlSession 共享。
為什么要在事務(wù)提交后才生效?
在這里我們討論一個(gè)問(wèn)題:為什么二級(jí)要在事務(wù)提交后才能生效呢?
因?yàn)槎?jí)緩存可以在不同的 SqlSession 間生效,畫(huà)個(gè)圖你就明白了:

如果 SqlSession1先修改了數(shù)據(jù),再查詢數(shù)據(jù),如果二級(jí)緩存在事務(wù)未提交時(shí)就生效,那么 SqlSession2 調(diào)用同樣的查詢時(shí)便會(huì)從 二級(jí)緩存中獲取數(shù)據(jù),但是此時(shí) SqlSession1回滾了事務(wù),那么此時(shí)就會(huì)導(dǎo)致 SqlSession2 從二級(jí)緩存獲取的數(shù)據(jù) 變成臟數(shù)據(jù),這就是為什么二級(jí)緩存要在事務(wù)提交后才能生效的原因。
3. 為什么要擴(kuò)展二級(jí)緩存?
MyBatis 中設(shè)計(jì)一級(jí)緩存和二級(jí)緩存的目的是為了提高數(shù)據(jù)庫(kù)訪問(wèn)的效率,但它們的作用范圍和使用場(chǎng)景有所不同,各自有其特定的用途和優(yōu)勢(shì)。
一級(jí)緩存 默認(rèn)開(kāi)啟,是基于 SqlSession 的,也就是說(shuō),它的作用范圍僅限于一次數(shù)據(jù)庫(kù)會(huì)話,所以當(dāng)會(huì)話關(guān)閉后,緩存就會(huì)被清除。這意味著不同會(huì)話之間無(wú)法共享緩存數(shù)據(jù)。而 二級(jí)緩存 是基于 Mapper 級(jí)別的,需要顯式配置開(kāi)啟,可以在多個(gè) SqlSession 之間共享。當(dāng)然也由于二級(jí)緩存的作用范圍更廣,因此需要更復(fù)雜的緩存失效策略和數(shù)據(jù)一致性管理,以避免數(shù)據(jù)不一致的問(wèn)題。二級(jí)緩存的引入是為了在更大范圍內(nèi)(多個(gè)會(huì)話之間)提高數(shù)據(jù)訪問(wèn)的效率,特別是在讀多寫(xiě)少的應(yīng)用場(chǎng)景。
4. 總結(jié)
二級(jí)緩存本質(zhì)上是 HashMap,在 PerpetualCache 實(shí)現(xiàn)類中
二級(jí)緩存是 Mapper 級(jí)別的,可以在不同 SqlSession 間共享
特殊的 readOnly 標(biāo)簽,默認(rèn)為 false,表示二級(jí)緩存中是被深拷貝的對(duì)象
二級(jí)緩存需要在事務(wù)提交后才能生效
執(zhí)行 Insert、Delete、Update 語(yǔ)句會(huì)使 當(dāng)前 Mapper 下的二級(jí)緩存失效
審核編輯 黃宇
-
源碼
+關(guān)注
關(guān)注
8文章
688瀏覽量
31419 -
mybatis
+關(guān)注
關(guān)注
0文章
64瀏覽量
7168
發(fā)布評(píng)論請(qǐng)先 登錄
芯盛智能XT6160企業(yè)級(jí)SSD主控芯片通過(guò)國(guó)家商用密碼產(chǎn)品二級(jí)認(rèn)證
OBOO鷗柏丨國(guó)產(chǎn)納米電容智慧黑板搭載飛騰騰銳D3000芯片新品上市
低功耗同步SRAM擴(kuò)展存儲(chǔ)器結(jié)構(gòu)特點(diǎn)
從軟件到硬件的轉(zhuǎn)場(chǎng):一場(chǎng)由OceanOS-CM0啟發(fā)的嵌入式思考
天璣9400+芯片助力REDMI K Pad性能火力全開(kāi)
匯編寄存器的知識(shí)
醫(yī)院隨訪管理系統(tǒng)源碼,三級(jí)隨訪系統(tǒng)源碼,Java+Springboot,Vue,Ant-Design+MySQL5
配電柜二級(jí)浪涌保護(hù)器的安裝選型與行業(yè)應(yīng)用方案
緩存之美:萬(wàn)文詳解 Caffeine 實(shí)現(xiàn)原理(上)
緩存之美:從根上理解 ConcurrentHashMap
HarmonyOS NEXT應(yīng)用元服務(wù)常見(jiàn)列表操作二級(jí)聯(lián)動(dòng)
二級(jí)浪涌電路之保險(xiǎn)絲選型
蔡司培訓(xùn)|提升技能必看——AUKOM 一級(jí)/二級(jí)課程培訓(xùn)
由 Mybatis 源碼暢談軟件設(shè)計(jì)(八):從根上理解 Mybatis 二級(jí)緩存
評(píng)論