統一緩存帝國 - 實戰 Spring Cache

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f9/f918988d09de7224ff57b675c0719292.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前三篇講解的緩存使用和分佈式鎖的都是基於 Redis 來做緩存的,本篇我來向大家介紹一種兼容所有緩存中間件的方案,不論我們是使用 Redis 還是 Ehcache,都不需要關心如何操作 Redis 或者 Ehcache,這套方案統統幫你搞定。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這套方案就是大名鼎鼎的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Spring Cache","attrs":{}}],"attrs":{}},{"type":"text","text":"。什麼?你沒有聽過,沒關係,本篇帶你一起探索。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"緩存系列文章:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"","title":"","type":null},"content":[{"type":"text","text":"《緩存實戰(一)》","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"","title":"","type":null},"content":[{"type":"text","text":"《緩存實戰(二)Redis分佈式鎖》","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"","title":"","type":null},"content":[{"type":"text","text":"《緩存實戰(三)Redisson 分佈式鎖》","attrs":{}}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、揭開 Spring Cache 的面紗","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1 現有緩存方案的痛點","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"試想一種場景","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.用戶 A 打開 APP,進入到了秒殺商品的詳情頁,那這個商品數據我們會先去數據庫查詢,然後返回給客戶端。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.因爲有大量用戶短時間內進入到了詳情頁,所以可以把活動列表緩存起來,直接讀緩存就可以了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.那下次再查詢商品時,直接去緩存查詢就可以了。如果秒殺商品下架了,緩存的數據不會用到了,就把緩存刪掉就可以了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.上面幾步看起來也沒啥問題,但是放緩存,刪除緩存這兩步是需要我們去手動寫代碼實現的。有沒有一種方式不用寫操作緩存的代碼?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"5.假如現在用的緩存中間件是 Redis,領導說要換成 Ehcache,操作緩存的代碼是不是又得重新擼一遍?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"總結下上面場景的痛點:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需要手寫操作緩存代碼,如添加緩存、更新緩存、刪除緩存。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"切換緩存組件並不容易,或者說沒有對緩存層進行抽象封裝,依賴具體的緩存中間件。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"哪有沒有一種方案可以幫助解決上面的兩個痛點呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這就是今天要介紹的 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"Spring Cache","attrs":{}}],"attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2 Spring Cache 介紹","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spring Cache 是 Spring 提供的一整套的緩存解決方案。雖然它本身並沒有提供緩存的實現,但是它提供了一整套的接口和代碼規範、配置、註解等,這樣它就可以整合各種緩存方案了,比如 Redis、Ehcache,我們也就不用關心操作緩存的細節。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spring 3.1 開始定義了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口來統一不同的緩存技術,並支持使用註解來簡化我們開發。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"Cache","attrs":{}}],"attrs":{}},{"type":"text","text":" 接口它包含了緩存的各種操作方式,同時還提供了各種","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"xxxCache","attrs":{}}],"attrs":{}},{"type":"text","text":"緩存的實現,比如 RedisCache 針對Redis,EhCacheCache 針對 EhCache,ConcurrentMapCache 針對 ConCurrentMap,具體有哪幾種,後面實戰中會介紹。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.3 Spring Cache 有什麼功效","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每次調用某方法,而此方法又是帶有緩存功能時,Spring 框架就會檢查","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"指定參數","attrs":{}}],"attrs":{}},{"type":"text","text":"的那個方法是否已經被調用過,如果之前調用過,就從緩存中取之前調用的結果;如果沒有調用過,則再調用一次這個方法,並緩存結果,然後再返回結果,那下次調用這個方法時,就可以直接從緩存中獲取結果了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.4 Spring Cache 的原理是什麼?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spring Cache 主要是作用在類上或者方法上,對類中的方法的返回結果進行緩存。那麼如何對方法增強,來實現緩存的功能?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"學過 Spring 的同學,肯定能一下子就反應過來,就是用 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"AOP","attrs":{}}],"attrs":{}},{"type":"text","text":"(面向切面編程)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"面向切面編程可以簡單地理解爲在類上或者方法前加一些說明,就是我們常說的註解。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spring Cache 的註解會幫忙在方法上創建一個切面(aspect),並觸發緩存註解的切點(poinitcut),聽起來太繞了,簡單點說就是:Spring Cache 的註解會幫忙在調用方法之後,去緩存","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"方法調用的最終結果","attrs":{}},{"type":"text","text":",或者在方法調用之前拿緩存中的結果,或者刪除緩存中的結果,這些讀、寫、刪緩存的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"髒活","attrs":{}},{"type":"text","text":"都交給 Spring Cache 來做了,是不是很爽,再也不用自己去寫緩存操作的邏輯了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.5 緩存註解","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spring 提供了四個註解來聲明緩存規則。@Cacheable,@CachePut,@CacheEvict,@Caching。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b6/b62429adfd5a48290f8b3d44aee37a19.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家先有個概念,後面我們再來看怎麼使用這些緩存註解。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、使用緩存","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 引入 Spring Cache 依賴","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 pom 文件中引入 spring cache 依賴,如下所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"xml"},"content":[{"type":"text","text":"\n org.springframework.boot\n spring-boot-starter-cache\n\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 配置使用哪種緩存","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spring Cache 支持很多緩存中間件作爲框架中的緩存,總共有 9 種選擇:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"caffeine:Caffeine 是一種高性能的緩存庫,基於 Google Guava。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"couchbase:","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"CouchBase","attrs":{}},{"type":"text","text":"是一款非關係型JSON文檔數據庫。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"generic:由泛型機制和 static 組合實現的泛型緩存機制。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"hazelcast:一個高度可擴展的數據分發和集羣平臺,可用於實現分佈式數據存儲、數據緩存。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"infinispan:分佈式的集羣緩存系統。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"jcache:JCache 作爲緩存。它是 JSR107 規範中提到的緩存規範。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"none:沒有緩存。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"redis:用 Redis 作爲緩存","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"simple:用內存作爲緩存。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d0/d0c6c0397a6ae4dcc9a2eae99302bc24.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們還是用最熟悉的 Redis 作爲緩存吧。配置 Redis 作爲緩存也很簡單,在配置文件 application.properties 中設置緩存的類型爲 Redis 就可以了, 如下所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6b/6b9de1e856fbd3e3e4a83caf1ae8cc69.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,別忘了還要在 pom 文件中 引入 Redis 的依賴,不然用不了 Redis。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"xml"},"content":[{"type":"text","text":"\n org.springframework.boot\n spring-boot-starter-data-redis\n\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.3 測試緩存","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那基礎的配置已經做好了,現在就是看怎麼使用 Spring Cache 了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(1)啓動類上添加 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"@EnableCaching","attrs":{}}],"attrs":{}},{"type":"text","text":"註解。本文案例就是在 啓動類 PassjavaQuestionApplication 添加 @EnableCaching註解。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c6/c626b90fd87df341f4bff5af6483b055.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(2)指定某方法開啓緩存功能。在方法上添加 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"@Cacheable","attrs":{}}],"attrs":{}},{"type":"text","text":" 緩存註解就可以了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"@Cacheable 註解中,可以添加四種參數:value,key,condition,unless。首先我們來看下 value 參數。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面的代碼出於演示作用,用了最簡單的邏輯,test 方法直接返回一個數字,連數據庫查詢都沒有做。不過沒關係,我們主要驗證 Spring Cache 是否對方法的結果進行了緩存。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@RequestMapping(\"/test\")\n@Cacheable({\"hot\"})\npublic int test() {\n return 222;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家注意,@Cacheable 註解中小括號裏面還含有大括號,大括號裏面還有 “hot” 字符串,這個 hot 字符串你可以把它當作一個緩存的名字,然後將 test 方法返回的結果存到 hot 緩存中。我們也可以用 value=\"hot\" 的方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一次調用 test 方法前,既沒有 hot 緩存,更沒有 test 的結果緩存。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"調用 test 方法後,Redis 中就創建出了 hot 緩存了,然後緩存了一個 key,如下圖所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b3/b331d654454460114636bc56029ccf57.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二次調用 test 方法時,就從緩存 hot 中將 test 方法緩存的結果 222 取出來了,爲了驗證沒有執行 test 中的方法,大家可以在 test 方法中打下 log 或者斷點。最後的驗證結果肯定是沒有走 test 方法的,而是直接從緩存中獲取的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那我們再來測試一個方法,方法名改爲 test2,且請求路徑也改爲 test2 了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@RequestMapping(\"/test2\")\n@Cacheable({\"hot\"})\npublic int test2() {\n return 456;\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"大家覺得這兩個方法的結果都會緩存嗎?還是隻會緩存第一個被調用的方法。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過測試,執行第一個 test 方法後,再執行 test2 方法,緩存結果一直是 222 不會變。因爲他們的 key 都是 默認的 SimpleKey[],所以兩個方法對應的緩存的 key 都叫這個,所以得到的緩存值是一樣的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(3)加上數據庫查詢的測試。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有的同學可能覺得上面的測試太簡單了,test 方法裏面啥都沒做,還緩存啥呢,完全沒必要啊。沒關係,大家的顧慮是對的,我們來加上數據庫查詢,安排~","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先說下場景:前端需要查詢某個題目的詳情,正常邏輯是查詢數據庫後返回結果。假定這個查詢操作非常頻繁,我們需要將題目詳情進行緩存。我們先看看常規 Redis 緩存方案:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先從 Redis 緩存中查看緩存中是否有該題目,如果緩存中有,則返回緩存中的題目;如果沒有,就從數據庫中查。查詢出題目後,就用 Redis 存起來,然後返回。這裏就要寫操作 Redis 的代碼了:查詢 Redis 緩存、更新 Redis 緩存。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// 查詢緩存,假定該題目詳情緩存的 key=question1\nredisTemplate.opsForValue().get(\"question1\"); \n// 更新緩存\nredisTemplate.opsForValue().set(\"question1\", questionEntity);\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那如果用 Spring Cache 註解的話,上面兩行代碼可以直接幹掉了。如下所示,加一個 @Cacheable 註解搞定。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Cacheable({\"question\", \"hot\"})\npublic QuestionEntity info(Long id) {\n return getById(id); // 查詢數據庫操作\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中 question 和 hot 是緩存的名字,我們可以將結果放到不同的緩存中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"結論:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果沒有指定請求參數,則緩存生成的 key name,是默認自動生成的,叫做 SimpleKey[]。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果指定了請求參數,則緩存的 key name 就是請求參數,比如上面 info 方法,key 等於我傳入的 id = 1。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"緩存中 key 對應的 value 默認使用 JDK 序列化後的數據。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"value 的過期時間爲 -1,表示永不過期。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.4 自定義配置類","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面保存的緩存數據都是默認設置,我們也可以自己定義配置,如下所示,在配置文件 application.properties 添加如下配置:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"# 使用 Redis 作爲緩存組件\nspring.cache.type=redis\n# 緩存過期時間爲 3600s\nspring.cache.redis.time-to-live=3600000\n# 緩存的鍵的名字前綴\nspring.cache.redis.key-prefix=passjava_\n# 是否使用緩存前綴\nspring.cache.redis.use-key-prefix=true\n# 是否緩存控制,防止緩存穿透\nspring.cache.redis.cache-null-values=true\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後需要加一個配置類:MyCacheConfig。可以在我的開源項目 passjava 獲取完整源碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {\n}\n","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.5 自定義 key","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然後我們可以指定 key 的值,可以在 @Cacheable 註解裏面加上 key 的值 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"#root.method.name","attrs":{}}],"attrs":{}},{"type":"text","text":"。這是一種特有的表達式,稱作 ","attrs":{}},{"type":"codeinline","content":[{"type":"text","text":"SpEL","attrs":{}}],"attrs":{}},{"type":"text","text":" 表達式,這裏代表用方法名作爲緩存 key 的名字。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Cacheable(value = {\"hot\"}, key = \"#root.method.name\")\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來就是見證奇蹟的時刻,調用 test 方法和 test2 方法,發現有兩個不同的 key,一個是 passjava_test1,另外一個 passjava_test2,它們的 key 就是前綴 passjava_ + 方法名 組成。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b7/b7c03e53ff88246d5c46a6ac4ec1cb6f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SpEL 表達式還有很多其它規則,如下所示:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a4/a4901477e8962b8e2778e7225c3a8ec3.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以根據項目需要選擇合適的表達式來自定義 key。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.6 自定義條件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了設置緩存條目的 key,我們還可以自定義條件來決定是否將緩存功能關閉。這裏就要用到@Cacheable 另外兩個屬性:condition 和 unless,它倆的格式還是用 SpEL 表達式。對應的四個屬性總結如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/93/93e92a0fdddc566724f8e630eed206a8.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代碼示例如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@Cacheable(value = \"hot\", unless = \"#result.message.containss('NoCache')\")\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當放回的結果 message 字段包含有 NoCache 就不會進行緩存。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.7 更新註解","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"@CachePut 也是用來更新緩存,和 @Cacheable 非常相似,不同點是 @CachePut 註解的方法始終都會執行,返回值也會也會放到緩存中。通常用在保存的方法上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"保存成功後,可以將 key 設置保存實例的 id。這個怎麼做呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"之前我們說過 key 可以通過 SpEL 表達式來指定,這裏就可以搭配 #result.id 來實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏還是用個例子來說明用法:創建題目的方法,返回題目實例,其中包含有題目 id。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@RequestMapping(\"/create\")\n@CachePut(value = \"hot\", key = \"#result.id\")\npublic QuestionEntity create(@Valid @RequestBody QuestionEntity question){\n return IQuestionService.createQuestion(question);\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"保存的 id 是自增的,值爲 123,所以緩存中的 key = passjava_123。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b3/b3c03ba74c61b6a1dcde83f3917c5e07.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.8 刪除緩存註解","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"@CacheEvict 註解的方法在調用時不會在緩存中添加任何東西,但是會從從緩存中移除之前的緩存結果。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"示例代碼如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"@RequestMapping(\"/remove/{id}\")\n@CacheEvict(value = \"hot\")\npublic R remove(@PathVariable(\"id\") Long id){\n IQuestionService.removeById(id);\n return R.ok();\n}\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"刪除條目的 key 與傳遞進來的 id 相同。我測試的時候傳的 id = 123,經過前綴passjava_組裝後就是 passjava_123,所以將之前緩存的 passjava_123 刪除了。重複執行也不會報錯。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意:@CacheEvict 和 @Cacheable、@CachePut 不同,它能夠應用在返回值爲 void 的方法上。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"@CacheEvict 還有些屬性可供使用,總結如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ac/acb19c366113dbac31ad45d80ad27f16.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、 總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文通過傳統使用緩存的方式的痛點引出 Spring 框架中的 Cache 組件。然後詳細介紹了 Spring Cache 組件的用法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"五大註解。 @Cacheable、@CachePut、@CacheEvict、@Caching,、@CacheConfig。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何自定義緩存條目的 key。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何自定義 Cache 配置。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何自定義緩存的條件。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"參考資料:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"www.passjava.cn","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Spring in Action","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章