MyBatis 緩存機制詳解(一級緩存+二級緩存+自定義緩存)
MyBatis 的緩存機制是其核心性能優化手段之一,目的是減少數據庫查詢次數,降低IO開銷,提升查詢效率。其設計遵循“分層緩存”理念,分為 一級緩存(SqlSession 級別) 和 二級緩存(Mapper 級別),同時支持集成第三方緩存(如 Redis)實現分佈式場景下的緩存共享。
一、緩存核心設計理念
- 緩存粒度:從“會話級”到“跨會話級”,逐步擴大緩存作用範圍;
- 失效策略:基於“數據一致性”優先,增刪改操作會觸發對應緩存失效;
- 存儲介質:默認基於內存(HashMap),支持自定義擴展(如磁盤、分佈式緩存);
- 命中規則:緩存 Key 由“SQL語句+參數+環境信息+Mapper 信息”組成,確保緩存精準命中。
二、一級緩存(SqlSession 級別緩存)
1. 基本定義
一級緩存是 SqlSession 實例級別的緩存,即同一個 SqlSession 內執行相同的查詢操作,只會第一次訪問數據庫,後續直接從內存中獲取結果。
2. 核心特性
- 默認開啓:無需任何配置,MyBatis 自動啓用,無法手動關閉;
- 作用範圍:僅當前 SqlSession 有效,不同 SqlSession 之間的緩存相互隔離;
- 存儲介質:內存中的 HashMap,鍵(Key)是緩存唯一標識,值(Value)是查詢結果對象;
- 線程不安全:SqlSession 是線程私有對象,不存在多線程併發訪問緩存的問題。
3. 緩存 Key 的構成(確保查詢唯一性)
一級緩存的 Key 由以下 4 部分組成,缺一不可:
Key = HashMap<Object, Object> {
"MappedStatementId": Mapper接口全類名+方法名(如com.example.mapper.UserMapper.selectById),
"SQL語句": 最終執行的SQL(含動態SQL拼接結果),
"參數": 查詢參數(如id=1001),
"環境信息": 數據庫連接環境(如數據源ID、事務狀態)
}
4. 緩存命中與失效場景
(1)命中場景
- 同一個 SqlSession 內,執行 相同的查詢方法+相同參數+相同環境;
- 兩次查詢之間沒有執行該表的增刪改操作(insert/update/delete);
- 兩次查詢之間沒有手動清除緩存(
sqlSession.clearCache())。
(2)失效場景(重點面試考點)
- 執行
sqlSession.close():關閉 SqlSession 時,一級緩存會被銷燬; - 執行增刪改操作(
insert()/update()/delete()):MyBatis 會自動清除當前 SqlSession 內的所有一級緩存(避免數據不一致); - 手動清除緩存:調用
sqlSession.clearCache()方法,主動清空當前 SqlSession 的緩存; - 跨 SqlSession 查詢:不同 SqlSession 之間的緩存相互獨立,無法共享;
- 查詢參數/方法不同:即使是同一 Mapper 方法,參數不同或方法名不同,也會生成不同的緩存 Key;
- 開啓全局二級緩存:一級緩存依然生效,但查詢結果會同步寫入二級緩存(不影響一級緩存命中)。
5. 一級緩存執行流程(示例)
// 1. 獲取 SqlSession(默認不自動提交事務)
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 2. 第一次查詢:訪問數據庫,結果存入一級緩存
User user1 = userMapper.selectById(1001);
System.out.println(user1); // 數據庫查詢
// 3. 第二次查詢:參數相同,直接從一級緩存獲取
User user2 = userMapper.selectById(1001);
System.out.println(user2); // 緩存命中,無數據庫查詢
// 4. 執行更新操作:觸發一級緩存清空
userMapper.updateName(1001, "新名字");
sqlSession.commit();
// 5. 第三次查詢:緩存已失效,重新訪問數據庫
User user3 = userMapper.selectById(1001);
System.out.println(user3); // 數據庫查詢
// 6. 關閉 SqlSession:一級緩存銷燬
sqlSession.close();
三、二級緩存(Mapper 級別緩存)
1. 基本定義
二級緩存是 Mapper 接口級別的緩存,即同一個 Mapper 接口下的所有 SqlSession 共享緩存(跨 SqlSession 共享)。例如,UserMapper 的二級緩存可被多個 SqlSession 訪問,適合查詢頻繁、修改少的場景。
2. 核心特性
- 默認關閉:需手動配置開啓;
- 作用範圍:同一個 Mapper 接口(namespace 相同),跨 SqlSession 共享;
- 存儲介質:默認是內存 HashMap,支持配置為磁盤存儲或第三方緩存(如 Redis);
- 線程安全:MyBatis 內部通過鎖機制保證多線程併發訪問緩存的安全性;
- 序列化要求:緩存的實體類必須實現
Serializable接口(默認緩存會序列化存儲,避免對象引用問題)。
3. 二級緩存的開啓與配置
(1)全局配置開啓(可選,默認已開啓)
在 mybatis-config.xml 中配置 cacheEnabled(默認值為 true,可省略):
<configuration>
<settings>
<!-- 開啓二級緩存總開關(關閉後所有 Mapper 的二級緩存都失效) -->
<setting name="cacheEnabled" value="true"/>
</settings>
</configuration>
(2)Mapper 級別開啓(必須配置)
在對應的 Mapper.xml 中添加 <cache> 標籤,開啓當前 Mapper 的二級緩存:
<!-- UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
<!-- 開啓二級緩存,配置緩存參數 -->
<cache
eviction="LRU" <!-- 緩存回收策略(默認 LRU) -->
flushInterval="60000" <!-- 緩存過期時間(毫秒,60秒) -->
size="1024" <!-- 緩存最大條目數(默認 1024) -->
readOnly="true" <!-- 是否只讀(默認 false) -->
blocking="false" <!-- 是否阻塞(緩存未命中時等待其他線程查詢結果,默認 false) -->
/>
<!-- 單個查詢方法可單獨控制是否使用二級緩存(默認使用) -->
<select id="selectById" resultType="User" useCache="true">
select * from user where id = #{id}
</select>
<!-- 增刪改方法可控制是否觸發緩存清空(默認觸發) -->
<update id="updateName" flushCache="true">
update user set name = #{name} where id = #{id}
</update>
</mapper>
(3)關鍵配置參數説明
|
參數名
|
取值範圍
|
作用説明
|
|
|
LRU(默認)/FIFO/SOFT/WEAK
|
緩存回收策略:
- LRU:最近最少使用(移除最長時間未被訪問的緩存);
- FIFO:先進先出(按緩存添加順序移除);
- SOFT:軟引用(JVM內存不足時移除);
- WEAK:弱引用(垃圾回收時直接移除)。
|
|
|
數值(毫秒)
|
緩存自動過期時間,默認無過期(一直有效)。
|
|
|
正整數
|
緩存最大存儲條目數,超出後按回收策略移除舊緩存(避免內存溢出)。
|
|
|
true/false(默認)
|
- true:緩存返回對象本身(性能高,但線程不安全,需確保對象不被修改);
- false:緩存返回對象的拷貝(通過序列化,線程安全,性能略低)。
|
|
|
true/false(默認)
|
- true:緩存未命中時,其他線程需等待當前線程查詢數據庫並寫入緩存(避免緩存穿透);
- false:緩存未命中時,所有線程均訪問數據庫(可能導致併發查詢)。
|
(4)實體類序列化要求
二級緩存默認會對查詢結果進行序列化存儲(即使 readOnly="true"),因此實體類必須實現 Serializable 接口,否則會拋出 NotSerializableException 異常:
// 實體類實現 Serializable 接口
public class User implements Serializable {
private Long id;
private String name;
// getter/setter...
}
4. 二級緩存命中與失效場景
(1)命中場景
- 同一個 Mapper 接口(namespace 相同);
- 不同 SqlSession 執行相同的查詢方法+相同參數;
- 兩次查詢之間沒有執行該 Mapper 下的增刪改操作;
- 緩存未過期且未達到最大條目數;
- 開啓了二級緩存(全局+Mapper 級別均開啓)。
(2)失效場景(重點面試考點)
- 執行增刪改操作:當前 Mapper 下的任何
insert()/update()/delete()操作(無論是否修改目標數據),都會清空該 Mapper 的所有二級緩存; - 手動清空緩存:調用
sqlSession.clearCache()(僅清空當前 SqlSession 的一級緩存)或sqlSessionFactory.getConfiguration().getCache("namespace").clear()(清空指定 Mapper 的二級緩存); - 未開啓二級緩存:全局開關
cacheEnabled="false"或 Mapper 未配置<cache>標籤; - 查詢方法禁用二級緩存:
select標籤設置useCache="false"(如實時性要求高的查詢); - 增刪改方法未觸發緩存清空:
insert/update/delete標籤設置flushCache="false"(不推薦,會導致數據不一致); - 實體類未實現
Serializable接口:緩存寫入失敗,無法命中; - 跨 Mapper 查詢:不同 Mapper 接口的緩存相互獨立,無法共享(如 UserMapper 和 OrderMapper 的緩存不互通)。
5. 二級緩存執行流程(示例)
// 1. 第一個 SqlSession:查詢並寫入二級緩存
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = mapper1.selectById(1001); // 數據庫查詢,結果存入一級緩存+二級緩存
sqlSession1.close(); // 關閉 SqlSession 時,一級緩存銷燬,二級緩存保留
// 2. 第二個 SqlSession:共享二級緩存
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = mapper2.selectById(1001); // 二級緩存命中,無數據庫查詢
sqlSession2.close();
// 3. 第三個 SqlSession:執行更新操作,觸發二級緩存清空
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
mapper3.updateName(1001, "新名字");
sqlSession3.commit(); // 提交事務,清空 UserMapper 的二級緩存
sqlSession3.close();
// 4. 第四個 SqlSession:緩存失效,重新查詢
SqlSession sqlSession4 = sqlSessionFactory.openSession();
UserMapper mapper4 = sqlSession4.getMapper(UserMapper.class);
User user4 = mapper4.selectById(1001); // 二級緩存已失效,數據庫查詢
sqlSession4.close();
四、一級緩存與二級緩存的區別(面試高頻)
|
對比維度
|
一級緩存(SqlSession 級別)
|
二級緩存(Mapper 級別)
|
|
開啓方式
|
默認開啓,無需配置
|
默認關閉,需全局+Mapper 配置
|
|
作用範圍
|
單個 SqlSession(線程私有)
|
同一個 Mapper 接口(跨 SqlSession)
|
|
存儲介質
|
內存 HashMap(非序列化)
|
內存 HashMap/磁盤/第三方緩存(需序列化)
|
|
實體類要求
|
無序列化要求
|
必須實現 Serializable 接口
|
|
失效觸發
|
關閉 SqlSession、增刪改、手動清空
|
增刪改操作、緩存過期、手動清空
|
|
數據一致性
|
高(會話內隔離,修改後緩存清空)
|
中(跨會話共享,需依賴失效機制)
|
|
適用場景
|
單次會話內重複查詢(如表單校驗)
|
多會話共享查詢(如字典數據、靜態數據)
|
|
線程安全
|
安全(線程私有)
|
安全(內部鎖機制)
|
核心結論:一級緩存是“會話內優化”,二級緩存是“跨會話優化”,二者協同工作(查詢時先查一級緩存→再查二級緩存→最後查數據庫)。
五、自定義緩存(集成第三方緩存)
MyBatis 的默認二級緩存是內存級緩存,存在以下侷限性:
- 不支持分佈式場景(多服務實例無法共享緩存);
- 內存有限,無法存儲大量數據;
- 服務重啓後緩存丟失。
因此,實際開發中常集成 Redis、Ehcache 等第三方緩存替代默認二級緩存,MyBatis 提供了 Cache 接口,支持自定義緩存實現。
1. 集成 Redis 實現二級緩存(實戰示例)
(1)核心依賴(Maven)
<!-- MyBatis 緩存接口 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- Redis 客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.6</version>
</dependency>
<!-- 序列化工具(可選,用於對象序列化) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.41</version>
</dependency>
(2)實現 MyBatis 的 Cache 接口
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 自定義 Redis 緩存實現
*/
public class RedisCache implements Cache {
// 緩存 ID(對應 Mapper 的 namespace)
private final String id;
// Redis 連接池
private final JedisPool jedisPool;
// 讀寫鎖(保證線程安全)
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 構造方法:MyBatis 會自動傳入 Mapper 的 namespace 作為 id
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache ID cannot be null");
}
this.id = id;
// 初始化 Redis 連接池(實際開發中應通過配置文件讀取參數)
this.jedisPool = new JedisPool("localhost", 6379);
}
@Override
public String getId() {
return id; // 緩存唯一標識(必須返回 Mapper 的 namespace)
}
// 存入緩存:key 是 MyBatis 生成的緩存 Key,value 是查詢結果
@Override
public void putObject(Object key, Object value) {
try (Jedis jedis = jedisPool.getResource()) {
// 序列化 key 和 value(使用 fastjson2)
String redisKey = key.toString();
String redisValue = com.alibaba.fastjson2.JSON.toJSONString(value);
// 存入 Redis,設置過期時間(30分鐘)
jedis.setex(redisKey, 1800, redisValue);
}
}
// 獲取緩存:根據 key 查詢 Redis
@Override
public Object getObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
String redisKey = key.toString();
String redisValue = jedis.get(redisKey);
if (redisValue == null) {
return null;
}
// 反序列化:根據實際返回類型轉換(此處簡化處理,實際需結合 TypeHandler)
return com.alibaba.fastjson2.JSON.parseObject(redisValue, Object.class);
}
}
// 移除緩存(MyBatis 內部很少調用,可空實現)
@Override
public Object removeObject(Object key) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.del(key.toString());
}
}
// 清空緩存(增刪改操作時觸發)
@Override
public void clear() {
try (Jedis jedis = jedisPool.getResource()) {
// 模糊刪除當前 Mapper 的所有緩存(key 前綴為 id + ":")
jedis.keys(id + ":*").forEach(jedis::del);
}
}
// 獲取緩存大小(可選實現)
@Override
public int getSize() {
try (Jedis jedis = jedisPool.getResource()) {
return Math.toIntExact(jedis.keys(id + ":*").size());
}
}
// 獲取讀寫鎖(MyBatis 用於併發控制)
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}
(3)在 Mapper.xml 中配置自定義緩存
<mapper namespace="com.example.mapper.UserMapper">
<!-- 配置 Redis 緩存實現(替代默認緩存) -->
<cache type="com.example.cache.RedisCache">
<!-- 可自定義緩存參數(通過構造方法或 setter 注入) -->
</cache>
<!-- 查詢方法使用 Redis 緩存 -->
<select id="selectById" resultType="User" useCache="true">
select * from user where id = #{id}
</select>
</mapper>
2. 自定義緩存的核心注意事項
- 緩存 Key 唯一性:必須基於
namespace + SQL + 參數生成 Key,避免不同 Mapper 緩存衝突; - 序列化/反序列化:確保實體類可序列化,且反序列化時類型匹配(可結合 MyBatis 的
TypeHandler優化); - 過期時間設置:避免緩存長期有效導致數據不一致,建議設置合理的過期時間(如 30 分鐘~1 小時);
- 分佈式鎖:分佈式場景下,需通過 Redis 分佈式鎖(如 RedLock)優化併發問題(避免緩存穿透/擊穿);
- 連接池管理:Redis 連接池需合理配置(最大連接數、空閒時間),避免連接泄露。
六、緩存機制的最佳實踐(面試+實戰)
1. 適用場景
- 一級緩存:默認啓用,無需額外配置,適合單次會話內重複查詢(如表單提交前的校驗、批量操作中的重複查詢);
- 二級緩存:適合查詢頻繁、修改少、實時性要求低的數據(如字典表、地區表、靜態配置數據);
- 第三方緩存(Redis):分佈式系統、微服務架構,需要跨服務共享緩存的場景。
2. 避坑指南
- 禁止在高頻修改的數據上使用二級緩存(如用户表、訂單表),否則會頻繁觸發緩存失效,反而降低性能;
- 多表關聯查詢不建議使用二級緩存(如
select * from user u join order o on u.id = o.user_id),因為關聯表的增刪改操作無法觸發當前 Mapper 的緩存清空,會導致數據不一致; - 實體類必須實現
Serializable接口(二級緩存/第三方緩存均需序列化); - 分佈式場景下,必須使用第三方緩存(如 Redis),默認二級緩存無法跨服務共享;
- 避免緩存穿透:對查詢結果為
null的數據也存入緩存(設置短期過期),防止惡意查詢不存在的 ID 擊垮數據庫; - 避免緩存擊穿:熱點數據設置永不過期,或通過互斥鎖(如 Redis 的
setnx)控制併發查詢。
3. 性能優化建議
- 合理設置二級緩存的過期時間(
flushInterval):根據數據更新頻率調整,如字典表可設置 1 小時,新聞列表可設置 5 分鐘; - 限制緩存條目數(
size):避免緩存過大導致內存溢出,建議設置為 1024~4096; - 開啓緩存只讀模式(
readOnly="true"):如果查詢結果無需修改,開啓只讀模式可提升性能(避免對象拷貝); - 結合 MyBatis 分頁插件(如 PageHelper):分頁查詢的緩存 Key 會包含分頁參數,避免緩存混淆;
- 監控緩存命中率:通過自定義緩存統計命中次數,優化緩存策略(如命中率低於 30%,建議關閉二級緩存)。
七、面試核心考點總結
- 緩存分層:一級緩存(SqlSession 級,默認開啓)和二級緩存(Mapper 級,默認關閉)的區別;
- 失效機制:增刪改操作會觸發緩存清空,跨 SqlSession 僅二級緩存可共享;
- 二級緩存配置:全局開關
cacheEnabled+ Mapper 標籤<cache>+ 實體類Serializable; - 自定義緩存:實現
Cache接口,集成 Redis 等第三方緩存的步驟; - 最佳實踐:哪些場景適合用二級緩存,哪些場景禁用,如何避免數據不一致。
MyBatis 緩存機制的核心是“平衡性能與數據一致性”,實際開發中需根據業務場景選擇合適的緩存策略,避免盲目開啓二級緩存導致的問題。