redis(2)---redis基本數據類型及常見命令
Redis的魅力
緩存大致可以分為兩類,一種是應用內緩存,比如Map(簡單的數據結構),以及EH Cache(Java第三方庫),另一種就是緩存組件,比如Memached,Redis;Redis(remote dictionary server)是一個基於KEY-VALUE的高性能的
存儲系統,通過提供多種鍵值數據類型來適應不同場景下的緩存與存儲需求
存儲結構
大家一定對字典類型的數據結構非常熟悉,比如map ,通過key value的方式存儲的結構。 redis的全稱是remote dictionary server(遠程字典服務器),它以字典結構存儲數據,並允許其他應用通過TCP協議讀寫字典中的內容。數
Redis常用命令網址:
http://redisdoc.com
數據類型
字符串類型
字符串類型是redis中最基本的數據類型,它能存儲任何形式的字符串,包括二進制數據。你可以用它存儲用戶的郵箱、json化的對象甚至是圖片。一個字符類型鍵允許存儲的最大容量是512M
內部數據結構
在Redis內部,String類型通過 int、SDS(simple dynamic string)作為結構存儲,int用來存放整型數據,sds存放字節/字符串和浮點型數據。在C的標準字符串結構下進行了封裝,用來提升基本操作的性能,同時也充分利用已有的
C的標準庫,簡化實現邏輯。我們可以在redis的源碼中【sds.h】中看到sds的結構如下;
redis3.2分支引入了五種sdshdr類型,目的是為了滿足不同長度字符串可以使用不同大小的Header,從而節省內存,每次在創建一個sds時根據sds的實際長度判斷應該選擇什麽類型的sdshdr,不同類型的sdshdr占用的內存空
間不同。這樣細分一下可以省去很多不必要的內存開銷,下面是3.2的sdshdr定義
`struct __attribute__ ((__packed__)) sdshdr8 { 8表示字符串最大長度是2^8-1 (長度為255)` `uint8_t len;//表示當前sds的長度(單位是字節)`` uint8_t alloc; //表示已為sds分配的內存大小(單位是字節)`` unsigned char flags; //用一個字節表示當前sdshdr的類型,因為有sdshdr有五種類型,所 以至少需要3位來表示 000:sdshdr5,001:sdshdr8,010:sdshdr16,011:sdshdr32,100:sdshdr64。 高5位用不到所以都為0。`` char buf[];//sds實際存放的位置``};`
sdshdr8的內存布局
列表類型
列表類型(list)可以存儲一個有序的字符串列表,常用的操作是向列表兩端添加元素或者獲得列表的某一個片段。列表類型內部使用雙向鏈表實現,所以向列表兩端添加元素的時間復雜度為O(1), 獲取越接近兩端的元素速度就越快。
這意味著即使是一個有幾千萬個元素的列表,獲取頭部或尾部的10條記錄也是很快的
內部數據結構
redis3.2之前,List類型的value對象內部以linkedlist或者ziplist來實現, 當list的元素個數和單個元素的長度比較小的時候,Redis會采用ziplist(壓縮列表)來實現來減少內存占用。否則就會采用linkedlist(雙向鏈表)結構。
redis3.2之後,采用的一種叫quicklist的數據結構來存儲list,列表的底層都由quicklist實現。這兩種存儲方式都有優缺點,雙向鏈表在鏈表兩端進行push和pop操作,在插入節點上復雜度比較低,但是內存開
銷比較大; ziplist存儲在一段連續的內存上,所以存儲效率很高,但是插入和刪除都需要頻繁申請和釋放內存;quicklist仍然是一個雙向鏈表,只是列表的每個節點都是一個ziplist,其實就是linkedlist和ziplist的結合,quicklist
中每個節點ziplist都能夠存儲多個數據元素,在源碼中的文件為【quicklist.c】,在源碼第一行中有解釋為:Adoubly linked list of ziplists意思為一個由ziplist組成的雙向鏈表;
hash類型
數據結構
map提供兩種結構來存儲,一種是hashtable、另一種是前面講的ziplist,數據量小的時候用ziplist. 在redis中,哈希表分為三層,分別是,源碼地址【dict.h】
dictEntry
管理一個key-value,同時保留同一個桶中相鄰元素的指針,用來維護哈希桶的內部鏈;
typedef struct dictEntry { void *key; union { //因為value有多種類型,所以value用了union來存儲 void *val; uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next;//下一個節點的地址,用來處理碰撞,所有分配到同一索引的元素通 過next指針鏈接起來形成鏈表key和v都可以保存多種類型的數據 } dictEntry;
dictht
實現一個hash表會使用一個buckets存放dictEntry的地址,一般情況下通過hash(key)%len得到的值就是buckets的
索引,這個值決定了我們要將此dictEntry節點放入buckets的哪個索引裏,這個buckets實際上就是我們說的hash表。dict.h的dictht結構中table存放的就是buckets的地址
typedef struct dict { dictType *type;//dictType裏存放的是一堆工具函數的函數指針, void *privdata;//保存type中的某些函數需要作為參數的數據 dictht ht[2];//兩個dictht,ht[0]平時用,ht[1] rehash時用 long rehashidx; //當前rehash到buckets的哪個索引,-1時表示非rehash狀態 int iterators; //安全叠代器的計數。 } dict;
比如我們要講一個數據存儲到hash表中,那麽會先通過murmur計算key對應的hashcode,然後根據hashcode取模得到bucket的位置,再插入到鏈表中 img
集合類型
集合類型中,每個元素都是不同的,也就是不能有重復數據,同時集合類型中的數據是無序的。一個集合類型鍵可以存儲至多232-1個 。集合類型和列表類型的最大的區別是有序性和唯一性
集合類型的常用操作是向集合中加入或刪除元素、判斷某個元素是否存在。由於集合類型在redis內部是使用的值為空的散列表(hash table),所以這些操作的時間復雜度都是O(1).
數據結構
Set在的底層數據結構以intset或者hashtable來存儲。當set中只包含整數型的元素時,采用intset來存儲,否則,采用hashtable存儲,但是對於set來說,該hashtable的value值用於為NULL。通過key來存儲元素
有序集合
有序集合類型,顧名思義,和前面講的集合類型的區別就是多了有序的功能在集合類型的基礎上,有序集合類型為集合中的每個元素都關聯了一個分數,這使得我們不僅可以完成插入、刪除
和判斷元素是否存在等集合類型支持的操作,還能獲得分數最高(或最低)的前N個元素、獲得指定分數範圍內的元素等與分數有關的操作。雖然集合中每個元素都是不同的,但是他們的分數卻可以相同
數據結構
zset類型的數據結構就比較復雜一點,內部是以ziplist或者skiplist+hashtable來實現,這裏面最核心的一個結構就是skiplist,也就是跳躍表
歸納總結補充Redis底層數據結構:
字符串(比較簡單)
列表(list)
對應兩種實現方法:一種壓縮列表(ziplist),一種雙向循環鏈表
當列表中存儲的數據量比較小的時候,采用壓縮列表,滿足以下兩個條件:
(1)列表中保存的單個數據(可能是字符串類型的)小於64字節
(2)列表中的數據個數少於512個
壓縮列表: 不是基礎數據結構,而是redis自己設計的一種數據儲存結構。類似數組,通過一片連續的內存空間來存儲數據。不過,他與數組不同的是,它允許存儲的數據大小不同。
這種數據結構,一方面比較節省內存,另一方面又可以支持不同數據類型的存儲。而且,因為數據存儲在一片連續的內存空間(對緩存非常友好),通過鍵來獲取值為列表類型的數據,讀取的效率也非常高。
當存儲數據不滿組上面兩個條件,則用雙向鏈表實現(上述已描述)。
字典(hash)
字典類型數據用來存儲一組數據對。每個數據對包含鍵值兩部分。字典實現方式。一、壓縮列表 二、散列表
當字典中存儲的數據量比較小的時候,采用壓縮列表,滿足以下兩個條件:
(1)字典中保存的鍵和值的大小都要小於64字節
(2)字典中的鍵值對個數少於512個
不滿足則使用散列表,redis使用MurmurHash2()這種運行速度快,隨機性好的哈希算法作為哈希函數。對於哈希沖突,redis使用鏈表法來解決。除此之外,redis還支持散列表的動態擴容、縮容。
當數據動態增加後,散列表的裝載因子會不停的變大。為了避免散列表的性能的下降,當裝載因子大於1時,redis會觸發擴容,將散列表擴大為原來的大小的2倍左右。
當數據動態減少後,散列表的裝載因子會不停的變小。為了避免散列表的性能的下降,當裝載因子小於0.1時,redis會觸發索容,將散列表縮小為原來的大小的2倍左右。
而擴容、縮容需要大量的數據搬遷和哈希值的重新計算,比較耗時。針對這個問題,redis使用了漸進式擴容縮容策略,將數據的搬移分批進行,避免了大量數據一次性搬移導致的服務停頓。
漸進式擴容縮容策略:為了解決一次性擴容耗時過多的情況,我們可以將擴容操作穿插在插入操作的過程中,分配完成。當裝載因子達到閾值後,我們只申請新空間,但不將老的數據搬移到新散列表中。
當有新數據要插入時,直接插入到新散列表中,並且從老的散列表中拿出一個數據放入到新散列表中。每次插入一個數據時,重復上面過程,把數據一點一點進行搬移。用這樣均攤的方法,任何情況下,插入一個數據的時間復雜度為O(1)
期間的查詢操作,謙容性考慮,先從新的散列表中查詢,沒找到再去老的散列表中查找。
哈希表解決哈希沖突的方法:一、開放尋址法(基於數組,內存友好序列化簡單,但刪除麻煩先標記,容易沖突) 當數據量比較小、裝載因子小的時候適用,比如:ThreadLocalMap適用 開放尋址法解決散列沖突問題。
二、鏈表法 適用於存儲大對象、大數據量的散列表,比起開放尋址法更加靈活,支持優化策略,比如用紅黑樹、跳表代替鏈表(當鏈表過大時)。
散列表的設計:
要求:
支持快速的查詢、插入、刪除操作
內存占用合理,不能浪費過多的內存
性能穩定
設計:
定義一個合適的散列函數即哈希函數
定義裝載因子閾值,設計動態擴容縮容策略
選擇合適的散列沖突解決辦法
集合和有序集合 都是基於hash的
redis(2)---redis基本數據類型及常見命令