Redis 架構原理及應用實踐

作者:小星的 java 學習筆記
鏈接:https://www.jianshu.com/p/6c970eb652d5

一. Redis 簡介

Redis 是完全開源免費的,是一個高性能的 key-value 類型的內存數據庫。整個數據庫統統加載在內存當中進行操作,定期通過異步操作把數據庫數據 flush 到硬盤上進行保存。因爲是純內存操作,Redis 的性能非常出色,每秒可以處理超過 10 萬次讀寫操作,是已知性能最快的 Key-Value DB。

Redis 的出色之處不僅僅是性能,Redis 最大的魅力是支持保存多種數據結構,此外單個 value 的最大限制是 1GB,因此 Redis 可以用來實現很多有用的功能,比方說用 List 來做 FIFO 雙向鏈表,實現一個輕量級的高性 能消息隊列服務,用他的 Set 可以做高性能的 tag 系統等等。另外 Redis 也可以對存入的 Key-Value 設置 expire 時間。總結來說,使用 Redis 的好處如下:

  1. 速度快,因爲數據存在內存中,讀的速度是 110000 次 /s, 寫的速度是 81000 次 /s;

  2. 支持豐富數據類型,支持 string,list,set,sorted set,hash;

  3. 支持事務,操作都是原子性,對數據的更改要麼全部執行,要麼全部不執行,事務中任意命令執行失敗,其餘命令依然被執行。也就是說 Redis 事務不保證原子性,也不支持回滾;事務中的多條命令被一次性發送給服務器,服務器在執行命令期間,不會去執行其他客戶端的命令請求。

  4. 豐富的特性:可用於緩存,消息(支持 publish/subscribe 通知),按 key 設置過期時間,過期後將會自動刪除,具體淘汰策略有:

  4.1.volatile-lru:從已經設置過期時間的數據集中,挑選最近最少使用的數據淘汰

  4.2.volatile-ttl:從已經設置過期時間的數據集中,挑選即將要過期的數據淘汰

  4.3.volatile-random:從已經設置過期時間的數據集中,隨機挑選數據淘汰

  4.4.allkeys-lru:從所有的數據集中,挑選最近最少使用的數據淘汰

  4.5.allkeys-random:從所有的數據集中,隨機挑選數據淘汰

  4.6.no-enviction:禁止淘汰數據

具體過期鍵的策略有:定時刪除(緩存過期時間到就刪除,創建 timer 耗 CPU),惰性刪除(獲取的時候檢查,不獲取一直留在內存,對內存不友好),定期刪除(CPU 和內存的折中方案)

  1. 支持數據持久化,可以將內存中的數據保存在磁盤中,重啓的時候可以再次加載進行使用;

  2. 支持數據的備份,即 master - slave 模式的數據備份。

Redis 的主要缺點是數據庫容量受到物理內存的限制,不能用作海量數據的高性能讀寫,因此 Redis 適合的場景主要侷限在較小數據量的高性能操作和運算上。

二. Redis 的數據類型

Redis 支持 5 中數據類型:string(字符串),hash(哈希),list(列表),set(集合),zset(sorted set:有序集合)。每種數據類型的具體命令請參考 Redis 命令參考

string

string 是 redis 最基本的數據類型。一個 key 對應一個 value。string 是二進制安全的。也就是說 redis 的 string 可以包含任何數據。比如 jpg 圖片或者序列化的對象。string 類型是 redis 最基本的數據類型,string 類型的值最大能存儲 512 MB。

hash

Redis hash 是一個鍵值對(key - value)集合。Redis hash 是一個 string 類型的 key 和 value 的映射表,hash 特別適合用於存儲對象。並且可以像數據庫中一樣只對某一項屬性值進行存儲、讀取、修改等操作。

list

Redis 列表是簡單的字符串列表,按照插入順序排序。我們可以網列表的左邊或者右邊添加元素。list 就是一個簡單的字符串集合,和 Java 中的 list 相差不大,區別就是這裏的 list 存放的是字符串。list 內的元素是可重複的。可以做消息隊列或最新消息排行等功能。

