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