
1. MyBatis緩存機制概述作為Java開發者MyBatis的緩存機制是我們日常開發中必須掌握的核心知識點。緩存機制的設計初衷是為了減少數據庫訪問次數提升系統性能。但在實際應用中如果對緩存機制理解不透徹很容易引發各種問題比如臟數據、緩存穿透等。MyBatis提供了一級緩存和二級緩存兩種機制一級緩存是SqlSession級別的緩存默認開啟二級緩存是Mapper級別的緩存需要手動配置開啟2. 一級緩存深度解析2.1 一級緩存工作機制一級緩存是MyBatis默認開啟的緩存機制它的作用范圍僅限于單個SqlSession內部。當同一個SqlSession中執行相同的SQL查詢時MyBatis會優先從緩存中獲取結果而不是直接訪問數據庫。一級緩存的工作流程如下執行查詢時MyBatis會根據SQL語句、參數、分頁等信息生成CacheKey先在本地緩存(LocalCache)中查找是否有對應的緩存結果如果命中緩存直接返回結果如果未命中則查詢數據庫并將結果存入緩存2.2 一級緩存配置與實驗一級緩存有兩個配置級別SESSION(默認)同一個SqlSession內的所有查詢共享緩存STATEMENT僅對當前Statement有效setting namelocalCacheScope valueSESSION/通過實驗可以驗證一級緩存的特性Test public void testLocalCache() { SqlSession sqlSession sqlSessionFactory.openSession(); try { UserMapper mapper sqlSession.getMapper(UserMapper.class); // 第一次查詢會訪問數據庫 User user1 mapper.selectById(1); // 第二次查詢直接從緩存獲取 User user2 mapper.selectById(1); // user1 user2 為true } finally { sqlSession.close(); } }2.3 一級緩存失效場景以下操作會導致一級緩存失效執行insert/update/delete操作調用SqlSession的clearCache()方法配置了localCacheScopeSTATEMENT手動修改了數據庫數據(其他SqlSession或外部程序修改)注意在分布式環境下一級緩存容易引發臟讀問題因為不同節點的SqlSession無法感知彼此的修改。3. 二級緩存全面剖析3.1 二級緩存工作機制二級緩存是Mapper級別的緩存多個SqlSession可以共享同一個Mapper的二級緩存。二級緩存的工作流程如下查詢時先檢查二級緩存如果命中則直接返回未命中則查詢數據庫并將結果存入二級緩存執行更新操作時清空對應的二級緩存3.2 二級緩存配置與使用啟用二級緩存需要兩步配置全局配置中開啟二級緩存setting namecacheEnabled valuetrue/在Mapper XML中配置緩存策略cache evictionLRU flushInterval60000 size512 readOnlytrue/3.3 二級緩存實驗驗證通過實驗可以驗證二級緩存的特性Test public void testSecondLevelCache() { SqlSession sqlSession1 sqlSessionFactory.openSession(); SqlSession sqlSession2 sqlSessionFactory.openSession(); try { UserMapper mapper1 sqlSession1.getMapper(UserMapper.class); UserMapper mapper2 sqlSession2.getMapper(UserMapper.class); // 第一次查詢訪問數據庫 User user1 mapper1.selectById(1); sqlSession1.commit(); // 必須提交才會放入二級緩存 // 第二次查詢從二級緩存獲取 User user2 mapper2.selectById(1); // user1 user2 為false因為二級緩存默認是序列化存儲 } finally { sqlSession1.close(); sqlSession2.close(); } }3.4 二級緩存的局限性二級緩存存在以下問題需要注意多表關聯查詢時容易出現臟數據分布式環境下需要額外處理緩存同步緩存策略配置不當可能導致內存溢出事務提交后才會更新緩存4. 緩存源碼深度分析4.1 一級緩存源碼實現一級緩存的核心實現位于BaseExecutor類中public abstract class BaseExecutor implements Executor { protected PerpetualCache localCache; public E ListE query(...) { // 生成CacheKey CacheKey key createCacheKey(...); // 嘗試從緩存獲取 list resultHandler null ? (ListE) localCache.getObject(key) : null; if (list null) { // 查詢數據庫 list queryFromDatabase(...); // 放入緩存 localCache.putObject(key, list); } return list; } }CacheKey的生成邏輯考慮了以下因素MappedStatement的ID分頁參數(offset, limit)SQL語句本身參數值環境ID4.2 二級緩存源碼實現二級緩存通過裝飾器模式實現核心類為CachingExecutorpublic class CachingExecutor implements Executor { private final Executor delegate; private final TransactionalCacheManager tcm new TransactionalCacheManager(); public E ListE query(...) { // 獲取MappedStatement對應的Cache Cache cache ms.getCache(); if (cache ! null) { // 檢查是否需要刷新緩存 flushCacheIfRequired(ms); if (ms.isUseCache() resultHandler null) { // 從二級緩存獲取 list (ListE) tcm.getObject(cache, key); if (list null) { // 委托給底層Executor查詢 list delegate.query(...); // 放入二級緩存 tcm.putObject(cache, key, list); } } } return list; } }5. 生產環境緩存最佳實踐5.1 緩存使用建議一級緩存在單機環境下可以保留默認配置分布式環境下建議設置為STATEMENT級別對于讀寫頻繁的場景考慮手動清空緩存二級緩存對于讀多寫少的場景可以考慮使用需要實現Serializable接口建議設置合理的回收策略和大小限制5.2 常見問題解決方案臟數據問題多表關聯查詢避免使用二級緩存可以考慮使用cache-ref建立緩存引用關系或者手動控制緩存清除緩存穿透對空結果也進行緩存使用布隆過濾器預先判斷緩存雪崩設置不同的過期時間使用多級緩存策略5.3 性能優化技巧選擇合適的回收策略LRU(最近最少使用)FIFO(先進先出)SOFT(軟引用)WEAK(弱引用)批量操作優化// 批量插入優化示例 insert idbatchInsert parameterTypejava.util.List INSERT INTO user(name,age) VALUES foreach collectionlist itemitem separator, (#{item.name},#{item.age}) /foreach /insert局部緩存控制// 指定方法不使用緩存 select idselectById resultMapuserMap useCachefalse SELECT * FROM user WHERE id #{id} /select // 強制刷新緩存 select idselectAll resultMapuserMap flushCachetrue SELECT * FROM user /select6. 高級應用與擴展6.1 自定義緩存實現MyBatis緩存支持自定義實現只需要實現Cache接口public class RedisCache implements Cache { private final ReadWriteLock readWriteLock new ReentrantReadWriteLock(); private final String id; private final RedisTemplateString, Object redisTemplate; public RedisCache(String id) { this.id id; // 初始化RedisTemplate } public void putObject(Object key, Object value) { redisTemplate.opsForValue().set(key.toString(), value); } public Object getObject(Object key) { return redisTemplate.opsForValue().get(key.toString()); } // 實現其他方法... }配置使用自定義緩存cache typecom.example.RedisCache/6.2 整合第三方緩存整合Ehcachedependency groupIdorg.mybatis.caches/groupId artifactIdmybatis-ehcache/artifactId version1.2.1/version /dependency cache typeorg.mybatis.caches.ehcache.EhcacheCache/整合Redisdependency groupIdorg.mybatis.caches/groupId artifactIdmybatis-redis/artifactId version1.0.0-beta2/version /dependency cache typeorg.mybatis.caches.redis.RedisCache/6.3 多級緩存架構對于高性能場景可以考慮多級緩存架構一級緩存SqlSession級別二級緩存Mapper級別分布式緩存Redis等本地緩存Caffeine等實現思路自定義Cache實現采用裝飾器模式組合多種緩存設置合理的緩存過期策略實現緩存同步機制7. 常見面試問題解析7.1 基礎問題MyBatis的一級緩存和二級緩存有什么區別作用范圍不同一級緩存是SqlSession級別二級緩存是Mapper級別配置方式不同一級緩存默認開啟二級緩存需要顯式配置存儲結構不同一級緩存使用HashMap二級緩存可配置多種實現什么情況下會導致MyBatis緩存失效執行insert/update/delete操作調用clearCache方法配置了flushCachetrue事務提交(二級緩存)緩存達到上限或過期7.2 進階問題MyBatis緩存是如何解決并發問題的一級緩存SqlSession是線程獨享的天然避免并發問題二級緩存通過SynchronizedCache裝飾器實現線程安全事務緩存通過TransactionalCache管理事務期間的緩存操作如何設計一個高性能的MyBatis緩存方案根據業務特點選擇合適的緩存粒度讀多寫少場景使用二級緩存寫多讀少場景考慮禁用或縮短緩存時間分布式環境使用集中式緩存對熱點數據實現多級緩存7.3 實戰問題如何處理多表關聯查詢的緩存問題方案1避免在關聯查詢上使用二級緩存方案2使用cache-ref建立緩存關聯方案3手動控制相關緩存的清除方案4將關聯查詢拆分為多個單表查詢MyBatis緩存與Spring Cache如何選擇MyBatis緩存ORM層緩存自動管理粒度較細Spring Cache業務層緩存更靈活支持多種實現最佳實踐MyBatis緩存處理數據庫訪問優化Spring Cache處理業務邏輯優化8. 個人實踐經驗分享在實際項目中使用MyBatis緩存時我總結了以下幾點經驗緩存監控很重要需要實時監控緩存命中率、內存占用等指標我們使用PrometheusGrafana搭建了監控系統。緩存key設計對于復雜查詢需要精心設計cacheKey避免因為參數順序不同導致緩存未命中。批量操作優化對于批量插入場景我們重寫了Executor實現了批量操作的緩存優化。動態緩存配置根據系統負載動態調整緩存策略高峰期放寬緩存限制低谷期收緊。故障處理建立緩存降級機制當緩存出現問題時可以快速切換為直接訪問數據庫。一個實用的工具類示例public class CacheHelper { public static void clearCache(SqlSessionFactory sqlSessionFactory, Class? mapperClass) { try (SqlSession sqlSession sqlSessionFactory.openSession()) { Configuration config sqlSession.getConfiguration(); Cache cache config.getCache(mapperClass.getName()); if (cache ! null) { cache.clear(); } } } public static void clearAllCache(SqlSessionFactory sqlSessionFactory) { try (SqlSession sqlSession sqlSessionFactory.openSession()) { sqlSession.clearCache(); Configuration config sqlSession.getConfiguration(); for (String name : config.getCacheNames()) { config.getCache(name).clear(); } } } }最后關于是否在生產環境使用MyBatis緩存我的建議是對于簡單的單表CRUD可以使用一級緩存對于復雜的查詢場景建議使用專門的緩存解決方案如Redis使用緩存前一定要做好充分的測試和監控