1. 程式人生 > >深入理解redis

深入理解redis

本文將主要從Redis適用範圍,與Memcached, Java容器對比,核心功能(Pipelining,

Pub/Sub,LRU,Transactions, Persistence, Replication),分散式架構設計,Cluster,

內部實現及資料結構來深入瞭解Redis,適用於已經瞭解並有Redis操作經驗之程式設計師。

1. Redis介紹 

REmote DIctionary Server(Redis)是一個開源(BSD協議)的,使用ANSI C語言編寫,

基於記憶體in-memory資料儲存結構,可以當作NoSQL資料庫,快取和訊息代理來使用。支援

多種資料結構,包括string, hashes, lists, set, sorted sets, bitmaps, hyperloglogs等。此外,

Redis還支援內建replication, LRU (Least Recently Used )演算法管理快取,Pub/Sub,部分

原子性操作,簡單事務支援,基於磁碟的資料持久化以及3.0實現了分散式Redis支援HA,

久違的Redis Cluster。從2013年5月開始,Redis的開發由Pivotal公司主持。

2. Redis之父Antirez

 

飲水思源,Redis之父Salvatore Sanfilippo, 一名來自義大利西西里島(Sicily)出生於

1977年的程式設計師,網名Antirez, 常居住於Catania。Antirez的IT職業生涯開始於系統管理員,

IT安全,並於Web 2.0的年代創立一家web網路公司,主要開發社交應用。之後,在一次實時

統計分析產品開發中,為了節省成本以及高效能擴充套件性,Antirez意識到需要一種支援多種複雜

資料結構的in-memory資料庫,並且支援快速操作。Redis就此誕生並開源。之後,VM慧眼相

中了Redis, 並僱傭Antirez去全職開發,而之後又衍生出Pivotal公司,Antirez則繼續主持Redis

開發。

3. Redis適用場景 

Redis通常被大家稱為資料結構伺服器,一種輕量級K/V資料儲存,如我們上文介紹,支援豐富

的資料型別。Redis以其速度而聞名,這使得其稱為某一特定領域適用的最優選擇。

那請問大家是在什麼場景下選用Redis呢?有小夥伴說大多數情況是大量資料需要快取吧。

那又請問這種情況為什麼不用Memcached呢?又或者乾脆選擇Java自帶容器ArrayList, HashMap,

ConcurrentHashMap, HashSet呢?我們帶著這兩個問題往下看。

 

3.1 網際網路/社交

Redis的使用者多為網際網路公司如Twitter, Weibo(微博), Uber, StackOverflow, Airbnb, Alibaba等。

對於社交網站Twitter, Weibo,這些網站主要使用Redis進行高效的社交關係管理,快取使用者好友,

粉絲,關注等,並且可以通過Redis內建資料模型快速計算共同好友等。

 

我們先看各大社交網站Twitter, Weibo(微博是國內最大的Redis使用者)。這些網站的每個使用者都有

好友,粉絲,關注等,並且可以相互檢視共同好友等。Redis可以用來幫助高效管理這些社交關係, 

如求共同好友:

如上,Redis很方便的使用有序集合的交集功能實現。其中user:100000:follow為使用者1的粉絲列表,

user:200000:follow為使用者2的粉絲列表,out:100000:200000為交集共同好友列表。

又如另一個常用場景,電商各種計數,如商品維度計數(關注度,喜歡數,評論數,瀏覽數等)。

可以採用Redis的Hash型別,並藉助其原子性操作來維護計數。

使用者維度計數(動態,關注數,粉絲數,發帖數等)

當然除了這些網際網路用途,Redis最常用的功能還是被當作快取。提到快取,大家當然會想到memcached。

