本篇文章,我們就一起聊一聊如何來更好的使用緩存,探尋下如何降低緩存交互過程的性能損耗、如何壓縮緩存的存儲空間占用、如何保證多個操作命令原子性等問題的解決策略,讓緩存在項目中可以發揮出更佳的效果。 ...
大家好,又見面了。
本文是筆者作為掘金技術社區簽約作者的身份輸出的緩存專欄系列內容,將會通過系列專題,講清楚緩存的方方面面。如果感興趣,歡迎關註以獲取後續更新。
通過前面的文章,我們一起剖析了Guava Cache
、Caffeine
、Ehcache
等本地緩存框架的原理與使用場景,也一同領略了以Redis
為代表的集中式緩存在分散式高併發場景下無可替代的價值。
現在的很多大型高併發系統都是採用的分散式部署方式,而作為高併發系統的基石,緩存是不可或缺的重要環節。項目中使用緩存的目的是為了提升整體的運算處理效率、降低對外的IO請求,而集中式緩存是獨立於進程之外部署的遠端服務,需要基於網路IO的方式交互。如果一個業務邏輯中涉及到非常頻繁的緩存操作,勢必會導致引入大量的網路IO交互,造成過大的性能損耗、加劇緩存伺服器的壓力。另外,對於現在互聯網系統的海量用戶數據,如何壓縮緩存數據占用容量,也是需要面臨的一個問題。
本篇文章,我們就一起聊一聊如何來更好的使用緩存,探尋下如何降低緩存交互過程的性能損耗、如何壓縮緩存的存儲空間占用、如何保證多個操作命令原子性等問題的解決策略,讓緩存在項目中可以發揮出更佳的效果。
通過BitMap降低Reids存儲容量壓力
在一些互聯網類的項目中,經常會有一些簽到相關功能。如果使用Redis來緩存用戶的簽到信息,我們一般而言會怎麼存儲呢?常見的會有下麵2種思路:
- 使用Set類型,每天生層1個Set,然後將簽到用戶添加到對應的Set中;
- 還是使用Set類型,每個用戶一個Set,然後將簽到的日期添加到Set中。
對於海量用戶的系統而言,按照上述的策略,那麼每天僅簽到信息這一項,就可能會有上千萬的記錄,一年累積下來的數據量更大 —— 這對Redis的存儲而言是筆不小的開銷。對於簽到這種簡單場景,只有簽到和沒簽到兩種情況,也即0/1
的場景,我們也可以通過BitMap來進行存儲以大大降低記憶體占用。
BitMap(點陣圖)
可以理解為一個bit數組,對應bit位可以存放0或者1,最終這個bit數組被轉換為一個字元串的形式存儲在Redis中。比如簽到這個場景,我們可以每天設定一個key,然後存儲的時候,我們可以將數字格式的userId表示在BitMap中具體的位置信息,而BitMap中此位置對應的bit值為1則表示該用戶已簽到。
Redis其實也提供了對BitMap存儲的支持。前面我們提過Redis支持String、Set、List、ZSet、Hash等數據結構,而BitMap能力的支持,其實是對String數據結構的一種擴展,使用String數據類型來支持BitMap的能力實現。比如下麵的代碼邏輯:
public void userSignIn(long userId) {
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
String redisKey = "UserSginIn_" + today;
Boolean hasSigned = stringRedisTemplate.opsForValue().getBit(redisKey, userId);
if (Boolean.TRUE.equals(hasSigned)) {
System.out.println("今日已簽過到!");
} else {
stringRedisTemplate.opsForValue().setBit("TodayUserSign", userId, true);
System.out.println("簽到成功!");
}
}
對於Redis而言,每天就只有一條key-value
數據。下麵對比下使用BitMap與使用普通key-value模式的數據占用情況對比。模擬構造10億用戶數據量進行壓測統計,結果如下:
- BitMap格式: 150M
- key-value格式: 41G
可以看出,在存儲容量占用方面,BitMap完勝。
關於pipeline管道批處理與multi事務原子性
使用Pipeline降低與Reids的IO交互頻率
在很多的業務場景中,我們可能會涉及到同時去執行好多條redis命令的操作,比如系統啟動的時候需要將DB中存量的數據全部載入到Redis中重建緩存的時候。如果業務流程需要頻繁的與Redis交互並提交命令,可能會導致在網路IO交互層面消耗太大,導致整體的性能降低。
這種情況下,可以使用pipeline
將各個具體的請求分批次提交到Redis伺服器進行處理。
private void redisPipelineInsert() {
stringRedisTemplate.executePipelined(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
try {
// 具體的redis操作,多條操作都在此處理,最後會一起提交到Redis遠端去執行
} catch (Exception e) {
log.error("failed to execute pipelined...", e);
}
return null;
}
});
}
使用pipeline的方式,可以減少客戶端與redis服務端之間的網路交互頻次,但是pipeline也只是負責將原本需要多次網路交互的請求封裝一起提交到redis上,在redis層面其執行命令的時候依舊是逐個去執行,並不會保證這一批次的所有請求一定是連貫被執行,其中可能會被插入其餘的執行請求。
也就是說,pipeline的操作是不具備原子性的。
使用multi實現請求的事務
前面介紹pipeline的時候強調了其僅僅只是將多個命令打包一起提交給了伺服器,然後伺服器依舊是等同於逐個提交上來的策略進行處理,無法保證原子性。對於一些需要保證多個操作命令原子性的場景下,可以使用multi
來實現。
當客戶端請求執行了multi命令之後,也即開啟了事務,服務端會將這個客戶端記錄為一個特殊的狀態,之後這個客戶端發送到伺服器上的命令,都會被臨時緩存起來而不會執行。只有當收到此客戶端發送exec
命令的時候,redis才會將緩存的所有命令一起逐條的執行並且保證這一批命令被按照發送的順序執行、執行期間不會被其他命令插入打斷。
代碼示例如下:
private void redisMulti() {
stringRedisTemplate.multi();
stringRedisTemplate.opsForValue().set("key1", "value1");
stringRedisTemplate.opsForValue().set("key2", "value2");
stringRedisTemplate.exec();
}
需要註意的一點是,redis的事務與關係型資料庫中的事務是兩個不同概念,Redis的事務不支持回滾,只能算是Redis中的一種特殊標記,可以將這個事務範圍內的請求以指定的順序執行,中間不會被插入其餘的請求,可以保證多個命令執行的原子性。
pipeline與multi區別
從上面分別對pipeline
與multi
的介紹,可以看出兩者在定位與功能分工上的差異點:
-
pipeline是客戶端行為,只是負責將客戶端的多個請求一次性打包傳遞到伺服器端,服務端依舊是按照和單條請求一樣的處理,批量傳遞到服務端的請求之間可能會插入別的客戶端的請求操作,所以它是無法保證原子性的,側重點在於其可以提升客戶端的效率(降低頻繁的網路交互損耗)
-
multi是服務端行為,通過開啟事務緩存,保證客戶端在事務期間提交的請求可以被一起集中執行。它的側重點是保證多條請求的原子性,執行期間不會被插入其餘客戶端的請求,但是由於開啟事務以及命令緩存等額外的操作,其對性能略微有一些影響。
多級緩存機制
本地+遠端的二級緩存機制
在涉及與集中式緩存之間頻繁交互的時候,通過前面介紹的pipeline方式可以適當的降低與服務端之間網路交互的頻次,但是很多情況下,依舊會產生大量的網路交互,對於一些追求極致性能的系統而言,可能依舊無法滿足訴求。
回想下此前文章中花費大量篇幅介紹的本地緩存,本地緩存在分散式場景下容易造成數據不一致的問題,但是其最大特點就是快,因為數據都存儲在進程內。所以可以將本地緩存作為集中式緩存的一個補充策略,對於一些需要高頻讀取且不會經常變更的數據,緩存到本地進行使用。
常見的本地+遠端
二級緩存有兩種存在形式。
- 獨立劃分,各司其職
這種情況,將緩存數據分為了2種類型,一種是不常變更的數據,比如系統配置信息等,這種數據直接系統啟動的時候從DB中載入並緩存到進程記憶體中,然後業務運行過程中需要使用時候直接從記憶體讀取。而對於其他可能會經常變更的業務層面的數據,則緩存到Redis中。
- 混合存儲,多級緩存
這種情況可以搭配Caffeine
或者Ehcache
等本地緩存框架一起實現。首先去本地緩存中執行查詢,如果查詢到則返回,查詢不到則去Redis中嘗試獲取。如果Redis中也獲取不到,則可以考慮去DB中進行回源兜底操作,然後將回源的結果存儲到Redis以及本地緩存中。這種情況下需要註意下如果數據發生變更的時候,需要刪除本地緩存,以確保下一次請求的時候,可以再次去Redis拉取最新的數據。
本地+遠端的二級緩存機制有著多方面的優點:
-
主要操作都在本地進行,可以充分的享受到本地緩存的速度優勢;
-
大部分操作都在本地進行,充分降低了客戶端與遠端集中式緩存伺服器之間的IO交互,也降低了帶寬占用;
-
通過本地緩存層,抵擋了大部分的業務請求,對集中式緩存伺服器端進行減壓,大大降低服務端的壓力;
-
提升了業務的可靠性,本地緩存實際上也是一種額外的副本備份,極端情況下,及時集中式緩存的服務端宕機,因為本地還有緩存數據,所以業務節點依舊可以對外提供正常服務。
二級緩存的應用身影
其實,在C-S架構
的系統裡面,多級緩存的概念使用的也非常的頻繁。經常Clinet端會緩存運行時需要的業務數據,然後採用定期更新或者事件觸發的方式從服務端更新本地的數據。而Server端負責存儲所有的數據,並保證數據更新的時候可以提供給客戶端進行更新獲取。
一個典型的例子,就是分散式系統中的配置中心或者是服務註冊管理中心。比如SpringCloud
家族的Eureka
,或者是Alibaba
開源的Nacos
。它們都有採用客戶端本地緩存+服務端數據統一存儲的方式,來保證整體的處理效率,降低客戶端對於Server端的實時交互依賴。
看一下Nacos
的交互示意:
從圖中可以表直觀的看到,Client將業務數據緩存到各自本地,這樣業務邏輯進行處理的時候就可以直接從本地緩存中查詢到相關的業務節點映射信息,而Server端只需要負責在數據有變更的事後推送到Client端更新到本地緩存中即可,避免了Server端去承載業務請求的流量壓力。整體的可靠性也得到了保證,避免了Server端異常對業務正常處理造成影響。
小結回顧
好啦,到這裡呢,《深入理解緩存原理與實戰設計》系列專欄的內容就暫告一段落咯。本專欄圍繞緩存這個巨集大命題進行展開闡述,從緩存各種核心要素、到本地緩存的規範與標準介紹,從手寫本地緩存框架、到各種優秀本地緩存框架的上手與剖析,從本地緩存到集中式緩存再到最後的多級緩存的構建,一步步全方位、系統性地做了介紹。希望通過本專欄的介紹,可以讓大家對緩存有個更加深刻的理解,可以更好的在項目中去使用緩存,讓緩存真正的成為我們項目中性能提升的神兵利器。
看到這裡,不知道各位小伙伴們對緩存的理解與使用,是否有了新的認識了呢?你覺得緩存還有哪些好的使用場景呢?歡迎評論區一起交流下,期待和各位小伙伴們一起切磋、共同成長。
我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點贊 + 關註讓我感受到您的支持。也可以關註下我的公眾號【架構悟道】,獲取更及時的更新。
期待與你一起探討,一起成長為更好的自己。
本文來自博客園,作者:架構悟道,歡迎關註公眾號[架構悟道]持續獲取更多乾貨,轉載請註明原文鏈接:https://www.cnblogs.com/softwarearch/p/16937368.html