這是一個系列的文章,打算把Redis的 基礎數據結構、 高級數據結構、 持久化的方式以及 高可用的方式都講一遍,公衆號會比其餘的平臺提早更新,感興趣的能夠提早關注,「 SH的全棧筆記」,下面開始正文。
若是你是一個有經驗的後端或者服務器開發,那麼必定據說過Redis,其全稱叫Remote Dictionary Server。是由C語言編寫的基於Key-Value的存儲系統。說直白點就是一個內存數據庫,既然是內存數據庫就會遇到若是服務器意外宕機形成的數據不一致的問題。redis
這跟不少遊戲服務器也是同樣的,感興趣的能夠參考我以前的文章遊戲服務器和Web服務器的區別。其數據首先會流向內存,基於快速的內存讀寫來實現高性能,而後按期將內存的數據中的數據落地。Redis其實也是這麼個流程,基於快速的內存讀寫操做,單機的Redis甚至可以扛住10萬的QPS。數據庫
Redis除了高性能以外,還擁有豐富的數據結構,支持大多數的業務場景。這也是其爲何如此受歡迎的緣由之一,下面咱們就來看一看Redis有哪些基礎數據類型,以及他們底層都是怎麼實現的。後端
其基礎數據類型有String
、List
、Hash
、Set
、Sorted Set
,這些都是經常使用的基礎數據類型,能夠看到很是豐富,幾乎可以知足大部分的需求了。其實還有一些高級數據結構,咱們在這章裏暫時先不提,只聊基礎的數據結構。數組
String能夠說是最基礎的數據結構了, 用法上能夠直接和Java中的String掛鉤,你能夠把String類型用於存儲某個標誌位,某個計數器,甚至狠一點,序列化以後的JSON字符串都行,其單個key限制爲512M。其常見的命令爲get
、set
、incr
、decr
、mget
。安全
set
賦值爲0,再調用incr
。固然若是該key的類型不能作加法運算,例如字符串,就會拋出錯誤可能大多數的人只是到用一用的地步,這也無可厚非,可是若是是做爲一個對技術有追求的開發,或者說你有想近大廠的想法,必定要有刨根問底的精神。只有當你真正知道一個東西的底層原理時,你遇到問題時才能提供給你更多的思路去解決問題。接下來咱們就來聊一下Redis中String底層是如何實現的。bash
咱們知道Redis是用C語言寫的,可是Redis卻沒有直接使用,而是本身實現了一個叫SDS(Simple Dynamic String)的結構來實現字符串,結構以下。服務器
struct sdshdr { // 記錄buf中已使用的字節數量 int len; // 記錄buf中未使用的字節數量 int free; // 字節數組,用於保存字符串 char buf[]; }
爲何Redis要本身實現SDS而不是直接用C的字符串呢?主要是由於如下幾點。微信
\0
,時間複雜度爲O(n),而SDS直接維護了長度的變量,取長度的時間複雜度爲O(1)free
變量解決了這個問題。向buf數組中寫入數據時,會先判斷剩餘的空間是否足夠塞入新數據,若是不夠,SDS就會從新分配緩衝區,加大以前的緩衝區。且加大的長度等於新增的數據的長度\0
會被截斷,而SDS不會由於數據中出現了\0
而截斷字符串,換句話說,不會由於一些特殊的字符影響實際的運算結果能夠結合下面的圖來理解SDS。網絡
總結一下就是上面列表的四個小標題,爲了減小獲取字符串長度開銷、避免緩衝區溢出、空間預分配&空間惰性釋放和保證二進制安全。數據結構
List也是一個使用頻率很高的數據結構,其設計到的命令太多了,就不像String那樣一個一個演示了,感興趣的你們能夠去搜一搜。命令有lpush、lpushx、rpush、rpushx、lpop、rpop、lindex、linsert、lrange、llen、lrem、lset、ltrim、rpoplpush、brpoplpush、blpop、brpop,其都是對數組中的元素的操做。
List的用途我認爲主要集中在如下兩個方面。
- 看成普通列表存儲數據(相似於Java的ArrayList)
- 用作異步隊列
普通列表這個天然沒必要多說,其中存放的必然業務中須要的數據,下面來着重聊一下異步隊列。
啥玩意,List還能當成隊列來玩?
List除了能被用作隊列,還能看成棧來使用。在上面介紹了不少操做List命令,當咱們用rpush/lpop組合命令的時候,實際上就是在使用一個隊列,而當咱們用rpush/rpop命令組合的時候,就是在使用一個棧。lpush/rpop和lpush/lpop是同理的。
假設咱們用的是rpush來生產消息,當咱們的程序須要消費消息的時候,就使用lpop從異步隊列中消費消息。可是若是採用這種方式,當隊列爲空時,你可能須要不停的去詢問隊列中是否有數據,這樣會形成機器的CPU資源的浪費。
因此你能夠採起讓當前線程Sleep一段時間,這樣的確能夠節省一部分CPU資源。可是你可能就須要去考慮Sleep的時間,間隔過短,CPU上下文切換可能也是一筆不小的開銷,間隔太長,那麼可能形成這條消息被延遲消費(不過都用異步隊列了,應該能夠忽略這個問題)。
除了Sleep,還有沒有其餘的方式?
有,答案是blpop。當咱們使用blpop去消費時,若是當前隊列是空的,那麼此時線程會阻塞住,直到下面兩種condition。
比起Sleep一段時間,實時性會好一點;比起輪詢,對CPU資源更加友好。
在Redis3.2以前,Redis採用的是ZipList(壓縮列表)或者LinkedList(鏈表)。當List中的元素同時知足每一個元素的小於64字節
和List元素個數小於512個
時,存儲的方式爲ZipList。但凡是有一個條件沒知足就會轉換爲LinkedList。
而在3.2以後,其實現變成了QuickList(快速列表)。LinkedList因爲是較爲基礎的東西,此處就不贅述了。
ZipList採用連續的內存緊湊存儲,不像鏈表那樣須要有額外的空間來存儲前驅節點和後續節點的指針。按照其存儲的區域劃分,大體能夠分爲三個部分,每一個部分也有本身的劃分,其詳細的結構以下。
header ziplist的頭部信息
entries 存儲實際節點的信息
若是採用鏈表的存儲方式,鏈表中的元素由指針相連,這樣的方式可能會形成必定的內存碎片。而指針也會佔用額外的存儲空間。而ZipList不會存在這些狀況,ZipList佔用的是一段連續的內存空間。
可是相應地,ZipList的修改操做效率較爲低下,插入和刪除的操做會設計到頻繁的內存空間申請和釋放(有點相似於ArrayList從新擴容),且查詢效率一樣會受影響,本質上ZipList查詢元素就是遍歷鏈表。
在3.2版本以後,list
的實現就換成了QuickList。QuickList將list分紅了多個節點,每個節點採用ZipList存儲數據。
其用法就跟Java中的HashMap中同樣,都是往map中去丟鍵值對。
基礎的命令以下:
其實大多數狀況下的使用跟HashMap是差很少的,沒有什麼較爲特殊的地方。
hash的底層實現也是有兩種,ZipList和HashTable。但具體採用哪種與Redis的版本無關,而與當前hash中所存的元素有關。首先當咱們建立一個hash的時候,採用的ZipList進行存儲。隨着hash中的元素增多,達到了Redis設定的閾值,就會轉換爲HashTable。
其設定的閾值以下:
ZipList上面咱們專門簡單分析了一下,理解這個設定應該也比較容易。當ZipList中的元素過多的時候,其查詢的效率就會變得低下。而HashTable的底層設計其實和Java中的HashMap差很少,都是經過拉鍊法解決哈希衝突。具體的能夠參考從基礎的使用來深挖HashMap這篇文章。
Set的概念能夠與Java中的Set劃等號,用於存儲一個不包含重複元素的集合。
其主要的命令以下,key表明redis中的Set,member表明集合中的元素。
sadd key member [...]
將一個或者多個元素加入到集合中,若是有已經存在的元素會忽略掉。srem key member [...]
將一個或者多個元素從集合中移除,不存在的元素會被忽略掉smembers key
返回集合中的全部成員dismember key member
判斷member在key中是否存在,若是存在則返回1,若是不存在則返回0scard key
返回集合key中的元素的數量move source destination member
將元素從source集合移動到destination集合。若是source中不包含member,則不會執行任何操做,當且僅當存在纔會從集合中移出。若是destination已經存在元素則不會對destination作任何操做。該命令是原子操做。spop key
隨機刪除並返回集合中的一個元素srandmember key
與spop同樣,只不過不會將元素刪除,能夠理解爲從集合中隨機出一個元素來。sinterstore destination key [...]
與sinter相似,可是會將得出的結果存到destination中。sunionstore destination key [...]
sdiffstore destination key [...]
與sdiff相似,可是會將得出的結果存到destination中。咱們知道Java中的Set有多種實現。在Redis中也是,有IntSet和HashTable兩種實現,首先初始化的時候使用的是IntSet,而知足以下的條件時,就會轉換成HashTable。
上面已經簡單的介紹了HashTable了,因此這裏只聊聊IntSet。
intset底層是一個數組,既然數據結構是數組,那麼存儲數據就能夠是有序的,這也使得intset的底層查詢是經過二分查找來實現。其結構以下。
struct intset { // 編碼方式 uint32_t encoding; // 集合包含元素的數量 uint32_t length; // 存儲元素的數組 int8_t contents[]; }
與ZipList相似,IntSet也是使用的一連串的內存空間,可是不一樣的是ZipList能夠存儲二進制的內容,而IntSet只能存儲整數;且ZipList存儲是無序的,IntSet則是有序的,這樣一來,元素個數相同的前提下,IntSet的查詢效率會更高。
其與Set的功能大體相似,只不過在此基礎上,能夠給每個元素賦予一個權重。你能夠理解爲Java的TreeSet。與List、Hash、Set同樣,其底層的實現也有兩種,分別是ZipList和SkipList(跳錶)。
初始化Sorted Set的時候,會採用ZipList做爲其實現,其實很好理解,這個時候元素的數量不多,採用ZipList進行緊湊的存儲會更加的節省空間。當期達到以下的條件時,就會轉換爲SkipList:
下面的命令中,key表明zset的名字;4表明score,也就是權重;而member就是zset中的key的名稱。
zadd key 4 member
用於增長元素zcard key
用於獲取zset中的元素的數量zrem key member [...]
刪除zset中一個或者多個keyzincrby key 1 member
給key的權重值加上score的值(也就是1)zscore key member
用於獲取指定key的權重值zrange key 0 -1
獲取zset中全部的元素,zrange key 0 -1 withscores
獲取全部元素和權重,withscores
參數的做用是決定是否將權重值也一塊兒返回。其返回的元素按照從小到大排序,若是元素具備相同的權重,則會按照字典序排序。zrevrange key 0 -1 withscores
,其與zrange
相似,只不過zrevrange
按照從大到小排序。zrangebyscore key 1 5
,返回key中權重在區間(1, 5]範圍內元素。固然也可使用withscores
來將權重值一併返回。其元素按照從小到大排序。1表明min,5表明max,他們也能夠分別是-inf和inf,當你不知道key中的score區間時,就可使用這個。還有一個相似於SQL中的limit的可選參數,在此就不贅述。除了可以對其中的元素添加權重以外,使用ZSet還能夠實現延遲隊列。
延遲隊列用於存放延遲任務,那什麼是延遲隊列呢?
舉個很簡單的例子, 你在某個電商APP中下訂單,可是沒有付款,此時它會提醒你,「訂單若是超過1個小時沒有支付,將會自動關閉」;再好比在某個活動結束前1個小時給用戶推送消息;再好比訂單完成後多少天自動確認收貨等等。
用人話解釋一遍,那就是過會纔要乾的事情。
那ZSet怎麼實現這個功能?
其實很簡單,就是將任務的執行時間設置爲ZSet中的元素權重,而後經過一個後臺線程定時的從ZSet中查詢出權重最小的元素,而後經過與當前時間戳判斷,若是大於當前時間戳(也就是該執行了)就將其從ZSet中取出。
那,怎麼取?
其實我看不少講Redis實現延遲隊列的博客都沒有把這個怎麼取講清楚,到底該用什麼命令,傳什麼參數。咱們使用zrangebyscore
命令來實現,還記得-inf和inf嗎,其全稱是infinity,分別表示無限小和無限大。
因爲咱們並不知道延遲隊列當中的score(也就是任務執行時間)的範圍,因此咱們能夠直接使用-inf和inf,完整命令以下。
zrangescore key -inf inf limt 0 1 withscores
仍是有點用,那ZSet底層是咋實現的呢?
上面已經講過了ZipList,就不贅述,下面聊聊SkipList。
SkipList存在於zset(Sorted Set)的結構中,以下:
struct zset { // 字典 dict *dict; // 跳錶 zskiplist *zsl; }
而SkipList的結構以下:
struct zskiplist { // 表頭節點和表尾節點 struct zskiplistNode *header, *tail; // 表中節點的數量 unsigned long length; // 表中層數最大的節點的層數 int level; }
不知道你們是否有想過,爲何Redis要使用SkipList來實現ZSet,而不用數組呢?
首先ZSet若是數組存儲的話,因爲ZSet中存儲的元素是有序的,存入的時候須要將元素放入數組中對應的位置。這樣就會對數組進行頻繁的增刪,而頻繁的增刪在數組中效率並不高,由於涉及到數組元素的移動,若是元素插入的位置是首位,那麼後面的全部元素都要被移動。
因此爲了應付頻繁增刪的場景,咱們須要使用到鏈表。可是隨着鏈表的元素增多,一樣的會出現問題,雖然增刪的效率提高了,可是查詢的效率變低了,由於查詢元素會從頭至尾的遍歷鏈表。全部若是有什麼方法可以提高鏈表的查詢效率就行了。
因而跳錶就誕生了。基於單鏈表,從第一個節點開始,每隔一個節點,創建索引。其實也是單鏈表。只不是中間省略了節點。
例如存在個單鏈表 1 3 4 5 7 8 9 10 13 16 17 18抽象以後的索引爲 1 4 7 9 13 17
若是要查詢16只須要在索引層遍歷到13,而後根據13存儲的下層節點(真實鏈表節點的地址),此時只須要再遍歷兩個節點就能夠找到值爲16的節點。
因此能夠從新給跳錶下一個定義,鏈表加多級索引的結構,就是跳錶
在跳錶中,查詢任意數據的時間複雜度是O(logn)。時間複雜度跟二分查找是同樣的。能夠換句話說,用單鏈表實現了二分查找。但這也是一種用空間換時間的思路,並非免費的。
關於Redis的基礎數據結構和其底層的原理就簡單的聊到這裏,以後的幾篇應該會聊聊Redis的高可用和其對應的解決方案,感興趣的能夠持續關注,公衆號會比其餘的平臺都先更新。
往期文章:
若是你以爲這篇文章對你有幫助,還麻煩點個贊,關個注,分個享,留個言
也能夠微信搜索公衆號【SH的全棧筆記】,固然也能夠直接掃描二維碼關注