Redis v.s. Memcached

  • 資料型別與操作:Redis擁有更多豐富的資料結構支援與操作,而Memcached則需客戶端自己處理並進行網路互動

 

  • 記憶體使用率:簡單K/V儲存,Memcached記憶體利用率更高(使用了slab與大小不同的chunk來管理記憶體),而如果採用Redis Hash來儲存則其組合壓縮,記憶體利用率高於Memcached

  • 效能:總體來說,二者效能接近;Redis使用了單核(單執行緒IO複用,封裝了AeEvent事件處理框架,實現了epoll,kqueue,select),Memcached採用了多核,各有利弊;當資料大於100K的時候,Memcached效能高於Redis

  • 資料持久化:Redis支援資料檔案持久化,RDB與AOF兩種策略;Memcached則不支援

  • 分散式:Memcached本身並不支援伺服器端分散式,客戶端只能藉助一致性雜湊分散式演算法來實現Memcached分散式儲存;當然Redis也是從3.0版本開始才支援伺服器端cluster的,重要的是現在支援了。

  • 其他方面:Redis提供其他一些功能,如Pub/Sub, Queue, 簡單Transacation, Replication等。

 

好了,千言不抵一圖:


Redis v.s. HashMap

作為單機資料快取,Java自帶容器也是備選方案之一。我們進行壓力測試,採用ConcurrentHashMap, Memcached(假設近似於Redis), MySQL進行Benchmark。

具體資料如下:

 

可以看出,HashMap(上圖顯示為一根線)整體操作插入/查詢/移除都 25 - 90倍快於Memcached/Redis; 而Memcached/Redis又 5 -12 倍快於MySQL InnoDB,符合預期。究其原因,Java容器單機記憶體直接讀取,無網路開銷,無須序列化等。

而反之之所以要用Redis的原因與好處則包括:

 

HashMap單機受限於記憶體容量,而正是Redis分散式之優勢

  • HashMap當資料量超過一定限制後,需要妥善管理堆記憶體,不然會造成記憶體溢位或者Memory Leak;Redis則具備了檔案永續性,以及Failover達到HA.

  • HashMap只能受限於本機,而Redis天生分散式,可以讓多個App Server訪問,負載均衡。

所以Redis適合全部資料都在記憶體的場景包括需要臨時持久化,尤其作為快取來使用,並支援對快取資料進行簡單處理計算;如涉及Redis與RDBMS雙向同步的話,則需要引入一些複雜度。

 

4. Redis核心功能  

 

4.1 Pipelining 

Reids是一個TCP客戶端-伺服器模式,使用了應請求/響應協議,客戶端與伺服器的互動是阻塞的方式。

如下一個請求/響應事例,客戶端發起4個INCR命令,服務端依次響應:

  • Client:INCR X

  • Server:1

  • Client:INCR X

  • Server:2

  • Client:INCR X

  • Server:3

  • Client:INCR X

  • Server:4

客戶端與伺服器通過網路連線,影響效能的關鍵因素往返時間(RTT Round Trip Time)則受限於網路

速度,如在網速較慢情況下RTT需要250毫秒,哪怕伺服器效能卓越可以每秒處理1萬個請求,然而整體

則客戶端受網路制約只能每秒處理4個請求。即便使用了loopback介面節省RTT時間,但如果需要大量的

寫操作,整體效能仍不可觀。

 

Redis引入了批處理,管道(Pipelining),即客戶端可以一次傳送多個命令給伺服器,無須等待伺服器的

返回,而伺服器端則回將請求放入一個有序的管道中,在執行完成後,一次性將返回值傳送回客戶端。

上述事例在使用Pipelining時如下,客戶端通過緩衝區,一次性批量傳送請求INCR,服務端則處理後統一

返回結果:

  • Client:INCR X

  • Client:INCR X

  • Client:INCR X

  • Client:INCR X

  • Server:1

  • Server:2

  • Server:3

  • Server:4


4.2 Pub/Sub

Reids提供瞭解耦訊息的釋出/訂閱(Pub/Sub)通訊模式,Pub/Sub採用事件作為基本通訊機制,支援時間

(非同時),空間(無須知道具體位置)與同步(可非同步模式)等解耦合,而釋出與訂閱則通過通道

(channel)來通訊。

Antirez稱,最初引入Pub/Sub是應使用者需求,並且Redis本身的架構已經支援Pub/Sub, 所以只用了150行

程式碼就把Pub/Sub已實現的內部功能暴露稱Pub/Sub API。

上圖為較早版本中釋出訊息的一段程式碼片段,可以看出訊息儲存在一個List連結串列資料結構中,訊息的型別為