set

redis 的 set 是字符串類型的無序集合。集合是通過哈希表實現的,因此添加、刪除、查找的複雜度都是 O(1)。redis 的 set 是一個 key 對應着多個字符串類型的 value,也是一個字符串類型的集合,和 redis 的 list 不同的是 set 中的字符串集合元素不能重複,但是 list 可以。利用唯一性,可以統計訪問網站的所有獨立 ip。

Zset

redis zset 和 set 一樣都是字符串類型元素的集合,並且集合內的元素不能重複。不同的是 zset 每個元素都會關聯一個 double 類型的分數。redis 通過分數來爲集合中的成員進行從小到大的排序。zset 的元素是唯一的,但是分數(score)卻可以重複。可用作排行榜等場景。

三. redis 適用場景

1. 會話緩存(Session Cache)

最常用的一種使用 Redis 的情景是會話緩存(session cache)。用 Redis 緩存會話比其他存儲(如 Memcached)的優勢在於:Redis 提供持久化。當維護一個不是嚴格要求一致性的緩存時,如果用戶的購物車信息全部丟失,大部分人都會不高興的。

2. 隊列

Reids 在內存存儲引擎領域的一大優點是提供 list 和 set 操作,這使得 Redis 能作爲一個很好的消息隊列平臺來使用。Redis 作爲隊列使用的操作,就類似於本地程序語言(如 Python)對 list 的 push/pop 操作。

3. 全頁緩存

大型互聯網公司都會使用 Redis 作爲緩存存儲數據,提升頁面相應速度。即使重啓了 Redis 實例,因爲有磁盤的持久化,用戶也不會看到頁面加載速度的下降。

4. 排行榜 / 計數器

Redis 在內存中對數字進行遞增或遞減的操作實現的非常好。集合(Set)和有序集合(Sorted Set)也使得我們在執行這些操作的時候變的非常簡單。

四. Redis 高可用架構

1. 持久化

Redis 是內存型數據庫,爲了保證數據在斷電後不會丟失,需要將內存中的數據持久化到硬盤上。Redis 提供了兩種持久化的方式,分別是 RDB(Redis DataBase)和 AOF(Append Only File)。

RDB

簡而言之,就是在不同的時間點,將 redis 存儲的數據生成快照並存儲到磁盤等介質上,可以將快照複製到其他服務器從而創建具有相同數據的服務器副本。如果系統發生故障,將會丟失最後一次創建快照之後的數據。如果數據量大,保存快照的時間會很長。

AOF

換了一個角度來實現持久化,那就是將 redis 執行過的所有寫指令記錄下來,在下次 redis 重新啓動時,只要把這些寫指令從前到後再重複執行一遍,就可以實現數據恢復了。將寫命令添加到 AOF 文件(append only file)末尾。

使用 AOF 持久化需要設置同步選項,從而確保寫命令同步到磁盤文件上的時機。這是因爲對文件進行寫入並不會馬上將內容同步到磁盤上,而是先存儲到緩衝區,然後由操作系統決定什麼時候同步到磁盤。選項同步頻率 always 每個寫命令都同步,eyerysec 每秒同步一次,no 讓操作系統來決定何時同步,always 選項會嚴重減低服務器的性能,everysec 選項比較合適,可以保證系統崩潰時只會丟失一秒左右的數據,並且 Redis 每秒執行一次同步對服務器幾乎沒有任何影響。no 選項並不能給服務器性能帶來多大的提升,而且會增加系統崩潰時數據丟失的數量。隨着服務器寫請求的增多,AOF 文件會越來越大。Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 文件中的冗餘寫命令。

其實 RDB 和 AOF 兩種方式也可以同時使用,在這種情況下,如果 redis 重啓的話,則會優先採用 AOF 方式來進行數據恢復,這是因爲 AOF 方式的數據恢復完整度更高。如果你沒有數據持久化的需求,也完全可以關閉 RDB 和 AOF 方式,這樣的話,redis 將變成一個純內存數據庫。

