聊聊redis中多樣的資料型別,以及叢集相關的知識

2022-01-10 19:01:24
本篇文章帶大家瞭解一下redis中多樣的資料型別,以及叢集相關的知識,帶大家搞懂叢集,希望對大家有所幫助!

多樣的資料型別

string 型別簡單方便,支援空間預分配,也就是每次會多分配點空間,這樣 string 如果下次變長的話,就不需要額外的申請空了,當然前提是剩餘的空間夠用。【相關推薦:Redis視訊教學

List 型別可以實現簡單的訊息佇列,但是注意可能存在訊息丟失哦,它並不持 ACK 模式。

Hash 表有點像關係型資料庫,但是當 hash 表越來越大的時候,請注意,避免使用 hgetall 之類的語句,因為請求大量的資料會導致redis阻塞,這樣後面的兄弟們就得等待了。

set 集合型別可以幫你做一些統計,比如你要統計某天活躍的使用者,可以直接把使用者ID扔到集合裡,集合支援一些騷操作,比如 sdiff 可以獲取集合之間的差集,sunion 可以獲取集合之間的並集,功能很多,但是一定需要謹慎,因為牛逼的功能是有代價的,這些操作需要耗費一些 CPU 和IO 資源,可能會導致阻塞,因此大集合之間的騷操作要慎用,

zset 可以說是最閃耀的星,可以做排序,因為可以排序,因此應用場景挺多,比如點贊前xx名使用者,延時佇列等等。

bitmap 點陣圖的好處就是在於節省空間,特別在做一些統計類的方面,比如要統計某一天有多少個使用者簽到了並且某個使用者是否簽到了,如果不用bitmap的話,你可能會想到用set。

SADD day 1234//簽到就新增到集合
SISMEMBER day 1234//判斷1234是否簽到
SCARD day   //有多少個簽到的

set 在功能上可以滿足,但是相比bitmap的話,set要更耗費儲存空間,set的底層主要是由整數集合或者 hashtable 組成,整數集合只有在資料量非常小的情況下才會使用,一般是小於512個元素,同時元素必須都是整數,對於set來說,整數集合的資料更加緊湊,他們在記憶體是上連續的,查詢的話只能是二分查詢了,時間複雜度是O(logN),而 hashtable 就不同了,這裡的 hashtable 和 redis 的5巨量資料型別中的hash是一樣的,只不過沒有 value 而已,value 指向個 null,同時也不存在衝突,因為這裡是集合,但是需要考慮 rehash 相關問題。ok,扯的有點遠,我們說的使用者簽到問題,在使用者非常多的情況下,set 的話肯定會用到 hashtable,hashtable 的話,其實每個元素都是個 dictEntry 結構體

typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下個雜湊表節點,形成連結串列
    struct dictEntry *next;

} dictEntry;

從這個結構體可以看到什麼呢?首先雖然值 union(沒有 value)和 next(沒有衝突)是空的,但是結構體本身需要空間,還需要加上個 key,這個佔用空間是實打實的,而如果用 bitmap 的話,一個bit位就可以代表一個數位,很省空間,我們來看看 bitmap 的方式如何設定和統計。

SETBIT day 1234 1//簽到
GETBIT day 1234//判斷1234是否簽到
BITCOUNT day//有多少個簽到的

bf 這是 redis4.0 之後支援的布隆過濾器 RedisBloom,但是需要單獨載入對應的 module,當然我們也可以基於上述的 bitmap 來實現自己的布隆過濾器,不過既然 redis 已經支援了,通過 RedisBloom 可以減少我們的開發時間,布隆過濾器是幹嘛的,我這裡就不贅述了,直接來看看 RedisBloom 相關的用法吧。

# 可以通過docker的方式快速拉取映象來玩耍
docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
docker exec -it redis-redisbloom bash
redis-cli
# 相關操作
bf.reserve sign 0.001 10000
bf.add sign 99 //99這個使用者加入
bf.add exists 99//判斷99這個使用者是否存在

因為布隆過濾器是存在誤判的,所有 bf 支援自定義誤判率,0.001就代表誤判率,10000 代表布隆過濾器可以儲存的元素個數,當實際儲存的元素個數超過這個值的時候,誤判率會提高。

HyperLogLog 可以用於統計,它的優點就是佔用的儲存空間極小,只需要 12KB 的記憶體就可以統計 2^64 個元素,那它主要統計什麼呢?其實主要就是基數統計,比如像 UV 這種,從功能上來說 UV 可以用 set 或者 hash 來儲存,但是缺點就是耗費儲存,容易使之變成大 key,如果想要節省空間,bitmap 也可以,12KB 空間的 bitmap 只能統計 12*1024*8=98304個元素,而 HyperLogLog 卻可以統計 2^64 個元素,但是這麼牛逼的技術其實是有誤差的,HyperLogLog 是基於概率來統計的,標準誤算率是 0.81%,在統計海量資料並且對精度要求不那麼高的場景下,HyperLogLog 在節省空間這塊還是很優秀的。

PFADD uv 1 2 3 //1 2 3是活躍使用者
PFCOUNT uv //統計