messagebulk。另外,Redis也支援萬用字元模式匹配的訂閱方式。

 

Pub/Sub v.s. 訊息中介軟體

Reids的Pub/Sub與訊息中介軟體相比,並不支援持久化,如系統宕機,網路問題等都會造成訊息丟失;

Redis的訊息多用於實時性較高的訊息推送,並不保證可靠性。Redis的訊息也無法支援水平擴充套件,

如通過增加consumer或者訂閱者分組之類進行負載均衡。

效能方面,Redis的Pub/Sub與常用訊息中介軟體如著名的RabbitMQ比較,則伯仲之間。

 

上圖以處理10萬個訊息為例,在釋出訊息方面,RabbitMQ的耗時為Redis的75%;訂閱消費訊息

RabbitMQ耗時為Redis的86%,可見整體效能Redis已與訊息中介軟體相近。

 

4.3 LRU 

LRU(Least Recently Used) 最近最久未使用演算法,是多數快取系統當記憶體受限時自動清理舊資料的

常用常用演算法之一。當Redis使用記憶體達到配置maxmemory時,Redis會根據配置的policy進行資料置

換處理,其中策略包括如下:

  • noenviction(不清除)

  • allkeys-lru(從所有資料集選擇最近最少用)

  • volatile-lru(從設定過期時間的資料集選擇最近最少用)

  • allkeys-random(所有資料集隨機選取淘汰),

  • volatile-random(以設定過期時間資料集中隨機),

  • volatile-ttl(從已設定過期過期時間的資料集中選擇,非LRU)

Redis出於效能及節約記憶體考量,採用的並非嚴格意義LRU演算法演算法,而是近似的LRU演算法,即Redis通過取樣

一小部分鍵,然後在樣本池中進行LRU。當然在Redis 3.0中,演算法進一步改進為維護回收候選鍵池,改善了性

能同時更接近於LRU演算法行為。

 

下圖為官網提供Reids LRU效能測試,分別為理論LRU演算法,10個樣本的Redis 3.0 LRU演算法,以及5個樣本的

Redis 2.8與3.0演算法的表現,其中淺灰色表示被置換出去的key,灰色為沒有被置換的key,綠色為新增的key。

測試演算法較簡單,首先預設匯入一定數目key,之後從第一個key遍歷至最後一個key(相當於按時間順序使用過了

當前遍歷的key),之後再增加50%的數目觸發清理策略。

可以看出,理論演算法為最舊的50%被替換;同樣樣本為5情況下,3.0表現要優於2.8,3.0則更接近於理論值,

2.8演算法則略為遜色。

 

4.4 Transactions 

 

絕大多數NoSQL選擇不支援事務,而Reids以命令方式(MULTI, EXEC, DISCARD, WA WATCH)提供了簡單的

類/偽事務支援,之所以稱之為類/偽事務,是因為Redis的只保證了事務必須ACID的C(一致性),I(隔離性),

並不保證A(原子性)與永續性(D),Redis事務甚至不支援回滾操作。

 

實際上,Redis的事務提供了一種將多個命令打包並置入事務佇列,之後批量一次性,有序的按照先進先出

(FIFO)的順序執行機制。事務在執行過程中不會被中斷,所有命令命令執行之後,事務才結束。

 

Transaction v.s. Pipelining

Redis的Transaction與Pipelining都是批量執行命令,其主要差別為:

  • Pipelining主要是提供了網路層面的優化,客戶端通過快取多個命令並按批次傳送給服務端處理以節省網路RTT(Round Trip TIme)時間開銷,但這些命令並不保證事務性(Redis的單執行緒保證了單個命令本身是原子的,但多個client可以發起多個命令序列執行)。

  • Transaction則保證了一個client發起的事務中的命令可以連續的執行,中間不會插入其它client的命令,而此則受益於Redis的單執行緒模式,很容易實現。

所以,Transaction與Pipelining不但不衝突,對於批量命令甚至可以結合起來使用以達到網路優化及事務性

保證雙重收效。

 

4.5 Persistence

Redis提供了兩種常用的不同級別的磁碟持久化方式,RDB與AOF(Append-Only-File)。RDB持久化在指定

