簡述Redis設計與實現
Redis是一個高性能的key-value的非關系型數據庫,Redis是運行在內存中的一種數據庫,但是它也可以持久化到磁盤中,Redis的實現有着更為復雜的數據結構並且提供對他們的原子性操作。
-
Redis的優勢
-
Redis支持數據的持久化,可以將內存中的數據保存在磁盤中,以便redis服務器重啟的時候再次加載使用。
-
Redis不僅僅支持簡單的key-value類型數據的存儲,同時還提供了其它的數據結構存儲例如:list、set、zset、hash等。
-
Redis支持三種模式部署分別是:單機部署、主從部署和集群部署;為數據備份提供了支持。
-
Redis性能極高,它的讀速度是110000次/s,寫的速度是80000次/s,
-
Redis的所有操作都是原子性的,要么成功,要么失敗,單個/多個操作都是原子性的,Redis也支持事務,但是Redis的事務是基於Multi和Exec指令的。
-
Redis還支持發布定於進行消息隊列特性等。
-
-
Redis的應用場景
- 緩存、排行榜、計數器、共享Session、分布式鎖、消息隊列、位操作、購物車、全局Id、限流、抽獎、點贊、簽到、商品標簽、關注等。
-
Redis的實現
Redis的實現在內部使用了大量的特性,例如:內存數據庫、高效的數據結構、網絡I/O多路復用、虛擬內存機制等。
-
數據結構
Redis使用了大量的數據結構,它的數據結構分別有以下幾種:簡單動態字符串、鏈表、字典、跳躍表、整數集合、壓縮列表等
- 簡單動態字符串
Redis的字符串沒有使用C語言的字符串,而是自己構建了一種簡單的動態字符串(SDS),C字符串並不記錄自身的長度信息,程序必須遍歷整個字符串,對遇到的每個字進行計數,直到遇到代表字符串結尾的空字符為 止,這個操作的復雜度為O(N)。而Redis的SDS獲取長度的時間復雜度是O(1),除此之外C語言的字符串還會存在緩沖區溢出,而SDS的空間分配策略解決了緩沖區溢出問題,當SDS API需要對SDS進行修改時,API會先檢查SDS空間是否滿足所需要求,不滿足的會進行自動擴容,不需要手動修改SDS空間大小,SDS也采用了空間與分配原則SDS每次進行擴容是(當前長度*2)+1,它也是二進制安全,兼容部分C字符串函數。
- 鏈表
鏈表這種數據結構在Redis中應用非常廣泛,鏈表提供了高效的節點重排能力,和順序的節點訪問方式,因為Redis使用的C語言沒有內置鏈表數這種數據結構,所以Redis自己實現了鏈表。Redis鏈表的實現實用的是雙向鏈表,在節點中分別存儲了value、next、prev;總的來說redis鏈表的特性如下:
- 雙向鏈表:從某個節點獲取前/后節點時間復雜度都是O(1);
- 無環鏈表:表頭節點的prev指針和表尾節點的next指針都指向了null;
- 帶有鏈表長度的計數器:Redis的鏈表有一個屬性來對鏈表節點進行計數,所以獲取鏈表節點的數量的復雜度為O(1)。
- 字典
字典在Redis中應用也非常廣泛,Redis的數據庫就是使用字典來作為底層實現的,對Redis的增、刪、改、查操作也是構建在對字典的操作之上。因為C語言沒有內置字典這種數據結構,所以Redis也是自己構建了字典的實現。Redis的字典使用哈希表作為底層的實現,一個哈希表里面可以有多個哈希表節點,每個哈希表節點保存了字典中的一個鍵值對。針對哈希沖突Redis使用的是拉鏈法《拉鏈法其實說白了就是在一個hash值上形成了一個單鏈表數組每次新的添加到最前面》解決沖突,,使用MurmurHash2算法來計算鍵的哈希值。
- Rehash
哈希表保存的鍵值對會逐漸地增多或者減少,Redis為了讓哈希表的 負載因子(load factor)維持在一個合理的范圍之內,當哈希表保存的鍵值對數量太多或 者太少時,程序需要對哈希表的大小進行相應的擴展或者收縮。擴展和收縮哈希表的工作可以通過執行rehash(重新散列)操作來完成。
- 跳表(skiplist)
Redis查詢快是因為它使用了跳表這種有序的數據結構,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。跳表支持平均O(logN)、最壞O(N)的復雜度節點的查找,
,每個跳躍表節點的層高是隨機生成一個介於1-32之間的值作為level數組的大小,這個大小就是層的高度。Redis的跳表實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用於保存跳 躍表信息(比如表頭節點、表尾節點、長度),而zskiplistNode則用於表示跳躍表節點。在同一個跳躍表中,多個節點可以包含相同的分值,但每個節點的成員對象必須是 唯一的。跳躍表中的節點按照分值大小進行排序,當分值相同時,節點按照成員對象的大小 進行排序。跳表的原理
- 整數集合
整數集合(intset)是集合鍵的底層實現之一,當一個集合只包含整數值元素,並且 這個集合的元素數量不多時,Redis就會使用整數集合作為集合鍵的底層實現。它可以保存類型為 int16_t、int32_t或者int64_t的整數值,並且保證集合中不會出現重復元素。整數集合僅支持升級操作,不支持降級操作。升級操作為整數集合帶來了操作上的靈活性,並且盡可能的節約了內存。
- 壓縮列表(ziplist)
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只包含少量列 表項,並且每個列表項要么就是小整數值,要么就是長度比較短的字符串,那么Redis就會使用壓縮列表來做列表鍵的底層實現。壓縮列表是Redis為了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的 順序型(sequential)數據結構。一個壓縮列表可以包含任意多個節點(entry),每個節 點可以保存一個字節數組或者一個整數值。壓縮列表可以包含多個節點,每個節點可以保存一個字節數組或者整數值。添加新節點到壓縮列表,或者從壓縮列表中刪除節點,可能會引發連鎖更新操作, 但這種操作出現的幾率並不高。
- 對象
在前面我們陸續介紹了Redis用到的所有主要數據結構,比如簡單動 態字符串(SDS)、雙端鏈表、字典、壓縮列表、整數集合等等。Redis並沒有直接使用這些數據結構來實現鍵值對數據庫,而是基於這些數據結構創 建了一個對象系統,這個系統包含字符串對象、列表對象、哈希對象、集合對象和有序集合對象這五種類型的對象,每種對象都用到了至少一種我們前面所介紹的數據結構。Redis的對象系統還實現了基於引用計數技術的內存回收機制,當程序不再使用某個對象的時候,這個對象所占用的內存就會被自動釋放;另外,Redis還通過引 用計數技術實現了對象共享機制,這一機制可以在適當的條件下,通過讓多個數據庫鍵共享同一個對象來節約內存。Redis的對象帶有訪問時間記錄信息,該信息可以用於計算數據庫鍵的空轉時長在服務器啟用了maxmemory功能的情況下,空轉時長較大的那些鍵可能會優先被服務器刪除。Redis每種對象都至少使用了兩種不同的編碼。
-
列表對象(list)的編碼可以是ziplist或者linkedlist。當列表對象可以同時滿足以下兩個條件時,列表對象使用ziplist編碼:列表對象保存的所有字符串元素的長度都小於64字節;列表對象保存的元素數量小於512個;不能滿足這兩個條件的列表對象需要使用 linkedlist編碼。
-
Hash對象哈希對象的編碼可以是ziplist或者hashtable。當哈希對象可以同時滿足以下兩個條件時,哈希對象使用ziplist編碼:哈希對象保存的所有鍵值對的鍵和值的字符串長度都小於64字節;哈希對象保存的鍵值對數量小於512個;不能滿足這兩個條件的哈希對象需要使用hashtable編碼。
-
集合對象(set) 的編碼可以是intset或者hashtable。當集合對象可以同時滿足以下兩個條件時,對象使用intset編碼:集合對象保存的所有元素都是整數值;集合對象保存的元素數量不超過512個。不能滿足這兩個條件的集合對象需要使用hashtable編碼。
-
有序集合對象(zset) 有序集合的編碼可以是ziplist或者skiplist。ziplist編碼的壓縮列表對象使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨 在一起的壓縮列表節點來保存,第一個節點保存元素的成員(member),而第二個元素 則保存元素的分值(score)。
-
- 簡單動態字符串
-
內存回收
因為C語言並不具備自動內存回收功能,所以Redis在自己的對象系統中構建了一個引 用計數(reference counting)技術實現的內存回收機制,通過這一機制,程序可以通過跟 蹤對象的引用計數信息,在適當的時候自動釋放對象並進行內存回收。
-
對象共享
Redis除了實現引用計數內存回收機制之外,對象的引用計數屬性還帶有對象共享的作用。假設數據庫中保存了整數值100的鍵不只有鍵A和鍵B兩個,而是有一百個,那 么服務器只需要用一個字符串對象的內存就可以保存原本需要使用一百個字符串對象的內存才能保存的數據。
-
對象的空轉時長
redisObject結構包含的一個屬性為lru屬性,該屬性記錄了對象最后一次被命令程序訪問的時間,對象會記錄自己的最后一次被訪問的時間,這個時間可以用於計算對象的空轉時間。
-
-
Redis鍵的過期策略
眾所周知,Redis是可以設置過期時間,那么Redis有幾種鍵過期策略呢?我們來說一下;Redis有三種過期策略分別是:定時過期、惰性過期、和定期過期。
- 定時過期
Redis的定時過期其實就是給每一個鍵添加一個定時器,這樣的做法無疑是會占用大量的CPU。
- 惰性過期
Redis的惰性過期其實就是在你每次訪問一個鍵的時候去檢查一下是否過期,如果過期了就刪除,這個策略會對CPU占用比較小,但是對內存不太友好,因為如果有很多一次性的key不在訪問,則會占用大量內存。
- 定期過期
Redis的定期過期其實就是一個定時任務,每隔一段時間去掃描一定數量的過期時間字典並清除過期的key。
- 定時過期
-
Redis的持久化、多機Redis
-
Redis持久化
Redis持久化分為兩種,分別是RDB持久化和AOF持久化,Redis默認開啟的是RDB持久化,如果開啟了AOF持久化那么AOF的持久化優先級高於RDB持久化。
- RDB持久化
因為Redis是內存數據庫,一旦服務器重啟你的保存的數據就會丟失,但是並不代表Redis沒有持久化機制,為了解決這個問題Redis提供了RDB持久化功能,這個功能可以定時將某個時間點的數據保存到磁盤中,RDB除了保存數據外,還有一些過期鍵操作,也就是說RDB持久化只會保存沒有過期的鍵,RDB持久化的實現原理是兩個命令分別是SAVE和BGSAVE命令,SAVE這個命令會阻塞線程因為它是同步的;BASAVE是異步他其實就是派生出一個子進程由子進程負責創建RDB文件進行持久化。 Redis服務重啟之后RDB文件載入到內存會阻塞服務器狀態,一直等待RDB文件載入完成,才會放棄阻塞。
- AOF持久化
Redis的AOF持久化是通過保存Redis服務器所執行的寫命令來進行數據庫狀態,AOF文件中的所有命令都以Redis命令請求協議的格式保存。命令請求會先保存到AOF緩沖區里面,之后再定期寫入並同步到AOF文件。
- RDB持久化
-
Redis多機部署
- 主從復制
Redis的主從復制分為兩種,第一種是RDB文件傳輸全量復制,也就是說你的slave第一次加入集群,那么它會將Master的RDB文件傳輸過來,進行全量復制,第二種是增量復制,增量復制采用的是命令傳輸模式。如果slave斷開之后再次重新連接Master,在Master同步備份有偏移量,如果沒有超出這個偏移量會繼續增量同步,如果超出偏移量則需要人為進行一次全量同步。,主從復制不會阻塞Master的I/O讀寫,全量復制是在Master上fork一個子進程執行bgsaveRDB文件。
- Sentinel(哨兵模式)
Redis的哨兵模式是高可用的一種解決方案,哨兵用來監控Redis服務器的運行狀態,如果一個集群中的Master下線之后Sentinel會重新選舉服務器,Sentinel會選擇出一個數據偏移量最大的作為主服務器,如果有多個相同偏移量的則按照運行ID進行排序選擇一台ID最小的作為主服務器。
- 集群
Redis集群是Redis提供的分布式數據庫方案,集群通過分片(sharding)來進行數據共享,並提供復制和故障轉移功能。它是Redis分布式數據庫一種的方案,集群提供了高可用性,一部分節點失效不影響處理請求,數據自動切分到多個節點中,集群部署至少需要3個Master和3個Slave。
- 主從復制
-
-
總結
本篇主要是簡述了Redis的整體的一個設計和他的部分功能的實現和使用的數據結構,以及主從復制和哨兵選擇Master節點按照什么選擇的等等。本系列有兩篇,下一篇將着重Redis的經典面試題。