GEO 是可以應用在地理位置的業務上,比如微信附近的人或者附近的車輛等等,先來看一下如果沒有GEO 這種資料結構,你如何知道你附近的人?首先得上報自己的地理位置資訊吧,比如經度 116.397128,緯度 39.916527,此時可以用 string、hash 資料型別儲存,但是如果要查詢你附近的人,string 和 hash 這種就無能為例了,你不可能每次都要遍歷全部的資料來判斷,這樣太耗時了,當然你也不可能通過 zset 這種資料結構來把經緯度資訊當成權重,但是如果我們能把經緯度資訊通過某種方式轉換成一個數位,然後當成權重好像也可以,這時我們只需通過zrangebyscore key v1 v2也可以找到附近的人。真的需要這麼麻煩嗎?於是 GEO 出現了,GEO 轉換經緯度為數位的方法是「二分割區間,區間編碼」,這是什麼意思呢?以經度為例,它的範圍是[-180,180],如果要採用3位編碼值,那麼就是需要二分3次,二分後落在左邊的用0表示,右邊的用1表示,以經度是121.48941 來說,第一次是在[0,180]這個區間,因此記1,第二次是在[90,180],因此再記1,第三次是在[90,135],因此記0。緯度也是同樣的邏輯,假設此時對應的緯度編碼後是010,最後把經緯度合併在一起,需要注意的是經度的每個值在偶數位,緯度的每個值在奇數位。

1 1 0   //經度
 0 1 0  //緯度
------------
101100 //經緯度對應的數值

原理是這樣,我們再來看看 redis 如何使用 GEO:

GEOADD location 112.123456 41.112345 99 //上報使用者99的地理位置資訊
GEORADIUS location  112.123456 41.112345 1 km ASC COUNT 10 //獲取附近1KM的人

搞懂叢集

生產環境用單範例 redis 的應該比較少,單範例的風險在於:

  • 單點故障即服務故障,沒有backup

  • 單範例壓力大,又要提供讀,又要提供寫

於是我們首先想到的就是經典的主從模式,而且往往是一主多從,這是因為大部分應用都是讀多寫少的情況,我們的主負責更新,從負責提供讀,就算我們的主宕機了,我們也可以選擇一個從來充當主,這樣整個應用依然可以提供服務。

複製過程的細節

當一個 redis 範例首次成為某個主的從的時候,這時主得把資料發給它,也就是 rdb 檔案,這個過程 master 是要 fork 一個子程序來處理的,這個子程序會執行 bgsave 把當前的資料重新儲存一下,然後準備發給新來的從,bgsave 的本質是讀取當前記憶體中的資料然後儲存到 rdb 檔案中,這個過程涉及大量的 IO,如果直接在主程序中來處理的話,大概率會阻塞正常的請求,因此使用個子程序是個明智的選擇。

那 fork 的子程序在 bgsave 過程中如果有新的變更請求會怎麼辦?

嚴格來說子程序出來的一瞬間,要儲存的資料應該就是當時那個點的快照資料,所以是直接把當時的記憶體再複製一份嗎?不復制的話,如果這期間又有變更改怎麼辦?其實這要說到寫實複製(COW)機制,首先從表象上來看記憶體是一整塊空間,其實這不太好維護,因此作業系統會把記憶體分成一小塊一小塊的,也就是記憶體分頁管理,一頁的大小一般是4K、8K或者16K等等,redis 的資料都是分佈在這些頁面上的,出於效率問題,fork 出來的子程序是和主程序是共用同一塊的記憶體的,並不會複製記憶體,如果這期間主程序有資料變更,那麼為了區分,這時最快捷的做法就是把對應的資料頁重新複製一下,然後主的變更就在這個新的資料頁上修改,並不會修改來的資料頁,這樣就保證了子程序處理的還是當時的快照。

以上說的變更是從快照的角度來考慮的,如果從資料的一致性來說,當快照的 rdb 被從庫應用之後,這期間的變更該如何同步給從庫?答案是緩衝區,這個緩衝區叫做 replication buffer,主庫在收到需要同步的命令之後,會把期間的變更都先儲存在這個緩衝區中,這樣在把 rdb 發給從庫之後,緊接著會再把 replication buffer 的資料也發給從庫,最終主從就保持了一致。

replication buffer不是萬能的補給劑

我們來看看 replication buffer 持續寫入的時間有多長。

  • 我們知道主從同步的時候,主庫會執行 fork 來讓子程序完成相應地工作,因此子程序從開始執行 bgsave 到執行完畢這期間,變更是要寫入 replication buffer 的。

  • rdb 生成好之後,需要把它傳送給從庫,這個網路傳輸是不是也需要耗點時間,這期間也是要寫入 replication buffer 的。

  • 從庫在收到 rdb 之後需要把 rdb 應用到記憶體裡,這期間從庫是阻塞的,無法提供服務,因此這期間也是要寫入 replication buffer 的。