的時間間隔生成資料集的時間點快照(Snapshot), AOF則類似RDMS的binlog日誌。 

由於單執行緒執行,Redis的RDB在備份時為不影響系統正常使用,藉助Linux的fork命令及copy on write機制,

在fork出的子程序中進行備份寫RDB檔案。RDB的備份相對簡單並且檔案小,但無法保證資料的完整性,

如Redis在RDB的間隔時間內宕機,則面臨丟失期間的資料。RDB較適合定期的歸檔備份及方便災難恢復。

而AOF則提供了更好的永續性,Redis會將每個命令都追加到AOF檔案中,並提供了三種持久化策略:

  • no fsync at all:Redis不呼叫fsync持久化, 作業系統決定同步時機(多數30秒)

  • fsync everysecond:每秒(延遲會兩秒)進行一次fsync將緩衝區資料寫入磁碟

  • fsync always:每個命令都進行同步,持久化寫入磁碟,安全但較慢

AOF後臺執行的方式與RDB類似,也是藉助fork開啟子程序進行寫入AOF檔案。為了減少AOF檔案的大小,

Redis提了了檔案壓縮命令,另外支援日誌重寫,後臺重構AOF檔案以減少所需命令。

 

4.6 Replication

 

Redis支援Master/Slave主從配置,實現弱一致性,支援負載均衡,提高HA。Redis的Master可以對應

多個Slave, Slave也可以級聯多個Slave。通常Master負責讀寫服務,Slave負責讀服務,Master與Slave

間靠Replication定期來進行同步。

Redis的主從結構如下圖所示DAG(有向無環圖)

Slave會定期給Master傳送SYNC命令進行同步,如果第一次連線則進行全量同步否則增量同步。

Master則啟動後臺快找程序saving來收集最近修改資料集所有命令並將其用db file傳輸給Slave。

 

 

另外,Redis從2.8版本開始支援中斷後(網路斷開等故障)的斷點續傳功能,無須重新同步。

Master維護一個記憶體緩衝區,主從伺服器都維護一個複製偏移量(offset)和master run id,

當斷開重新連線後,Master判斷兩個master run id是否相同,並根據指定的偏移量繼續斷點續傳。

 

 

5. Redis系統架構  

 

Sentinel架構

Reids Sentinel誕生於2012年(Redis 2.4版本),建議在單機Redis或者客戶端模式Cluster的時候

(非 3.0版本Redis Cluster)採用,作為HA, Failover來使用。

Sentinel主要提供了叢集管理,包括監控,通知,自動故障恢復。如上圖,當其中一個master無法

正常工作時,Sentinel將把一個Slave提升為Master, 從而自動恢復故障。而Sentinel本身也做到了

分散式,可以部署多個Sentinel例項來監控Redis例項(建議基數,至少3個Sentinel例項來監控一

組Redis Master/Slaves),多個Sentinel程序間通過Gossip協議來確定Master是否宕機,通過

Agreement協議來決定是否執行故障自動遷移以及重新選主,整體設計類似ZooKeeper

(應該是作者參考了當年的ZK吧?)。

 

Cluster架構

Redis Cluster開始設計於2011年(早於Sentinel),正式誕生於2015年愚人節,Redis 3.0,其中之

苦辣酸甜只有Antirez自己知道。

 

 

如上圖,從3.0開始, Redis從一個單純的NoSQL記憶體資料庫變成了一個真正分散式NoSQL資料庫。

概括來說,所謂分散式即支援資料分片,並且自動管理故障恢復(failover)與備份(replication)。

 

如上圖Redis Cluster採用了無中心結構,每個節點都儲存,共享資料和叢集狀態,每個節點與

其它所有節點通訊,使用Gossip協議來傳播及發現新節點,通過分割槽來提供一定程度可用性,

當某個node的Master宕機時,Cluste會自動選舉一個Slave形成一個新的Master,這裡應該是

借鑑,重用了Sentinel的功能。

另外,Redis Cluster並沒有使用通常的一致性雜湊, 而引入雜湊槽的概念,Cluster中固定有

16384個slot, 每個key通過CRC16校驗後對16384取模來決定其對應slot的位置,而每個node