2. 複製

Redis 爲了解決單點數據庫問題,會把數據複製多個副本部署到其他節點上,通過複製,實現 Redis 的高可用性,實現對數據的冗餘備份,保證數據和服務的高度可靠性。Redis 有主從和主備兩種方式解決單點問題,主備(keepalived)模式下主機備機對外提供同一個虛擬 IP,客戶端通過虛擬 IP 進行數據操作,正常期間主機一直對外提供服務,宕機後 VIP 自動漂移到備機上。主從模式下當 Master 宕機後,通過選舉算法 (Paxos、Raft) 從 slave 中選舉出新 Master 繼續對外提供服務,主機恢復後以 slave 的身份重新加入,此模式下可以使用讀寫分離,如果數據量比較大,不希望過多浪費機器,還希望在宕機後,做一些自定義的措施,比如報警、記日誌、數據遷移等操作,推薦使用主從方式,因爲和主從搭配的一般還有個管理監控中心(哨兵)。

①從數據庫向主數據庫發送 sync(數據同步) 命令。

②主數據庫接收同步命令後,會保存快照,創建一個 RDB 文件。

③當主數據庫執行完保持快照後,會向從數據庫發送 RDB 文件,而從數據庫會接收並載入該文件。

④主數據庫將緩衝區的所有寫命令發給從服務器執行。

⑤以上處理完之後,之後主數據庫每執行一個寫命令,都會將被執行的寫命令發送給從數據庫。可以同步發送也可以異步發送,同步發送可以不用每臺都同步,可以配置一臺 master,一臺 slave,同時這臺 salve 又作爲其他 slave 的 master。異步方式無法保證數據的完整性,比如在異步同步過程中主機突然宕機了,也稱這種方式爲數據弱一致性。

注意:在 Redis2.8 之後,主從斷開重連後會根據斷開之前最新的命令偏移量進行增量複製。

3. 哨兵

哨兵是 Redis 集羣架構中非常重要的一個組件,哨兵的出現主要是解決了主從複製出現故障時需要人爲干預的問題。

1.Redis 哨兵主要功能

(1)集羣監控:負責監控 Redis master 和 slave 進程是否正常工作

(2)消息通知:如果某個 Redis 實例有故障,那麼哨兵負責發送消息作爲報警通知給管理員

(3)故障轉移:如果 master node 掛掉了,會自動轉移到 slave node 上

(4)配置中心:如果故障轉移發生了,通知 client 客戶端新的 master 地址

2.Redis 哨兵的高可用

原理:當主節點出現故障時,由 Redis Sentinel 自動完成故障發現和轉移,並通知應用方,實現高可用性。哨兵機制建立了多個哨兵節點 (進程),共同監控數據節點的運行狀況。同時哨兵節點之間也互相通信,交換對主從節點的監控狀況。每隔 1 秒每個哨兵會向整個集羣:Master 主服務器 + Slave 從服務器 + 其他 Sentinel(哨兵)進程,發送一次 ping 命令做一次心跳檢測。這個就是哨兵用來判斷節點是否正常的重要依據,涉及兩個新的概念:主觀下線和客觀下線。一個哨兵節點判定主節點 down 掉是主觀下線,只有半數哨兵節點都主觀判定主節點 down 掉,此時多個哨兵節點交換主觀判定結果,纔會判定主節點客觀下線。基本上哪個哨兵節點最先判斷出這個主節點客觀下線,就會在各個哨兵節點中發起投票機制 Raft 算法(選舉算法),最終被投爲領導者的哨兵節點完成主從自動化切換的過程。

4. 集羣

至少部署兩臺 Redis 服務器構成一個小的集羣,主要有 2 個目的:

高可用性:在主機掛掉後,自動故障轉移,使前端服務對用戶無影響。

讀寫分離:將主機讀壓力分流到從機上。

可在客戶端組件上實現負載均衡,根據不同服務器的運行情況,分擔不同比例的讀請求壓力。

