浅谈缓存
1. 什么是緩存
緩存有很多種,從 CPU 緩存、磁盤緩存到瀏覽器緩存等,本文所說的緩存,主要針對后端系統的緩存。也就是將程序或系統經常要使用的對象存在內存中,以便在使用時可以快速調用,也可以避免加載數據或者創建重復的實例,以達到減少系統開銷,提高系統效率的目的。
2. 為什么要用緩存
我們一般都會把數據存放在關系型數據庫中,不管數據庫的性能有多么好,一個簡單的查詢也要消耗毫秒級的時間,這樣我們常說的 QPS 就會被數據庫的性能所限制,我們想要提高QPS,只能選擇更快的存儲設備。
在日常開發有這樣的一種場景:某些數據的數據量不大、不經常變動,但訪問卻很頻繁。受限于硬盤 IO 性能或者遠程網絡等原因,每次都直接獲取會消耗大量的資源。可能會導致我們的響應變慢甚至造成系統壓力過大,這在一些業務上是不能忍的,而緩存正是解決這類問題的神器。
但是有一點需要注意,就是緩存的占用空間以及緩存的失效策略,下文也會提到。
使用緩存的場景
對于緩存來說,數據不常變更且查詢比較頻繁是最好的場景,如果查詢量不夠大或者數據變動太頻繁,緩存也就是失去了意義。
3. 緩存的使用
日常工作使用的緩存可以分為內部緩存和外部緩存。
內部緩存一般是指存放在運行實例內部并使用實例內存的緩存,這種緩存可以使用代碼直接訪問。
外部緩存一般是指存放在運行實例外部的緩存,通常是通過網絡獲取,反序列化后進行訪問。
一般來說對于不需要實例間同步的,都更加推薦內部緩存,因為內部緩存有訪問方便,性能好的特點;需要實例間同步的數據可以使用外部緩存。
下面對這兩種類型的緩存分別的進行介紹。
3.1 內部緩存
為什么要是用內部緩存
在系統中,有些數據量不大、不常變化,但是訪問十分頻繁,例如省、市、區數據。針對這種場景,可以將數據加載到應用的內存中,以提升系統的訪問效率,減少無謂的數據庫和網路的訪問。
內部緩存的限制就是存放的數據總量不能超出內存容量,畢竟還是在 JVM 里的。
最簡單的內部緩存 - Map
如果只是需要將一些數據緩存起來,避免不必要的數據庫查詢,那么 Map 就可以滿足。
對于字典型的數據,在項目啟動的時候加載到 Map 中,程序就可以使用了,也很容易更新。
// 配置存放的MapMap<String, String> configs = new HashMap<String, String>(); // 初始化或者刷新配置的Map public void reloadConfigs () { Map<String, String> m = loadConfigFromDB(); configs = m;} // 使用 configs. getOrDefault ( "auth.id" , "1" );
功能強大的內部緩存 - Guava Cache / Caffeine
如果你需要緩存有強大的性能,或者對緩存有更多的控制,可以使用 Guava 里的 Cache 組件。
它是 Guava 中的緩存工具包,是非常簡單易用且功能強大的 JVM 內緩存,支持多種緩存過期策略。
LoadingCache<String, String> configs = CacheBuilder.newBuilder().maximumSize(1000) // 設置最大大小.expireAfterWrite(10, TimeUnit.MINUTES) // 設置過期時間, 10分鐘.build(new CacheLoader<String, String>() {// 加載緩存內容public String load(String key) throws Exception {return getConfigFromDB(key);}public Map<String, String> loadAll() throws Exception {return loadConfigFromDB();}});//CacheLoader.loadAll// 獲取某個key的值try {return configs.get(key);} catch (ExecutionException e) {throw new OtherException(e.getCause());}// 顯式的放入緩存configs.put(key, value)// 個別清除緩存configs.invalidate(key)// 批量清除緩存configs.invalidateAll(keys)// 清除所有緩存項configs.invalidateAll()本地緩存的優點:
-
直接使用內存,速度快,通常存取的性能可以達到每秒千萬級
-
可以直接使用 Java 對象存取
本地緩存的缺點:
-
數據保存在當前實例中,無法共享
-
重啟應用會丟失
Guava Cache 的替代者 Caffeine
Spring 5 使用 Caffeine 來代替 Guava Cache,應該是從性能的角度考慮的。從很多性能測試來看 Caffeine 各方面的性能都要比 Guava 要好。
Caffeine 的 API 的操作功能和 Guava 是基本保持一致的,并且 Caffeine 為了兼容之前 Guava 的用戶,做了一個 Guava 的 Adapter, 也是十分的貼心。
如果想了解更多請參考:是什么讓 Spring 5 放棄了使用 Guava Cache?
3.2 外部緩存
最著名的外部緩存 - Redis / Memcached
也許是 Redis 太有名,只要一提到緩存,基本上都會說起 Redis。但其實這類緩存的鼻祖應該是 LiveJournal 開發的 Memcached。
Redis / Memcached 都是使用內存作為存儲,所以性能上要比數據庫要好很多,再加上Redis 還支持很多種數據結構,使用起來也挺方便,所以作為很多人的首選。
Redis 確實不錯,不過即便是使用內存,也還是需要通過網絡來訪問,所以網絡的性能決定了 Reids 的性能;
我曾經做過一些性能測試,在萬兆網卡的情況下,對于 Key 和 Value 都是長度為 20 Byte 的字符串的 get 和 set 是每秒10w左右的,如果 Key 或者 Value 的長度更大或者使用數據結構,這個會更慢一些;
作為一般的系統來使用已經綽綽有余了,從目前來看,Redis 確實很適合來做系統中的緩存。
如果考慮多實例或者分布式,可以考慮下面的方式:
-
Jedis 的 ShardedJedis( 調用端自己實現分片 )
-
twemproxy / codis( 第三方組件實現代理 )
-
Redis Cluster( 3.0 之后官方提供的集群方案 )
這些方案各有特點,這次先不展開討論,有興趣的可以先研究一下。
Redis有很多優點:
-
很容易做數據分片、分布式,可以做到很大的容量
-
使用基數比較大,庫比較成熟
同時也有一些缺點:
-
Java 對象需要序列化才能保存
-
如果服務器重啟,再不做持久化的情況下會丟失數據,即使有持久化也容易出現各種各樣的問題
4. 緩存的更新策略
使用緩存時,更新策略是非常重要的。最常見的緩存更新策略是 Cache Aside Pattern:
-
失效:應用程序先從 cache 取數據,沒有得到,則從數據庫中取數據,成功后,放到緩存中。
-
命中:應用程序從 cache 中取數據,取到后返回。
-
更新:先把數據存到數據庫中,成功后,再讓緩存失效。
不管是內部緩存還是外部緩存,都可以使用這樣的更新策略,如果緩存系統支持,也可以通過設置過期時間來更新緩存。
更多的更新策略可以參考左耳朵耗子的這篇緩存更新的套路。
5. 緩存使用常見誤區
序列化方案的選擇
序列化的選擇,盡量避免使用 Java 原生的機制,因為原生的序列化依賴 serialVersionUID 來判斷版本,如果改變就無法正常的反序列化。
一般推薦使用 Json 或者 Hessian、ProtoBuf 等二進制方式。
緩存大對象
在緩存中存放大對象,存取的代價都比較高。實際使用時,往往只是需要其中的一部分,這樣會導致每一次讀取都消耗更多的網絡和內存資源,也會浪費緩存的容量。
當然如果每次都是用完整的對象,這樣做是沒有問題的。
使用緩存進行數據共享
使用緩存來當作線程甚至進程之間的數據共享方式,會讓系統間產生隱形的依賴,并且也可能會產生一些競爭,常常會發生問題。所以不推薦使用這種方式來共享數據。
沒有及時更新或者刪除緩存中已經過期或失效的數據
這個理解起來就很簡單了,如果沒有及時更新或者刪除,就有可能讀取到錯誤的數據,從而導致業務的錯誤。
對于支持設置過期時間的緩存系統,可以對每一個數據設置合適的過期時間,來盡量避免這樣的情況。
總結
- 上一篇: 插播面试题:海量数据求最大值Topk或者
- 下一篇: LRU实现