負責一部分的slot管理,當node變化時,動態調整slot的分佈,而資料則無須挪動。對於客戶端

來說,client可以向任意一個例項請求,Cluster會自動定位需要訪問的slot。

上圖查詢路由過程中,我們隨機發送到任意一個Redis例項,這個例項會按照上文提到的CRC16

校驗後取模定位,並轉發至正確的Redis例項中。

 

然而,完全去中心化的架構同時也失去了一些靈活與總控能力,如可通過引入中央控制的自動發

現節點的變化及時Rebalance,分割槽粒度的備份,故障時分割槽自動調整,Gossip訊息的通訊開銷,

路由表維護等。

值得一提的是,Redis的企業版/商業版Redis Labs Enterprise Cluster(RLEC)則似乎解決了我們

上述問題,如引入了中央控制Cluster Manager來管理,監控,分片遷移等工作, 引入高效能Proxy

隱藏幕後的路由實現等。

 

當然,前提是商業化付費版本了,我們也期待未來的開源Redis逐步可以引入這些概念。

 

6. Redis 內部資料結構  

 

Redis支援豐富的資料型別,並提供了大量簡單高效的功能。為了高效使用Redis,開發設計人員需要對Redis的資料結構進行選型,如選擇連結串列還是集合等。 下面我們快速瞭解一下Redis內部資料結構及其程式碼實現。Redis的底層資料結構總覽圖:

 

 

 

上圖列出了Redis內部底層的一些重要資料結構,包括List, Set, Hash, String等。

來看幾個比較核心的的資料結構。

 

Redis Object 

 

Redis 3.x後的redisObject如下圖所示:

 

redisObject定義中使用了位欄位(bit filed)。簡單來說,redisObject定義了型別,編碼方式,

LRU時間,引用計數,*ptr指向實際儲存值指標。

  • type: redisObject的型別,字串,列表,集合,有序集,雜湊表等

  • encoding: 底層實現結構,字串,整數,跳躍表,壓縮列表等

  • ptr:實際指向儲存值的資料結構

舉個具體例子,redisObject{type: REDIS_LIST, encoding:REDIS_ENCODING_LINKEDLIST}, 

這個物件是Redis列表,其值儲存在一個連結串列中,ptr指標指向這個列表。

用慣了JVM的虛擬機器,我們也來回顧一下C或者Redis是如何管理物件的。總體而言,Redis自己實現物件管理機制,並基於引用計數的垃圾回收。

毫無疑問的直接malloc開闢記憶體空間,設定type,encoding,引用計數預設1,設定預設LRU。可以看出refcount,猜測其內部是基於引用計數來管理釋放記憶體空間的。事實要以程式碼為準。


果不其然,Redis提供了incrRefCount與decrRefCount來管理物件跟蹤物件的引用,  當減少引用時檢測計數器為是否需要釋放記憶體物件。

RedisDB    

 

RedisDB內部資料結構,封裝了資料庫層面的資訊:

 

從redisDB來看,幾個重要屬性:

  • id:資料庫內部編號,僅供內部操作使用,如AOF等

  • dict:存放整個資料庫的鍵值對,鍵為字串,值為Redis的資料結構,如List, Set, Hash等。

  • expires:鍵的過期時間

具體程式碼如下:

 

程式碼註釋較為清晰,其中long long是C99標準新加的,64位長整型。

 

RedisServer    

 

RedisServer程式碼在Redis 3.0支援Cluster後變得較複雜,我們只列出部分程式碼。

 

總體來說包含如下幾個大部分:

  • 通用部分:如pid程序id,資料庫指標,命令字典表,Sentinel模式標誌位等

  • 網路資訊:如port TCP監聽埠,Cluster Bus監聽socket, 已使用slot數量,Active的客戶端列表, 當前客戶端,Slaves列表等。

  • 其它資訊:如AOF資訊,統計資訊, 配置資訊(如已經配置總db數量dbnum等),日誌資訊,Replication配置,Pub/Sub, Cluster資訊,Lua指令碼資訊配置等等。

 

Redis Hash

Redis的雜湊表/字典是其核心資料結構之一,值得深入研究。Redis Hash資料結構, Hash新建立時,