緩存數據量不斷增加時,單機內存不夠使用,需要把數據切分不同部分,分佈到多臺服務器上。可在客戶端對數據進行分片,數據分片算法詳見一致性 Hash 詳解、虛擬桶分片。

當數據量持續增加時,應用可根據不同場景下的業務申請對應的分佈式集羣。這塊最關鍵的是緩存治理這塊,其中最重要的部分是加入了代理服務(Codis 和 Twemproxy)。應用通過代理訪問真實的 Redis 服務器進行讀寫,這樣做的好處是避免越來越多的客戶端直接訪問 Redis 服務器難以管理,而造成風險,在代理這一層可以做對應的安全措施,比如限流、授權、分片,避免客戶端越來越多的邏輯代碼,不但臃腫升級還比較麻煩。代理這層無狀態的,可任意擴展節點,對於客戶端來說,訪問代理跟訪問單機 Redis 一樣。

Redis Cluster 是 Redis 官網給出的集羣架構

客戶端與 Redis 節點直連, 不需要中間 Proxy 層,直接連接任意一個 Master 節點,根據公式 HASH_SLOT=CRC16(key) mod 16384,計算出映射到哪個分片上,然後 Redis 會去相應的節點進行操作

具有如下優點:

(1) 無需 Sentinel 哨兵監控,如果 Master 掛了,Redis Cluster 內部自動將 Slave 切換 Master

(2) 可以進行水平擴容

(3) 支持自動化遷移,當出現某個 Slave 宕機了,那麼就只有 Master 了,這時候的高可用性就無法很好的保證了,萬一 Master 也宕機了,咋辦呢?針對這種情況,如果說其他 Master 有多餘的 Slave ,集羣自動把多餘的 Slave 遷移到沒有 Slave 的 Master 中。

缺點:

(1) 批量操作是個坑,不同的 key 會劃分到不同的 slot 中,因此直接使用 mset 或者 mget 等操作是行不通的。如果執行的 key 數量比較少,就不用 mget 了,就用串行 get 操作。如果真的需要執行的 key 很多,就使用 Hashtag 保證這些 key 映射到同一臺 Redis 節點上。

(2) 資源隔離性較差,容易出現相互影響的情況。

五. Redis 高併發及熱 key 解決之道

1. 併發設置 key 及分佈式鎖

Redis 是一種單線程機制的 nosql 數據庫,基於 key-value,數據可持久化落盤。由於單線程所以 Redis 本身並沒有鎖的概念,多個客戶端連接並不存在競爭關係,但是利用 jedis 等客戶端對 Redis 進行併發訪問時會出現問題。比如多客戶端同時併發寫一個 key,一個 key 的值是 1,本來按順序修改爲 2,3,4,最後是 4,但是順序變成了 4,3,2,最後變成了 2。使用分佈式鎖防止併發設置 Key 的原理及代碼見:使用 Redis 實現分佈式鎖及其優化,另外一種方式是使用消息隊列, 把並行讀寫進行串行化。

2. 熱 key 問題

熱 key 問題說來也很簡單,就是瞬間有幾十萬的請求去訪問 redis 上某個固定的 key,從而壓垮緩存服務的情情況。其實生活中也是有不少這樣的例子。比如 XX 明星結婚。那麼關於 XX 明星的 Key 就會瞬間增大,就會出現熱數據問題。那麼如何發現熱 KEY 呢:

1. 憑藉業務經驗,進行預估哪些是熱 key

2. 在客戶端進行收集

3. 在 Proxy 層做收集

4. 用 redis 自帶命令(monitor 命令、hotkeys 參數)

  1. 自己抓包評估

解決方案:

  1. 利用二級緩存,比如利用 ehcache,或者一個 HashMap 都可以。在你發現熱 key 以後,把熱 key 加載到系統的 JVM 中。

2. 備份熱 key,不要讓 key 走到同一臺 redis 上。我們把這個 key,在多個 redis 上都存一份。可以用 HOTKEY 加上一個隨機數(N,集羣分片數)組成一個新 key。

