
1. 項目概述為什么要在SpringBoot中開啟MyBatis-Plus二級緩存在任何一個有一定用戶量的Web應用中數據庫查詢往往是性能瓶頸最集中的地方。我們經常遇到這樣的場景一個首頁的渲染需要關聯查詢用戶信息、文章列表、推薦內容等這些查詢在每次請求時都重復執行即使數據在短時間內根本沒有變化。對于讀多寫少的業務這種重復的、無意義的數據庫I/O操作不僅消耗了寶貴的數據庫連接資源也直接拉長了接口的響應時間。MyBatis-Plus作為MyBatis的增強工具其內置的二級緩存功能就是為了解決這類“重復查詢”問題而生的。它允許我們將查詢結果緩存到應用進程的內存或集成的第三方緩存如Redis中當后續完全相同的查詢命中時直接返回緩存的結果完全繞過數據庫。這聽起來像是一個“銀彈”尤其是在使用SpringBoot快速構建應用時開啟它似乎不費吹灰之力。然而在實際的生產環境中我見過太多團隊因為盲目或不當使用二級緩存而踩坑。數據不一致、內存溢出、緩存雪崩等問題層出不窮輕則導致功能異常重則引發線上事故。所以今天我們不只談“如何開啟”更要深入探討“開啟后帶來的問題”以及“如何安全、有效地使用它”。這篇文章將基于我處理過的多個中大型項目的緩存實踐為你拆解MyBatis-Plus二級緩存的機制、配置細節以及那些你必須提前知曉的“坑”。2. MyBatis-Plus二級緩存的核心機制與配置詳解要駕馭一個工具必須先理解它的工作原理。MyBatis-Plus的二級緩存并非其獨創它繼承并增強了MyBatis原生的二級緩存機制。理解以下幾個核心概念是后續一切操作和問題排查的基礎。2.1 緩存的作用域與生命周期首先我們需要區分一級緩存和二級緩存。一級緩存SqlSession級別默認開啟。它的作用域是一個數據庫會話SqlSession。在同一個SqlSession中執行兩次相同的SQL查詢第二次會直接使用緩存。一旦執行了增、刪、改操作或者調用了sqlSession.clearCache()或者關閉了SqlSession這個緩存就會失效。它的生命周期太短對于Web應用通常每個請求一個SqlSession來說意義不大。二級緩存Mapper級別/Namespace級別我們需要手動開啟。它的作用域是一個Mapper命名空間Namespace。所有在這個Mapper中執行的查詢只要緩存條件匹配都可以共享結果。它的生命周期與整個應用進程綁定如果使用進程內緩存或者與配置的緩存服務器如Redis的生命周期綁定。這才是我們提升性能所關注的重點。MyBatis-Plus二級緩存的核心思想是以Mapper為單位將查詢結果對象序列化后存儲起來。當同一個Mapper內執行完全相同的SQL包括SQL語句和參數時優先從緩存中獲取結果。2.2 在SpringBoot中開啟二級緩存的完整步驟假設我們有一個SpringBoot 2.x MyBatis-Plus 3.x的項目。以下是開啟并配置二級緩存的詳細流程我會解釋每一步的意圖。第一步在application.yml中開啟全局緩存配置mybatis-plus: configuration: # 開啟二級緩存這是總開關 cache-enabled: true這個配置對應MyBatis原生配置中的cacheEnabled設置為true僅僅表示“允許”每個Mapper使用二級緩存但具體哪個Mapper用還需要在Mapper接口上聲明。第二步在目標Mapper接口上添加CacheNamespace注解這是最關鍵的一步。你需要在希望啟用二級緩存的Mapper接口上打上這個注解。import org.apache.ibatis.annotations.CacheNamespace; import com.baomidou.mybatisplus.core.mapper.BaseMapper; CacheNamespace // 關鍵注解表明此Mapper啟用二級緩存 public interface UserMapper extends BaseMapperUser { // 你的自定義方法 }CacheNamespace注解告訴MyBatis-Plus這個Mapper的所有查詢操作除非被單獨設置不使用緩存的結果都應該被緩存。這里有一個常見的誤解以為在application.yml里開了就行忘了加這個注解結果緩存根本沒生效排查半天。第三步可選但推薦配置緩存實現MyBatis默認使用一個簡單的PerpetualCache永久緩存實現它就是一個HashMap存在于JVM堆內存中。在生產環境這通常不夠用。我們可以集成更專業的緩存比如Ehcache、Redis等。以集成Redis為例首先需要引入依賴這里以Spring Boot Data Redis為例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency然后你需要實現MyBatis的Cache接口創建一個RedisCache類。這個過程稍顯復雜需要處理序列化、過期策略、鍵的生成規則等。不過MyBatis-Plus社區或一些開源項目通常有現成的實現可供參考。集成后在CacheNamespace注解中指定實現類CacheNamespace(implementation com.yourpackage.RedisCache.class) public interface UserMapper extends BaseMapperUser { }使用Redis作為緩存后端好處是解決了應用重啟緩存丟失、多實例應用緩存共享的問題但引入了網絡開銷和Redis的運維復雜度。第四步理解并配置序列化二級緩存存儲的是查詢結果映射后的Java對象。這些對象需要被序列化才能存儲無論是內存還是Redis。MyBatis默認使用JDK序列化效率低且兼容性可能有問題。如果你使用默認緩存確保你的實體類實現了Serializable接口。如果使用其他緩存如Redis你通常需要配置更高效的序列化器如Jackson2JsonRedisSerializer。完成以上步驟二級緩存就基本開啟了。你可以寫一個單元測試連續調用兩次同一個查詢方法在日志中設置mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl觀察第二次查詢是否沒有打印SQL日志來驗證緩存是否生效。3. 二級緩存帶來的四大核心問題與深度剖析開啟了緩存性能測試時可能看到驚人的提升但千萬別高興太早。以下是我在實踐中總結的四個最具代表性的問題每一個都可能讓你在深夜接到報警電話。3.1 數據一致性問題臟讀的幽靈這是二級緩存最致命、也最常見的問題。緩存的核心矛盾在于它存儲的是某個時間點的數據快照而數據庫中的數據是隨時可能變化的。問題場景服務A通過UserMapper查詢id1的用戶結果被緩存。服務B甚至是同一個服務的另一個線程通過UserMapper更新了id1的用戶信息并成功提交事務。服務A再次查詢id1的用戶由于緩存未失效它讀到的是舊的、臟的數據。根因分析 MyBatis的二級緩存失效機制默認是基于Mapper命名空間的。也就是說當在UserMapper上執行了一個updateById操作MyBatis會使整個UserMapper的緩存全部清空。這聽起來很粗暴但至少能保證一致性對吧問題出在“事務”上。在Spring管理的事務中緩存的清空動作發生在事務提交之后??紤]這個場景Transactional public void updateUser() { // 1. 查詢用戶結果放入緩存 User user userMapper.selectById(1L); // 2. 修改用戶 user.setName(NewName); // 3. 更新數據庫此時事務未提交 userMapper.updateById(user); // 4. 在同一個方法內再次查詢 User cachedUser userMapper.selectById(1L); // 問題cachedUser.getName() 很可能還是舊值 }為什么因為第3步的update操作雖然執行了但事務還沒提交數據庫里的數據還沒變MyBatis可能還不會立即清緩存。更關鍵的是第4步的查詢如果命中了一級緩存SqlSession級別它根本不會走到二級緩存那一步直接返回了舊對象。即使沒有一級緩存在事務提交前二級緩存也可能未被清除。解決方案與心得設置CacheNamespace(flushInterval時間)為緩存設置一個自動刷新間隔毫秒例如flushInterval600001分鐘。這屬于妥協方案數據會有最多1分鐘的不一致適用于對實時性要求不高的數據。在涉及更新的業務方法上手動清除緩存在updateUser方法最后顯式調用SqlSession的clearCache()方法或者如果你使用了自定義的Cache實現直接調用其清除方法。這要求你對緩存有更強的控制力。使用更細粒度的緩存策略放棄Mapper級別的緩存使用諸如Spring CacheCacheable,CacheEvict這樣的注解在方法級別上控制緩存的讀和寫。你可以更精確地指定當更新用戶時只清除“user::1”這個鍵的緩存而不是所有用戶緩存。這是目前更主流、更推薦的做法。MyBatis-Plus的二級緩存更像是一個“基礎設施開關”而Spring Cache是更上層的“業務緩存抽象”。心理建設對于強一致性要求極高的數據如賬戶余額、庫存直接放棄查詢緩存老老實實讀數據庫?;蛘卟捎谩跋雀聰祿煸賱h除緩存”的Cache-Aside模式并處理好緩存刪除失敗的重試機制。3.2 緩存序列化與對象關聯的陷阱當你使用默認的進程內緩存時一切似乎風平浪靜。一旦你開始使用分布式緩存如Redis或者你的實體對象存在復雜的關聯關系坑就來了。問題場景 你的User對象里有一個ListOrder屬性通過TableField(exist false)標注并在服務層手動查詢填充。當你將User對象緩存到Redis后下次反序列化出來時這個ListOrder可能會丟失如果未正確序列化或者更糟糕地你緩存了一個巨大的、不斷增長的關聯對象集合導致緩存體積爆炸。根因分析序列化兼容性JDK序列化對類版本serialVersionUID極其敏感。如果你修改了實體類結構而沒有更新UID反序列化會失敗。Jackson等JSON序列化器可能不處理transient字段以外的循環引用導致棧溢出?!芭謱ο蟆本彺婺銦o意中緩存了一個包含大量懶加載代理如Hibernate Proxy或巨大集合的對象。當這個對象被序列化時可能會觸發整個對象圖的加載性能災難就此發生。解決方案與心得緩存“瘦”對象最好是DTO堅決不要緩存帶有TableField(exist false)的關聯屬性。最佳實踐是為緩存專門設計一個UserCacheDTO只包含需要緩存的核心字段id, name, avatar等。在查詢后將User對象轉換為UserCacheDTO再進行緩存。這保證了緩存內容的精簡和穩定。選擇高效的序列化方案放棄JDK序列化。使用Kryo、FST或Jackson JSON。在Redis中Jackson2JsonRedisSerializer是不錯的選擇但要注意配置ObjectMapper忽略循環引用和transient字段。仔細檢查實體類確保所有不需要或不能序列化的字段如HttpSession、數據庫連接等標記為transient或者使用JsonIgnore注解如果你用JSON序列化。3.3 緩存穿透、雪崩與擊穿這三個是分布式緩存的經典問題在使用MyBatis-Plus二級緩存并搭配Redis時同樣會遇到。緩存穿透查詢一個數據庫中根本不存在的數據。請求會穿過緩存直接訪問數據庫。如果被惡意攻擊大量請求查詢不存在的ID數據庫可能被壓垮。應對將“空結果”也進行緩存但設置一個較短的過期時間如30秒。可以使用一個特殊的標記值如“##NULL##”來表示。緩存雪崩設置緩存時采用了相同的過期時間導致在某一時刻大量緩存同時失效所有請求涌向數據庫。應對為緩存數據設置一個隨機的過期時間偏移量例如基礎過期時間(-5~5分鐘的隨機數)讓緩存失效時間點分散開。緩存擊穿某個熱點key如首頁頭條新聞在失效的瞬間有大量并發請求同時到來未命中緩存全部去查詢數據庫。應對使用互斥鎖Mutex Lock。在緩存失效時不是所有線程都去查庫而是讓一個線程去查其他線程等待查完后寫入緩存其他線程再從緩存讀取。在Java中可以用synchronized關鍵字或ReentrantLock在應用層實現更優雅的方式是使用Redis的SETNX命令實現分布式鎖。注意MyBatis-Plus原生的二級緩存開箱即用功能并沒有內置這些高級防護機制。如果你直接使用其默認實現就需要自己在業務代碼或自定義的Cache實現類中加入這些邏輯。這再次說明了對于復雜的生產環境直接使用Spring Cache等更高級的抽象或者直接操作Redis Template往往比使用MyBatis-Plus的二級緩存更可控。3.4 多表關聯查詢與緩存作用域混淆這是一個非常隱蔽的問題。假設你有UserMapper和OrderMapper并且有一個查詢需要關聯用戶和訂單。問題場景 你在UserMapper.xml中寫了一個復雜的select通過join語句同時查詢出了User和Order的數據。這個查詢結果被緩存在UserMapper的命名空間下。后來你通過OrderMapper更新了某個訂單的狀態。但是OrderMapper的更新操作只會清空OrderMapper命名空間下的緩存而不會觸碰到UserMapper的緩存。導致UserMapper中那個關聯查詢的結果仍然包含著舊的訂單狀態。根因分析 MyBatis的二級緩存是命名空間隔離的它沒有跨命名空間的緩存依賴感知能力。一個Mapper無法知道自己的數據被其他Mapper的查詢所引用。解決方案與心得避免在查詢層做多表關聯這是最根本的解決方案。遵循“領域驅動”或“簡潔架構”的思想在Service層分別調用UserMapper和OrderMapper進行單表查詢然后在內存中進行數據組裝俗稱“拼裝”。這樣緩存是單表的更新訂單只會清除訂單緩存用戶緩存不受影響雖然可能有一致性延遲但邊界清晰問題更容易追蹤。使用cache-refMyBatis提供了cache-ref namespace.../標簽可以讓一個Mapper引用另一個Mapper的緩存。這樣UserMapper和OrderMapper就共享了同一個緩存實例對任何一個Mapper的更新都會清空共享緩存。但這相當于回到了粗粒度的緩存清除可能誤傷過多。放棄使用二級緩存處理復雜關聯明確二級緩存的定位——它最適合緩存變化不頻繁的、單表的主鍵查詢或簡單條件查詢。對于復雜的、涉及多表關聯的業務查詢應該使用專門的緩存策略如Spring Cache或者不緩存。4. 生產環境下的最佳實踐與決策指南經過以上問題的剖析你可能會覺得二級緩存“危如累卵”。別擔心任何技術都有其適用場景。下面是我的經驗總結告訴你什么時候該用該怎么用。4.1 何時應該考慮開啟二級緩存數據字典/配置類數據例如國家城市列表、系統參數配置等幾乎從不更新但被頻繁查詢。用戶基礎信息如用戶頭像、昵稱等更新頻率較低一天幾次但讀取頻率極高。熱點文章/商品詳情在活動期間某些熱點內容被海量讀取且內容在活動期間固定。復雜的統計報表查詢耗時極長如幾分鐘且數據允許有一定的延遲如T1的報表。核心判斷原則讀遠大于寫且對數據一致性要求不是實時強一致。4.2 更推薦的替代方案Spring Cache 自定義緩存邏輯對于大多數SpringBoot項目我個人的建議是謹慎使用MyBatis-Plus自帶的二級緩存優先考慮使用Spring Cache。為什么關注點分離MyBatis-Plus的職責是數據訪問層DAO的增強。而緩存更多是一種業務層或服務層的優化策略。使用Spring CacheCacheable,CacheEvict,CachePut你可以將緩存規則聲明在Service方法上代碼更清晰職責更明確。更精細的控制你可以輕松指定緩存的key支持SpEL表達式可以按條件緩存conditionunless可以在更新時只清除特定的keyCacheEvict(key “‘user::’ #id”)而不是清空整個Mapper的緩存。更好的集成Spring Cache抽象了緩存提供商可以無縫在Ehcache、Caffeine、Redis等之間切換配置更統一。示例Service public class UserServiceImpl implements UserService { Autowired private UserMapper userMapper; Override Cacheable(value “userCache”, key “‘user::’ #id”) public User getUserById(Long id) { // 這里調用的是你的Mapper方法 return userMapper.selectById(id); } Override CacheEvict(value “userCache”, key “‘user::’ #id”) public void updateUser(User user) { userMapper.updateById(user); } }這樣緩存的控制權完全在你手中避免了MyBatis二級緩存那些隱晦的、基于命名空間的行為。4.3 如果決定使用必須做的監控與保障如果你因為歷史原因或特定場景必須使用MyBatis-Plus二級緩存請務必做好以下監控監控緩存命中率在自定義的Cache實現中加入計數邏輯統計getObject的調用次數和命中次數。過低的命中率如低于70%意味著緩存策略可能有問題或者數據變化太頻繁不適合緩存。監控緩存大小如果是本地緩存定期通過JMX或監控工具查看緩存占用的堆內存大小防止內存泄漏或OOM。如果是Redis監控其內存使用量。建立緩存的降級開關在應用配置中心如Nacos、Apollo配置一個開關可以在出現緩存問題如數據大面積不一致時一鍵關閉所有緩存讓系統回退到直接訪問數據庫的狀態。這是一個非常重要的運維保障手段。關鍵業務的數據一致性校驗對于特別重要的數據可以定期運行一個離線任務對比緩存中的數據與數據庫中最新的數據并報告差異。這能幫你提前發現緩存同步機制的問題。開啟MyBatis-Plus二級緩存就像給數據庫查詢加裝了一個渦輪增壓器能在特定路況下爆發出強勁動力。但它也需要更精密的調校和更謹慎的駕駛習慣。理解其內部機制預見其潛在問題并準備好應對方案你才能穩穩地享受它帶來的性能紅利而不是被它拖入故障的泥潭。在架構選型上多想一想“我們真的需要這個級別的緩存嗎”、“有沒有更簡單可控的方案”往往比盲目追求技術特性更為重要。