在不影響效率情況下,Redis預設使用zipmap作為底層實現以節省空間,只有當size超出一定限制後

(hash-max-zipmap-entries ),Redis才會自動把zipmap轉換為下圖Hash Table。


 

上圖字典的底層實現為雜湊表,每個字典包含2個雜湊表,ht[0], ht[1], 1號雜湊表是在rehash過程中才

使用的。而雜湊表則由dictEntry構成。

 

程式碼層面,每個字典包含了3個內部資料結構:

  • Dict:字典的根結構,包含了2個dictht,其中2作為rehashing之用

  • Dictht:包含了linkedlist dictEntry

  • DictEntry:包含了3個數據結構(double/uint64_6/int64t)的連結串列,類似Java HashMap中的Entry結構

 

Hash演算法

目前Redis中引入了一些經典雜湊演算法,而HashTable則主要為以下兩種:

 

  • MurmurHash2 32bit演算法:著名的非加密型雜湊函式,能產生32位或64位雜湊值,最新版本為

  • MurmurHash3。該演算法針對一個字串進行雜湊,可表現較強離散性。

  • 基於djb演算法實現雜湊演算法:該演算法較為簡單,同樣是將字串轉換為雜湊值。主要利用字串中

  • 的ASCII碼與一個隨機seed,進行變換得到雜湊值。

評估一個雜湊演算法的優劣,主要看其雜湊值的離散均勻效果以及消除衝突程度。Redis在HashTable中

引入的上述兩種演算法不失簡單高效,離散均勻。

 

Rehash

類似Java中的HashMap, 當有新鍵值對新增到Redis字典時,有可能會觸發rehash。Redis中處理雜湊

碰撞的方法與Java一樣,都是採用連結串列法,整個雜湊表的效能則依賴於它的大小size和它已經儲存節點

數量used的比率。

比率在1:1時,雜湊表的效能最好,如果節點數量比雜湊表大小大很多的話,則整個雜湊表就退化成多個

連結串列,其效能優勢全無。

 

上圖的雜湊表,平均每次失敗查詢需要訪問5個節點。為了保持高效效能,在不修改鍵值對情況下,

需要進行rehash,目標是將ratio比率維持在1:1左右。

 

Ratio = Used / Size

 

rehash觸發條件:

  • 自然rehash:ratio >= 1, 且變數dict_can_resize為真

  • 強制rehash:ratio大於dict_force_resize_ratio(v 3.2.1版本為5)

rehash執行過程:

  • 建立ht[1]並分配至少2倍於ht[0] table的空間

  • 將ht[0] table中的所有鍵值對遷移到ht[1] table

  • 將ht[0]資料清空,並將ht[1]替換為新的ht[0]

Redis雜湊為了避免整個rehash過程中服務被阻塞,採用了漸進式的rehash,即rehash程式啟用後,並不是

馬上執行直到完成,而是分多次,漸進式(incremental)的完成。同時,為了保證併發安全,在執行rehash

中間執行新增時,新的節點會直接新增到ht[1]而不是ht[0], 這樣保證了資料的完整性與安全性。

另一方面,雜湊的Rehash在還提供了創新的(相對於Java HashMap)收縮(shrink)字典,當可用節點遠遠

大於已用節點的時候,rehash會自動進行收縮,具體過程與上面類似以保證比率始終高效使用。

 

7. 總結

本文從Redis的一些核心功能,適用範圍與本質原因,分散式架構設計,Cluster,重要內部資料結構及程式碼片段等層面深入瞭解,在Redis使用方面,開發及設計人員通常需要對Redis有一定深度理解,對業務模型進行相應的資料結構選型,還需提前預估記憶體使用,是否需要持久化,分散式等。總體來說,Redis非常適合與傳統關係型資料庫結合使用,做高效能資料快取,輕量級訊息佇列及跨機器共享記憶體。希望本文對Redis開發人員有所幫助,碰到一些實現細節,建議還是要深入其原始碼一探究竟。有關其它Redis的命令操作及其它高階功能則可參考Redis官網。

關於redis的幾點舞誤區

https://www.cnblogs.com/renjiaqi/p/8994715.html