replication buffer 既然是個 buffer,那麼它的大小就是有限的,如果說上面3個步驟中,只要有一個耗時長,就會導致 replication buffer 快速增長(前提是有正常的寫入),當 replication buffer 超過了限制之後就會導致主庫和從庫之間的連線斷開,斷開之後如果從庫再次連線上來就會導致重新開始複製,然後重複同樣的漫長的複製步驟,因此這個 replication buffer 的大小還是很關鍵的,一般需要根據寫入的速度、每秒寫入的量和網路傳輸的速度等因素來綜合判斷。

從庫網路不好和主庫斷了該怎麼辦?

正常來說,只要主從之間的連線建立好了,後面主庫的變更可以直接發給從庫,讓從庫直接回放,但是我們並不能保證網路環境是百分百的通暢的,因此也要考慮從庫和主庫之間的斷聯問題。

應該是在 redis2.8 以前,只要從庫斷聯,哪怕只有很短的時間,後面從庫再次連線上來的時候,主庫也會直接無腦的進行全量同步。在 2.8 版本及以後,開始支援增量複製了,增量複製的原理就是得有個緩衝區來儲存變更的記錄,這裡這個緩衝區叫做repl_backlog_buffer,這個緩衝區從邏輯上來說是個環形緩衝區,寫滿了就會從頭開始覆蓋,所以也有大小限制。在從庫重新連線上來的時候,從庫會告訴主庫:「我當前已經複製到了xx位置」,主庫收到從庫的訊息之後開始檢視xx位置的資料是否還在 repl_backlog_buffer 中,如果在的話,直接把xx後面的資料發給從庫即可,如果不在的話,那無能為力了,只能再次進行全量同步。

需要一個管理者

在主從模式下,如果主庫掛了,我們可以把一個從庫升級成主庫,但是這個過程是手動的,靠人力來操作,不能使損失降到最低,還是需要一套自動管理和選舉的機制,這就是哨兵,哨兵它本身也是個服務,只不過它不處理資料的讀寫而已,它只負責管理所有的 redis 範例,哨兵每隔一段時間會和各個 redis 通訊(ping 操作),每個 redis 範例只要在規定的時間內及時回覆,就可以表明自己的立場。當然哨兵本身也可能存在宕機或者網路不通的情況,因此一般哨兵也會搭建個哨兵叢集,這個叢集的個數最好是奇數,比如3個或者5這個這種,奇數的目的主要就是為了選舉(少數服從多數)。

當某個哨兵在發起 ping 後沒有及時收到 pong,那麼就會把這個 redis 範例標記下線,此時它還是不是真正的下線,這時其他的哨兵也會判定當前這個哨兵是不是真正的下線,當大多數哨兵都認定這個 redis 是下線狀態,那麼就會把它從叢集中踢出去,如果下線的是從庫,那麼還好,直接踢出去就ok,如果是主庫還要觸發選舉,選舉也不是盲目選舉,肯定是要選出最合適的那個從來充當新的主庫。這個最合適充當主庫的庫,一般會按照以下優先順序來確定:

  • 權重,每個從庫其實都可以設定一個權重,權重越高的從庫會被優先選擇

  • 複製的進度,每個從庫複製的進度可能是不一樣的,優先選擇當前和主庫資料差距最小的那個

  • 服務的 ID,其實每個 redis 範例都有自己的 ID,如果以上條件都一樣,那麼會選擇 ID 最小的那個庫來充當主庫

更強的橫向伸縮性

主從模式解決了單點故障問題,同時讀寫分離技術使得應用支撐能力更強,哨兵模式可以自動監管叢集,實現自動選主,自動剔除故障節點的能力。

正常來說只要讀的壓力越來越大,我們可以新增從庫來緩解,那如果主庫壓力很大怎麼辦?這就得提到接下來要說的分片技術了,我們只需要把主庫切成幾片,部署到不同的機器上即可。這個分片就是 redis 中的概念了,當分片的時候,redis 會預設分成 0~16383 也就是一共 16384 個槽,然後把這些槽平均分到每個分片節點上就可以起到負載均衡的作用了。每個 key 具體該分到哪個槽中,主要是先 CRC16 得到一個 16bit 的數位,然後這個數位再對 16384 取模即可:

crc16(key)%16384

然後使用者端會快取槽資訊,這樣每當一個 key 到來時,只要通過計算就知道該發給哪個範例來處理來了。但是使用者端快取的槽資訊並不是一成不變的,比如在增加範例的時候,這時候會導致重新分片,那麼原來使用者端快取的資訊就會不準確,一般這時候會發生兩個常見的錯誤,嚴格來說也不是錯誤,更像一種資訊,一個叫做MOVED,一個叫做ASK。moved的意思就說,原來是範例A負責的資料,現在被遷移到了範例B,MOVED 代表的是遷移完成的,但是 ASK 代表的是正在遷移過程中,比如原來是範例A負責的部分資料,現在被遷移到了範例B,剩下的還在等待遷移中,當資料遷移完畢之後 ASK 就會變成 MOVED,然後使用者端收到 MOVED 資訊之後就會再次更新下本地快取,這樣下次就不會出現這兩個錯誤了。

更多程式設計相關知識,請存取:!!

以上就是聊聊redis中多樣的資料型別,以及叢集相關的知識的詳細內容,更多請關注TW511.COM其它相關文章!