探討下如何更好的使用緩存 —— Redis緩存的特殊用法以及與本地緩存一起構建多級緩存的實現

来源:https://www.cnblogs.com/softwarearch/archive/2023/01/17/16937368.html
-Advertisement-
Play Games

本篇文章,我們就一起聊一聊如何來更好的使用緩存,探尋下如何降低緩存交互過程的性能損耗、如何壓縮緩存的存儲空間占用、如何保證多個操作命令原子性等問題的解決策略,讓緩存在項目中可以發揮出更佳的效果。 ...


大家好,又見面了。


本文是筆者作為掘金技術社區簽約作者的身份輸出的緩存專欄系列內容,將會通過系列專題,講清楚緩存的方方面面。如果感興趣,歡迎關註以獲取後續更新。


通過前面的文章,我們一起剖析了Guava CacheCaffeineEhcache本地緩存框架的原理與使用場景,也一同領略了以Redis為代表的集中式緩存在分散式高併發場景下無可替代的價值。

現在的很多大型高併發系統都是採用的分散式部署方式,而作為高併發系統的基石,緩存是不可或缺的重要環節。項目中使用緩存的目的是為了提升整體的運算處理效率、降低對外的IO請求,而集中式緩存是獨立於進程之外部署的遠端服務,需要基於網路IO的方式交互。如果一個業務邏輯中涉及到非常頻繁的緩存操作,勢必會導致引入大量的網路IO交互,造成過大的性能損耗、加劇緩存伺服器的壓力。另外,對於現在互聯網系統的海量用戶數據,如何壓縮緩存數據占用容量,也是需要面臨的一個問題。

本篇文章,我們就一起聊一聊如何來更好的使用緩存,探尋下如何降低緩存交互過程的性能損耗、如何壓縮緩存的存儲空間占用、如何保證多個操作命令原子性等問題的解決策略,讓緩存在項目中可以發揮出更佳的效果。

通過BitMap降低Reids存儲容量壓力

在一些互聯網類的項目中,經常會有一些簽到相關功能。如果使用Redis來緩存用戶的簽到信息,我們一般而言會怎麼存儲呢?常見的會有下麵2種思路:

  1. 使用Set類型,每天生層1個Set,然後將簽到用戶添加到對應的Set中;
  2. 還是使用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區別

從上面分別對pipelinemulti的介紹,可以看出兩者在定位與功能分工上的差異點:

  • 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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 2023-01-17 一、Spring管理druid步驟 (1)導入jar包 <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <arti ...
  • 抽獎程式 ''' 抽獎程式 使用時可以修改嘉賓名單,然後單機‘開始’和‘停止’按鈕 來控制界面上名單的滾動實現抽獎功能,涉及的模塊主要 有多線程 ''' import itertools import random import threading import time import tkinte ...
  • 摘要:本文主要講解圖像局部直方圖均衡化和自動色彩均衡化處理。這些演算法可以廣泛應用於圖像增強、圖像去噪、圖像去霧等領域。 本文分享自華為雲社區《[Python從零到壹] 五十四.圖像增強及運算篇之局部直方圖均衡化和自動色彩均衡化處理》,作者: eastmount。 一.局部直方圖均衡化 前文通過調用O ...
  • 簡介 限流顧名思義是對流量大小進行限制,防止請求數量超過系統的負載能力,導致系統崩潰,起到保護作用。 現實生活中限流也隨處可見,節假日出門旅行的人數會劇增,對於旅游景點來說往往會不堪重負,如果不進行人數控制,對整個景點的壓力會非常大,游客的體驗也會非常差,還容易出現安全事故等危險。 同樣的在一線城市 ...
  • 轉載:https://blog.csdn.net/tslx1020/article/details/128250777 1、spawn - 冷啟動 frida-trace -U -f com.apple.ExampleCode -m “+[NSURL URLWithString:]" 2、attac ...
  • 伺服器信息 在阿裡雲買了個搶占式的伺服器,地區為華南廣州,系統為Ubuntu 20.04,8核16GB。 安裝Docker 命令如下: $ apt-get update -y $ apt-get upgrade -y $ apt-get install -y docker.io 安裝成功後,檢查一下 ...
  • 2023-01-14 一、Spring底層IOC實現 1、IOC:將對象的控制器反轉給Spring 2、BeanFactory與ApplicationContext (1)BeanFactory:IOC容器的基本實現,是Spring內部的使用介面,是面向Spring本身的,不是提供給開發人員使用的。 ...
  • 牛牛剛剛出生,嗷嗷待哺,一開始他只能學說簡單的數字,你跟他說一個整數,他立刻就能學會。輸入一個整數,輸出這個整數。 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...