MyBatis 緩存機制詳解(一級緩存+二級緩存+自定義緩存)

MyBatis 的緩存機制是其核心性能優化手段之一,目的是減少數據庫查詢次數,降低IO開銷,提升查詢效率。其設計遵循“分層緩存”理念,分為 一級緩存(SqlSession 級別)二級緩存(Mapper 級別),同時支持集成第三方緩存(如 Redis)實現分佈式場景下的緩存共享。

一、緩存核心設計理念

  1. 緩存粒度:從“會話級”到“跨會話級”,逐步擴大緩存作用範圍;
  2. 失效策略:基於“數據一致性”優先,增刪改操作會觸發對應緩存失效;
  3. 存儲介質:默認基於內存(HashMap),支持自定義擴展(如磁盤、分佈式緩存);
  4. 命中規則:緩存 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)失效場景(重點面試考點)
  1. 執行 sqlSession.close():關閉 SqlSession 時,一級緩存會被銷燬;
  2. 執行增刪改操作(insert()/update()/delete()):MyBatis 會自動清除當前 SqlSession 內的所有一級緩存(避免數據不一致);
  3. 手動清除緩存:調用 sqlSession.clearCache() 方法,主動清空當前 SqlSession 的緩存;
  4. 跨 SqlSession 查詢:不同 SqlSession 之間的緩存相互獨立,無法共享;
  5. 查詢參數/方法不同:即使是同一 Mapper 方法,參數不同或方法名不同,也會生成不同的緩存 Key;
  6. 開啓全局二級緩存:一級緩存依然生效,但查詢結果會同步寫入二級緩存(不影響一級緩存命中)。

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)關鍵配置參數説明

參數名

取值範圍

作用説明

eviction

LRU(默認)/FIFO/SOFT/WEAK

緩存回收策略:

- LRU:最近最少使用(移除最長時間未被訪問的緩存);

- FIFO:先進先出(按緩存添加順序移除);

- SOFT:軟引用(JVM內存不足時移除);

- WEAK:弱引用(垃圾回收時直接移除)。

flushInterval

數值(毫秒)

緩存自動過期時間,默認無過期(一直有效)。

size

正整數

緩存最大存儲條目數,超出後按回收策略移除舊緩存(避免內存溢出)。

readOnly

true/false(默認)

- true:緩存返回對象本身(性能高,但線程不安全,需確保對象不被修改);

- false:緩存返回對象的拷貝(通過序列化,線程安全,性能略低)。

blocking

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)失效場景(重點面試考點)
  1. 執行增刪改操作:當前 Mapper 下的任何 insert()/update()/delete() 操作(無論是否修改目標數據),都會清空該 Mapper 的所有二級緩存;
  2. 手動清空緩存:調用 sqlSession.clearCache()(僅清空當前 SqlSession 的一級緩存)或 sqlSessionFactory.getConfiguration().getCache("namespace").clear()(清空指定 Mapper 的二級緩存);
  3. 未開啓二級緩存:全局開關 cacheEnabled="false" 或 Mapper 未配置 <cache> 標籤;
  4. 查詢方法禁用二級緩存:select 標籤設置 useCache="false"(如實時性要求高的查詢);
  5. 增刪改方法未觸發緩存清空:insert/update/delete 標籤設置 flushCache="false"(不推薦,會導致數據不一致);
  6. 實體類未實現 Serializable 接口:緩存寫入失敗,無法命中;
  7. 跨 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 的默認二級緩存是內存級緩存,存在以下侷限性:

  • 不支持分佈式場景(多服務實例無法共享緩存);
  • 內存有限,無法存儲大量數據;
  • 服務重啓後緩存丟失。

因此,實際開發中常集成 RedisEhcache 等第三方緩存替代默認二級緩存,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%,建議關閉二級緩存)。

七、面試核心考點總結

  1. 緩存分層:一級緩存(SqlSession 級,默認開啓)和二級緩存(Mapper 級,默認關閉)的區別;
  2. 失效機制:增刪改操作會觸發緩存清空,跨 SqlSession 僅二級緩存可共享;
  3. 二級緩存配置:全局開關 cacheEnabled + Mapper 標籤 <cache> + 實體類 Serializable
  4. 自定義緩存:實現 Cache 接口,集成 Redis 等第三方緩存的步驟;
  5. 最佳實踐:哪些場景適合用二級緩存,哪些場景禁用,如何避免數據不一致。

MyBatis 緩存機制的核心是“平衡性能與數據一致性”,實際開發中需根據業務場景選擇合適的緩存策略,避免盲目開啓二級緩存導致的問題。