3. 熱點數據儘量不要設置過期時間,在數據變更時同步寫緩存,防止高併發下重建緩存的資源損耗。可以用 setnx 做分佈式鎖保證只有一個線程在重建緩存,其他線程等待重建緩存的線程執行完,重新從緩存獲取數據即可。

3. 緩存穿透

緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,但是出於容錯的考慮,如果從存儲層查不到數據則不寫入緩存層。緩存穿透將導致不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護後端存儲的意義。造成緩存穿透的基本有兩個。第一,業務自身代碼或者數據出現問題,第二,一些惡意攻擊、爬蟲等造成大量空命中,下面我們來看一下如何解決緩存穿透問題。解決緩存穿透的兩種方案:

1)緩存空對象

緩存空對象會有兩個問題:

第一,空值做了緩存,意味着緩存層中存了更多的鍵,需要更多的內存空間 (如果是攻擊,問題更嚴重),比較有效的方法是針對這類數據設置一個較短的過期時間,讓其自動剔除。

第二,緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有一定影響。例如過期時間設置爲 5 分鐘,如果此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時可以利用消息系統或者其他方式清除掉緩存層中的空對象。

2)布隆過濾器攔截

如下圖所示,在訪問緩存層和存儲層之前,將存在的 key 用布隆過濾器提前保存起來,做第一層攔截。如果布隆過濾器認爲該用戶 ID 不存在,那麼就不會訪問存儲層,在一定程度保護了存儲層。有關布隆過濾器的相關知識,可以參考:布隆過濾器,可以利用 Redis 的 Bitmaps 實現布隆過濾器,GitHub 上已經開源了類似的方案,讀者可以進行參考:redis bitmaps 實現布隆過濾器

緩存空對象和布隆過濾器方案對比

4. 緩存雪崩

數據未加載到緩存中,或者緩存同一時間大面積的失效,從而導致所有請求都去查數據庫,導致數據庫 CPU 和內存負載過高,甚至宕機。

可以從以下幾個方面防止緩存雪崩:

1)保證緩存層服務高可用性

和飛機都有多個引擎一樣,如果緩存層設計成高可用的,即使個別節點、個別機器、甚至是機房宕掉,依然可以提供服務,例如前面介紹過的 Redis Sentinel 和 Redis Cluster 都實現了高可用。

2)Redis 備份和快速預熱

Redis 備份保證 master 出問題切換爲 slave 迅速能夠承擔線上實際流量,快速預熱保證緩存及時被寫入緩存,防止穿透到庫。

3)依賴隔離組件爲後端限流並降級

無論是緩存層還是存儲層都會有出錯的概率,可以將它們視同爲資源。作爲併發量較大的系統,假如有一個資源不可用,可能會造成線程全部 hang 在這個資源上,造成整個系統不可用。降級在高併發系統中是非常正常的:比如推薦服務中,如果個性化推薦服務不可用,可以降級補充熱點數據,不至於造成前端頁面是開天窗。

在實際項目中,我們需要對重要的資源 (例如 Redis、 MySQL、 Hbase、外部接口) 都進行隔離,讓每種資源都單獨運行在自己的線程池中,即使個別資源出現了問題,對其他服務沒有影響。但是線程池如何管理,比如如何關閉資源池,開啓資源池,資源池閥值管理,這些做起來還是相當複雜的,這裏推薦一個 Java 依賴隔離工具 Hystrix(https://github.com/Netflix/Hystrix),如下圖所示。

4)提前演練

在項目上線前,演練緩存層宕掉後,應用以及後端的負載情況以及可能出現的問題,在此基礎上做一些預案設定。

5. 緩存預熱

緩存預熱就是系統上線前,將相關的緩存數據直接加載到緩存系統。這樣就可以避免上線後在用戶請求的時候,先查詢數據庫,然後再將數據緩存的問題!用戶直接查詢事先被預熱的緩存數據!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/b85s_NYHY9MpZ6Yq1N1AiQ