MyBatis一二級緩存機(jī)制全解析
引言
在現(xiàn)代Web應(yīng)用中,數(shù)據(jù)庫訪問往往是性能瓶頸之一。MyBatis作為流行的持久層框架,其緩存機(jī)制是提升應(yīng)用性能的關(guān)鍵特性。理解MyBatis的一二級緩存不僅有助于優(yōu)化應(yīng)用性能,還能避免因緩存不當(dāng)導(dǎo)致的數(shù)據(jù)一致性問題。本文將從基礎(chǔ)概念到高級原理,全方位解析MyBatis緩存機(jī)制。
一、緩存的基本概念:為什么需要緩存?
1.1 緩存的價值
想象一下,如果你每次需要知道時間都去天文臺查詢,效率會很低。相反,看一眼手表(緩存)就能立即獲取時間。MyBatis緩存扮演的就是這個“手表”的角色,它避免了頻繁訪問數(shù)據(jù)庫(天文臺),極大提升了查詢效率。
1.2 緩存的經(jīng)濟(jì)學(xué)原理
- 時間局部性:剛被訪問的數(shù)據(jù)很可能再次被訪問
- 空間局部性:相鄰的數(shù)據(jù)很可能被一起訪問
- 訪問成本:內(nèi)存訪問(納秒級)vs 磁盤/網(wǎng)絡(luò)訪問(毫秒級)
二、一級緩存:SqlSession級別的緩存
2.1 什么是SqlSession?
在深入一級緩存前,需要先理解SqlSession。SqlSession不是數(shù)據(jù)庫連接(Connection),而是一次數(shù)據(jù)庫對話的抽象:
// SqlSession相當(dāng)于一次完整對話,不是一通電話
SqlSession session = sqlSessionFactory.openSession();
try {
// 對話中的多次查詢
userMapper.getUser(1); // 第一次查詢
orderMapper.getOrders(1); // 第二次查詢
accountMapper.getBalance(1); // 第三次查詢
session.commit(); // 確認(rèn)對話內(nèi)容
} finally {
session.close(); // 結(jié)束對話
}2.2 一級緩存的核心特性
作用范圍:SqlSession內(nèi)部(一次對話)
默認(rèn)狀態(tài):自動開啟,無法關(guān)閉
生命周期:隨SqlSession創(chuàng)建而創(chuàng)建,隨其關(guān)閉而銷毀
2.3 一級緩存的工作原理
// 示例代碼展示一級緩存行為
public void demonstrateLevel1Cache() {
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
System.out.println("第一次查詢用戶1:");
User user1 = mapper.selectById(1); // 發(fā)SQL:SELECT * FROM user WHERE id=1
System.out.println("第二次查詢用戶1:");
User user2 = mapper.selectById(1); // 不發(fā)SQL!從一級緩存讀取
System.out.println("查詢用戶2:");
User user3 = mapper.selectById(2); // 發(fā)SQL:參數(shù)不同,緩存未命中
System.out.println("修改用戶1:");
mapper.updateUser(user1); // 清空一級緩存
System.out.println("再次查詢用戶1:");
User user4 = mapper.selectById(1); // 發(fā)SQL:緩存被清空
session.close();
}2.4 一級緩存的數(shù)據(jù)結(jié)構(gòu)
一級緩存的實現(xiàn)非常簡單直接:
// 一級緩存的核心實現(xiàn)類
public class PerpetualCache implements Cache {
// 核心:就是一個ConcurrentHashMap!
private final Map<Object, Object> cache = new ConcurrentHashMap<>();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value); // 簡單的Map.put()
}
@Override
public Object getObject(Object key) {
return cache.get(key); // 簡單的Map.get()
}
}緩存Key的生成規(guī)則:
// CacheKey包含以下要素,決定兩個查詢是否"相同" // 1. Mapper Id(namespace + method) // 2. 分頁參數(shù)(offset, limit) // 3. SQL語句 // 4. 參數(shù)值 // 5. 環(huán)境Id // 這意味著:即使SQL相同,參數(shù)不同,也會生成不同的CacheKey
2.5 一級緩存的失效場景
- 執(zhí)行任何UPDATE/INSERT/DELETE操作
- 手動調(diào)用clearCache()
- 設(shè)置flushCache="true"
- SqlSession關(guān)閉
- 查詢參數(shù)變化(因為CacheKey不同)
三、二級緩存:Mapper級別的全局緩存
3.1 二級緩存的核心特性
作用范圍:Mapper級別(跨SqlSession共享)
默認(rèn)狀態(tài):默認(rèn)關(guān)閉,需要手動開啟
生命周期:隨應(yīng)用運行而存在
3.2 二級緩存的配置
<!-- 1. 全局配置開啟二級緩存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 2. Mapper XML中配置 -->
<mapper namespace="com.example.UserMapper">
<!-- 基本配置 -->
<cache/>
<!-- 詳細(xì)配置 -->
<cache
eviction="LRU" <!-- 淘汰策略 -->
flushInterval="60000" <!-- 刷新間隔(毫秒) -->
size="1024" <!-- 緩存對象數(shù) -->
readOnly="true" <!-- 是否只讀 -->
blocking="false"/> <!-- 是否阻塞 -->
</mapper>
<!-- 3. 在具體查詢上使用緩存 -->
<select id="selectById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<!-- 4. 增刪改操作刷新緩存 -->
<update id="updateUser" flushCache="true">
UPDATE user SET name = #{name} WHERE id = #{id}
</update>3.3 二級緩存的數(shù)據(jù)結(jié)構(gòu)
二級緩存不像一級緩存那么簡單,它采用了裝飾器模式:
二級緩存裝飾器鏈(層層包裝): ┌─────────────────────────┐ │ SerializedCache │ ← 序列化存儲 │ LoggingCache │ ← 日志統(tǒng)計 │ SynchronizedCache │ ← 線程安全 │ LruCache │ ← LRU淘汰 │ PerpetualCache │ ← 基礎(chǔ)HashMap └─────────────────────────┘
每個裝飾器都有特定功能:
- PerpetualCache:基礎(chǔ)存儲,使用HashMap
- LruCache:最近最少使用淘汰
- SynchronizedCache:保證線程安全
- LoggingCache:記錄命中率
- SerializedCache:序列化對象,防止修改
3.4 二級緩存的工作流程
public void demonstrateLevel2Cache() {
// 用戶A查詢(第一個訪問者)
SqlSession sessionA = sqlSessionFactory.openSession();
UserMapper mapperA = sessionA.getMapper(UserMapper.class);
User user1 = mapperA.selectById(1); // 查詢數(shù)據(jù)庫
sessionA.close(); // 關(guān)鍵:關(guān)閉時才會寫入二級緩存
// 用戶B查詢(不同SqlSession)
SqlSession sessionB = sqlSessionFactory.openSession();
UserMapper mapperB = sessionB.getMapper(UserMapper.class);
User user2 = mapperB.selectById(1); // 從二級緩存讀取,不發(fā)SQL
// 管理員更新數(shù)據(jù)
SqlSession sessionC = sqlSessionFactory.openSession();
UserMapper mapperC = sessionC.getMapper(UserMapper.class);
mapperC.updateUser(user1); // 清空相關(guān)二級緩存
sessionC.commit();
sessionC.close();
// 用戶D再次查詢
SqlSession sessionD = sqlSessionFactory.openSession();
UserMapper mapperD = sessionD.getMapper(UserMapper.class);
User user3 = mapperD.selectById(1); // 緩存被清,重新查詢數(shù)據(jù)庫
sessionD.close();
}3.5 二級緩存的同步機(jī)制
二級緩存有一個重要特性:事務(wù)提交后才更新。這意味著:
// 場景:事務(wù)內(nèi)查詢,事務(wù)提交前其他會話看不到更新 SqlSession session1 = sqlSessionFactory.openSession(); UserMapper mapper1 = session1.getMapper(UserMapper.class); // 修改數(shù)據(jù),但未提交 mapper1.updateUser(user); // 此時二級緩存還未更新 // 另一個會話查詢 SqlSession session2 = sqlSessionFactory.openSession(); UserMapper mapper2 = session2.getMapper(UserMapper.class); User user2 = mapper2.selectById(1); // 可能讀到舊數(shù)據(jù)! session1.commit(); // 提交后,二級緩存才會更新 // 之后的新查詢才會看到新數(shù)據(jù)
四、一二級緩存的對比與選擇
4.1 核心差異對比
| 特性 | 一級緩存 | 二級緩存 |
|---|---|---|
| 作用范圍 | SqlSession內(nèi)部 | Mapper級別,跨SqlSession |
| 默認(rèn)狀態(tài) | 開啟 | 關(guān)閉 |
| 數(shù)據(jù)結(jié)構(gòu) | 簡單HashMap | 裝飾器鏈 |
| 共享性 | 私有,不共享 | 公共,所有會話共享 |
| 生命周期 | 隨SqlSession創(chuàng)建銷毀 | 隨應(yīng)用運行持久存在 |
| 性能影響 | 極?。▋?nèi)存訪問) | 中等(可能有序列化開銷) |
| 適用場景 | 會話內(nèi)重復(fù)查詢 | 跨會話共享查詢 |
4.2 生活化比喻
一級緩存 = 私人對話記憶
- 你和朋友的聊天內(nèi)容,只有你們兩人知道
- 聊天結(jié)束(SqlSession關(guān)閉),記憶逐漸模糊
二級緩存 = 公司公告欄
- 重要通知寫在公告欄,所有員工都能看到
- 通知更新時,需要擦掉舊的,寫上新的
- 公告欄內(nèi)容持久存在,直到被更新
4.3 使用場景建議
適合一級緩存的場景:
// 場景1:方法內(nèi)多次查詢相同數(shù)據(jù)
public void processOrder(Long orderId) {
Order order1 = validateOrder(orderId); // 第一次查數(shù)據(jù)庫
Order order2 = calculateDiscount(orderId); // 走一級緩存
Order order3 = generateInvoice(orderId); // 走一級緩存
}
// 場景2:循環(huán)內(nèi)查詢
for (int i = 0; i < 100; i++) {
Config config = configMapper.getConfig("system_timeout");
// 只有第一次查數(shù)據(jù)庫,后續(xù)99次走緩存
}適合二級緩存的場景:
// 場景1:讀多寫少的配置數(shù)據(jù)
SystemConfig config = configMapper.getConfig("app_settings");
// 多個用戶頻繁讀取,很少修改
// 場景2:熱門商品信息
Product product = productMapper.getHotProduct(666);
// 商品詳情頁,大量用戶訪問同一商品
// 場景3:靜態(tài)字典數(shù)據(jù)
List<City> cities = addressMapper.getAllCities();
// 城市列表,很少變化不適合緩存的場景:
// 場景1:實時性要求高的數(shù)據(jù) Stock stock = stockMapper.getRealTimeStock(productId); // 庫存信息,需要實時準(zhǔn)確 // 場景2:頻繁更新的數(shù)據(jù) UserBalance balance = accountMapper.getBalance(userId); // 用戶余額,每次交易都變化 // 場景3:大數(shù)據(jù)量查詢 List<Log> logs = logMapper.getTodayLogs(); // 數(shù)據(jù)量大,緩存占用內(nèi)存過多
五、緩存的高級特性與原理
5.1 緩存淘汰策略
MyBatis提供了多種淘汰策略:
<cache eviction="策略類型" size="緩存大小">
可用策略:
- LRU(Least Recently Used):最近最少使用(默認(rèn))
- FIFO(First In First Out):先進(jìn)先出
- SOFT:軟引用,內(nèi)存不足時被GC回收
- WEAK:弱引用,GC時立即回收
5.2 LRU緩存的實現(xiàn)原理
public class LruCache implements Cache {
private final Cache delegate;
// 使用LinkedHashMap實現(xiàn)LRU
private Map<Object, Object> keyMap;
private Object eldestKey;
public void setSize(final int size) {
keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
eldestKey = eldest.getKey();
}
return tooBig;
}
};
}
@Override
public Object getObject(Object key) {
// 訪問時更新順序
keyMap.get(key);
return delegate.getObject(key);
}
}5.3 緩存查詢的完整流程
查詢執(zhí)行流程: 1. 請求到達(dá)CachingExecutor(二級緩存入口) 2. 生成CacheKey(包含SQL、參數(shù)等信息) 3. 查詢二級緩存 └─ 命中 → 返回結(jié)果 └─ 未命中 → 繼續(xù) 4. 查詢一級緩存 └─ 命中 → 返回結(jié)果,并放入二級緩存(事務(wù)提交時) └─ 未命中 → 繼續(xù) 5. 查詢數(shù)據(jù)庫 6. 結(jié)果存入一級緩存 7. 事務(wù)提交時,一級緩存刷入二級緩存 8. 返回結(jié)果
六、緩存的最佳實踐與避坑指南
6.1 最佳實踐
1. 合理配置緩存大小
<!-- 根據(jù)數(shù)據(jù)特點設(shè)置合適的大小 --> <cache size="1024"/> <!-- 緩存1024個對象 -->
2. 設(shè)置合理的刷新間隔
<!-- 對于變化不頻繁但需要定期更新的數(shù)據(jù) --> <cache flushInterval="1800000"/> <!-- 30分鐘自動刷新 -->
3. 選擇性使用緩存
<!-- 某些查詢跳過緩存 -->
<select id="getRealTimeData" useCache="false">
SELECT * FROM realtime_table
</select>
<!-- 某些查詢強制刷新緩存 -->
<select id="getImportantData" flushCache="true">
SELECT * FROM important_table
</select>4. 關(guān)聯(lián)查詢的緩存策略
<!-- 關(guān)聯(lián)查詢時,使用cache-ref同步緩存 -->
<mapper namespace="com.example.UserMapper">
<cache/>
<!-- 其他配置 -->
</mapper>
<mapper namespace="com.example.OrderMapper">
<!-- 引用UserMapper的緩存 -->
<cache-ref namespace="com.example.UserMapper"/>
</mapper>6.2 常見問題與解決方案
問題1:臟讀問題
場景:一個會話修改數(shù)據(jù)但未提交,另一個會話從二級緩存讀取到舊數(shù)據(jù)。
解決方案:
// 設(shè)置事務(wù)隔離級別
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateUser(User user) {
userMapper.updateUser(user);
}
// 或者在Mapper中設(shè)置flushCache
@Update("UPDATE user SET name=#{name} WHERE id=#{id}")
@Options(flushCache = Options.FlushCachePolicy.TRUE)
int updateUser(User user);問題2:內(nèi)存溢出
場景:緩存大量數(shù)據(jù)導(dǎo)致JVM內(nèi)存不足。
解決方案:
- 設(shè)置合理的緩存大小和淘汰策略
- 使用軟引用/弱引用緩存
- 定期清理不活躍的緩存
問題3:分布式環(huán)境緩存不一致
場景:多臺服務(wù)器,每臺有自己的緩存,數(shù)據(jù)不一致。
解決方案:
- 使用集中式緩存(Redis、Memcached)替代默認(rèn)二級緩存
- 實現(xiàn)自定義Cache接口:
public class RedisCache implements Cache {
private JedisPool jedisPool;
@Override
public void putObject(Object key, Object value) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.set(serialize(key), serialize(value));
}
}
@Override
public Object getObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
byte[] value = jedis.get(serialize(key));
return deserialize(value);
}
}
}問題4:緩存穿透
場景:查詢不存在的數(shù)據(jù),每次都查數(shù)據(jù)庫。
解決方案:
// 緩存空對象
public User getUser(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
// 緩存空值,設(shè)置短過期時間
cacheNullValue(id);
return null;
}
return user;
}6.3 監(jiān)控與調(diào)試
開啟緩存日志
# 查看緩存命中情況 logging.level.org.mybatis=DEBUG logging.level.com.example.mapper=TRACE
監(jiān)控緩存命中率
// 獲取緩存統(tǒng)計信息
Cache cache = sqlSession.getConfiguration()
.getCache("com.example.UserMapper");
if (cache instanceof LoggingCache) {
LoggingCache loggingCache = (LoggingCache) cache;
System.out.println("命中次數(shù): " + loggingCache.getHitCount());
System.out.println("未命中次數(shù): " + loggingCache.getMissCount());
System.out.println("命中率: " +
(loggingCache.getHitCount() * 100.0 /
(loggingCache.getHitCount() + loggingCache.getMissCount())) + "%");
}
七、總結(jié)與思考
7.1 核心要點回顧
- 一級緩存:SqlSession級別,自動開啟,基于HashMap,簡單高效
- 二級緩存:Mapper級別,需手動開啟,基于裝飾器模式,功能豐富
- 緩存Key:由SQL、參數(shù)等要素生成,決定查詢是否"相同"
- 事務(wù)同步:二級緩存在事務(wù)提交后才更新,避免臟讀
- 適用場景:根據(jù)數(shù)據(jù)特點選擇合適的緩存策略
7.2 設(shè)計思想啟示
MyBatis緩存設(shè)計體現(xiàn)了幾個重要軟件設(shè)計原則:
- 單一職責(zé)原則:每個緩存裝飾器只負(fù)責(zé)一個功能
- 開閉原則:通過裝飾器模式,無需修改原有代碼即可擴(kuò)展功能
- 接口隔離:Cache接口定義清晰,便于自定義實現(xiàn)
7.3 實際應(yīng)用建議
在實際項目中:
- 從小開始:先使用一級緩存,確有需要再開啟二級緩存
- 測試驗證:上線前充分測試緩存效果和內(nèi)存占用
- 監(jiān)控調(diào)整:生產(chǎn)環(huán)境監(jiān)控緩存命中率,根據(jù)實際情況調(diào)整配置
- 文檔記錄:記錄緩存配置和策略,便于團(tuán)隊協(xié)作和維護(hù)
7.4 未來展望
隨著微服務(wù)和云原生架構(gòu)的普及,MyBatis緩存也在演進(jìn):
- 分布式緩存集成:更好支持Redis等分布式緩存
- 多級緩存策略:本地緩存+分布式緩存的組合使用
- 智能緩存管理:基于訪問模式的自動緩存優(yōu)化
結(jié)語
MyBatis緩存機(jī)制是一個看似簡單實則精妙的設(shè)計。理解它不僅能幫助我們優(yōu)化應(yīng)用性能,還能加深對緩存設(shè)計模式的理解。記住,緩存是提升性能的利器,但也可能成為數(shù)據(jù)一致的陷阱。合理使用、謹(jǐn)慎配置、持續(xù)監(jiān)控,才能讓緩存真正為應(yīng)用賦能。
緩存不是銀彈,而是需要精心調(diào)校的利器。 在實際開發(fā)中,應(yīng)根據(jù)業(yè)務(wù)特點、數(shù)據(jù)特性和訪問模式,選擇最合適的緩存策略,在性能與一致性之間找到最佳平衡點。
到此這篇關(guān)于MyBatis一二級緩存機(jī)制全解析的文章就介紹到這了,更多相關(guān)MyBatis一二級緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot自定義MessageConverter與內(nèi)容協(xié)商管理器contentNegotiationManag
這篇文章主要介紹了SpringBoot自定義MessageConverter與內(nèi)容協(xié)商管理器contentNegotiationManager的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-10-10
Spring?Security內(nèi)置過濾器的維護(hù)方法
這篇文章主要介紹了Spring?Security的內(nèi)置過濾器是如何維護(hù)的,本文給我們分析一下HttpSecurity維護(hù)過濾器的幾個方法,需要的朋友可以參考下2022-02-02
Jenkins 關(guān)閉和重啟詳細(xì)介紹及實現(xiàn)
這篇文章主要介紹了Jenkins的關(guān)閉、重啟的相關(guān)資料,用jar -jar jenkins.war來啟動jenkins服務(wù)器,那么我們?nèi)绾侮P(guān)閉或者重啟jenkins服務(wù)器呢,這里就給出實現(xiàn)的方法,需要的朋友可以參考下2016-11-11
java servlet獲得客戶端相關(guān)信息的簡單代碼
這篇文章主要介紹了java servlet獲得客戶端相關(guān)信息的簡單代碼,有需要的朋友可以參考一下2013-12-12
Java List<JSONObject>如何轉(zhuǎn)換為List<實體類>
這篇文章主要介紹了Java List<JSONObject>如何轉(zhuǎn)換為List<實體類>的方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-05-05

