Redis五大基本數(shù)據(jù)結(jié)構(gòu)詳解

0 概述


1 通用命令












2 數(shù)據(jù)結(jié)構(gòu)和內(nèi)部編碼

2.1 Redis 沒(méi)有傳統(tǒng)關(guān)系型數(shù)據(jù)庫(kù)的Table 模型

schema 所對(duì)應(yīng)的db僅以編號(hào)區(qū)分。同一個(gè)db 內(nèi),key 作為頂層模型,它的值是扁平化的。也就是說(shuō)db 就是key的命名空間
key的定義通常以“:” 分隔,如:Article:Count:1
我們常用的Redis數(shù)據(jù)類(lèi)型有:string、list、set、map、sorted-set

2.2 redisObject通用結(jié)構(gòu)

Redis中的所有value 都是以object 的形式存在的,其通用結(jié)構(gòu)如下

typedef struct redisObject {
    unsigned [type] 4;
    unsigned [encoding] 4;
    unsigned [lru] REDIS_LRU_BITS;
    int refcount;
    void *ptr;
} robj;
  • type 指的是前面提到的 string、list 等類(lèi)型
  • encoding 指的是這些結(jié)構(gòu)化類(lèi)型具體的實(shí)現(xiàn)方式,同一個(gè)類(lèi)型可以有多種實(shí)現(xiàn)
    e.g. string 可以用int 來(lái)實(shí)現(xiàn),也可以使用char[] 來(lái)實(shí)現(xiàn);list 可以用ziplist 或者鏈表來(lái)實(shí)現(xiàn)
  • lru 表示本對(duì)象的空轉(zhuǎn)時(shí)長(zhǎng),用于有限內(nèi)存下長(zhǎng)時(shí)間不訪問(wèn)的對(duì)象清理
  • refcount 對(duì)象引用計(jì)數(shù),用于GC
  • ptr 指向以encoding 方式實(shí)現(xiàn)這個(gè)對(duì)象實(shí)際實(shí)現(xiàn)者的地址
    如:string 對(duì)象對(duì)應(yīng)的SDS地址(string的數(shù)據(jù)結(jié)構(gòu)/簡(jiǎn)單動(dòng)態(tài)字符串)

3 單線(xiàn)程



4 string-字符串

Redis中的string 可以表示很多語(yǔ)義

  • 字節(jié)串(bits)
  • 整數(shù)
  • 浮點(diǎn)數(shù)

這三種類(lèi)型,redis會(huì)根據(jù)具體的場(chǎng)景完成自動(dòng)轉(zhuǎn)換,并且根據(jù)需要選取底層的承載方式
例如整數(shù)可以由32-bit/64-bit、有符號(hào)/無(wú)符號(hào)承載,以適應(yīng)不同場(chǎng)景對(duì)值域的要求


也能是 json 串或者 xml 結(jié)構(gòu)

4.1 內(nèi)存結(jié)構(gòu)

在Redis內(nèi)部,string的內(nèi)部以 int、SDS(簡(jiǎn)單動(dòng)態(tài)字符串 simple dynamic string)作為存儲(chǔ)結(jié)構(gòu)

  • int 用來(lái)存放整型
  • SDS 用來(lái)存放字節(jié)/字符和浮點(diǎn)型SDS結(jié)構(gòu)

(1)SDS

typedef struct sdshdr {
    // buf中已經(jīng)占用的字符長(zhǎng)度
    unsigned int len;
    // buf中剩余可用的字符長(zhǎng)度
    unsigned int free;
    // 數(shù)據(jù)空間
    char buf[];
}
結(jié)構(gòu)圖

上面存儲(chǔ)的內(nèi)容為“Redis”,Redis采用類(lèi)似C語(yǔ)言的存儲(chǔ)方法,使用'\0'結(jié)尾(僅僅是定界符)
上面SDS的free 空間大小為0,當(dāng)free > 0時(shí),buf中的free 區(qū)域的引入提升了SDS對(duì)字符串的處理性能,可以減少處理過(guò)程中的內(nèi)存申請(qǐng)和釋放次數(shù)

(2) buf 的擴(kuò)容與縮容

當(dāng)對(duì)SDS 進(jìn)行操作時(shí),如果超出了容量。SDS會(huì)對(duì)其進(jìn)行擴(kuò)容,觸發(fā)條件如下:

  • 字節(jié)串初始化時(shí),buf的大小 = len + 1,即加上定界符'\0'剛好用完所有空間
  • 當(dāng)對(duì)串的操作后小于1M時(shí),擴(kuò)容后的buf 大小 = 業(yè)務(wù)串預(yù)期長(zhǎng)度 * 2 + 1,也就是擴(kuò)大2倍。
  • 對(duì)于大小 > 1M的長(zhǎng)串,buf總是留出 1M的 free空間,即2倍擴(kuò)容,但是free最大為 1M。

(3)字節(jié)串與字符串

SDS中存儲(chǔ)的內(nèi)容可以是ASCII 字符串,也可以是字節(jié)串
由于SDS通過(guò)len 字段來(lái)確定業(yè)務(wù)串的長(zhǎng)度,因此業(yè)務(wù)串可以存儲(chǔ)非文本內(nèi)容
對(duì)于字符串的場(chǎng)景,buf[len] 作為業(yè)務(wù)串結(jié)尾的'\0' 又可以復(fù)用C的已有字符串函數(shù)

(4)SDS編碼的優(yōu)化

value 在內(nèi)存中有2個(gè)部分:redisObject和ptr 指向的字節(jié)串部分。在創(chuàng)建時(shí),通常要分別為2個(gè)部分申請(qǐng)內(nèi)存,但是對(duì)于小字節(jié)串,可以一次性申請(qǐng)。























String類(lèi)型的value基本操作

除此之外,string 類(lèi)型的value還有一些CAS的原子操作,如:get、set、set value nx(如果不存在就設(shè)置)、set value xx(如果存在就設(shè)置)。

String 類(lèi)型是二進(jìn)制安全的,也就是說(shuō)在Redis中String類(lèi)型可以包含各種數(shù)據(jù),比如一張JPEG圖片或者是一個(gè)序列化的Ruby對(duì)象。一個(gè)String類(lèi)型的值最大長(zhǎng)度可以是512M。

在Redis中String有很多有趣的用法

  • 把String當(dāng)做原子計(jì)數(shù)器,這可以使用INCR家族中的命令來(lái)實(shí)現(xiàn):INCR, DECR, INCRBY
  • 使用APPEND命令來(lái)給一個(gè)String追加內(nèi)容。
  • 把String當(dāng)做一個(gè)隨機(jī)訪問(wèn)的向量(Vector),這可以使用GETRANGESETRANGE命令來(lái)實(shí)現(xiàn)
  • 使用GETBITSETBIT方法,在一個(gè)很小的空間中編碼大量的數(shù)據(jù),或者創(chuàng)建一個(gè)基于Redis的Bloom Filter 算法。

5 List

Redis的列表類(lèi)型中存儲(chǔ)一系列String值,這些String按照插入的順序排序
Redis的List可以從頭部(左側(cè))加入元素,也可以從尾部(右側(cè))加入元素

5.1 內(nèi)存數(shù)據(jù)結(jié)構(gòu)

List 類(lèi)型的 value對(duì)象,由 linkedlist 或者 ziplist 實(shí)現(xiàn)
當(dāng) List 元素個(gè)數(shù)少并且元素內(nèi)容長(zhǎng)度不大采用ziplist 實(shí)現(xiàn),否則使用linkedlist

5.1.1 linkedlist實(shí)現(xiàn)

鏈表的代碼結(jié)構(gòu)

typedef struct list {
  // 頭結(jié)點(diǎn)
  listNode *head;
  // 尾節(jié)點(diǎn)
  listNode *tail;
  // 節(jié)點(diǎn)值復(fù)制函數(shù)
  void *(*dup)(void * ptr);
  // 節(jié)點(diǎn)值釋放函數(shù)
  void *(*free)(void *ptr);
  // 節(jié)點(diǎn)值對(duì)比函數(shù)
  int (*match)(void *ptr, void *key);
  // 鏈表長(zhǎng)度
  unsigned long len;  
} list;

// Node節(jié)點(diǎn)結(jié)構(gòu)
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;
linkedlist 結(jié)構(gòu)圖

5.1.2 ziplist實(shí)現(xiàn)

ziplist 存儲(chǔ)在連續(xù)內(nèi)存


組成結(jié)構(gòu)圖
  • zlbytes:表示ziplist 的總長(zhǎng)度
  • zltail:指向最末元素。
  • zllen:表示元素的個(gè)數(shù)。
  • entry:為元素內(nèi)容。
  • zlend:恒為0xFF,作為ziplist的定界符

從上面的結(jié)構(gòu)可以看出,對(duì)于linkedlist和 ziplist,它們的rpush、rpop、llen的時(shí)間復(fù)雜度都是O(1)
但是對(duì)于ziplist,lpush、lpop都會(huì)牽扯到所有數(shù)據(jù)的移動(dòng),時(shí)間復(fù)雜度為O(N)
但是由于List的元素少,體積小,這種情況還是可控的

對(duì)于ziplist 的每個(gè)Entry 其結(jié)構(gòu)如下



記錄前一個(gè)相鄰的Entry的長(zhǎng)度,作用是方便進(jìn)行雙向遍歷,類(lèi)似于linkedlist 的prev 指針
ziplist是連續(xù)存儲(chǔ),指針由偏移量來(lái)承載
Redis中實(shí)現(xiàn)了2種方式的實(shí)現(xiàn)

  • 當(dāng)前鄰 Entry的長(zhǎng)度小于254 時(shí),使用1字節(jié)來(lái)實(shí)現(xiàn)
  • 否則使用5個(gè)字節(jié)

這里面會(huì)有個(gè)問(wèn)題,就是當(dāng)前一個(gè)Entry的長(zhǎng)度變化時(shí),這時(shí)候有可能會(huì)造成后續(xù)的所有空間移動(dòng)。當(dāng)然這種情況發(fā)生的可能性比較小。

Entry內(nèi)容本身是自描述的,意味著第二部分(Entry內(nèi)容)包含了幾個(gè)信息:Entry內(nèi)容類(lèi)型、長(zhǎng)度和內(nèi)容本身。而內(nèi)容本身包含:類(lèi)型長(zhǎng)度部分和內(nèi)容本身部分。類(lèi)型和長(zhǎng)度同樣采用變長(zhǎng)編碼:

  • 00xxxxxx :string類(lèi)型;長(zhǎng)度小于64,0~63可由6位bit 表示,即xxxxxx表示長(zhǎng)度
  • 01xxxxxx|yyyyyyyy : string類(lèi)型;長(zhǎng)度范圍是[64, 16383],可由14位 bit 表示,即xxxxxxyyyyyyyy這14位表示長(zhǎng)度。
  • 10xxxxxx|yy..y(32個(gè)y) : string類(lèi)型,長(zhǎng)度大于16383.
  • 1111xxxx :integer類(lèi)型,integer本身內(nèi)容存儲(chǔ)在xxxx 中,只能是1~13之間取值。也就是說(shuō)內(nèi)容類(lèi)型已經(jīng)包含了內(nèi)容本身。
  • 11xxxxxx :其余的情況,Redis用1個(gè)字節(jié)的類(lèi)型長(zhǎng)度表示了integer的其他幾種情況,如:int_32、int_24等。
    由此可見(jiàn),ziplist 的元素結(jié)構(gòu)采用的是可變長(zhǎng)的壓縮方法,針對(duì)于較小的整數(shù)/字符串的壓縮效果較好

LPUSH 命令是在頭部加入一個(gè)新元素,RPUSH 命令是在尾部加入一個(gè)新元素。當(dāng)在一個(gè)空的鍵值(key)上執(zhí)行這些操作時(shí)會(huì)創(chuàng)建一個(gè)新的列表。類(lèi)似的,當(dāng)一個(gè)操作清空了一個(gè)list時(shí),這個(gè)list對(duì)應(yīng)的key會(huì)被刪除。這非常好理解,因?yàn)閺拿畹拿志涂梢钥闯鲞@個(gè)命令是做什么操作的。如果使用一個(gè)不存在的key調(diào)用的話(huà)就會(huì)使用一個(gè)空的list。

一些例子:

LPUSH mylist a   # 現(xiàn)在list是 "a"
LPUSH mylist b   # 現(xiàn)在list是"b","a"
RPUSH mylist c   # 現(xiàn)在list是 "b","a","c" (注意這次使用的是 RPUSH)

list的最大長(zhǎng)度是2^32 – 1個(gè)元素(4294967295,一個(gè)list中可以有多達(dá)40多億個(gè)元素)

從時(shí)間復(fù)雜度的角度來(lái)看,Redis list類(lèi)型的最大特性是:即使是在list的頭端或者尾端做百萬(wàn)次的插入和刪除操作,也能保持穩(wěn)定的很少的時(shí)間消耗。在list的兩端訪問(wèn)元素是非常快的,但是如果要訪問(wèn)一個(gè)很大的list中的中間部分的元素就會(huì)比較慢了,時(shí)間復(fù)雜度是O(N)

Redis的List類(lèi)型有很多有趣的用法

  • 在社交網(wǎng)絡(luò)中使用List進(jìn)行時(shí)間表建模,使用LPUSH命令在用戶(hù)時(shí)間線(xiàn)中加入新的元素,然后使用LRANGE 命令來(lái)獲得最近加入的元素。
  • 可以把LPUSHLTRIM 命令結(jié)合使用來(lái)實(shí)現(xiàn)定長(zhǎng)的列表,列表中只保存最近的N個(gè)元素
  • 在創(chuàng)建后臺(tái)運(yùn)行的工作時(shí),Lists可以作為消息傳遞原語(yǔ),例如著名的Ruby庫(kù) Resque
  • 還有很多可以使用lists來(lái)做的事,這種數(shù)據(jù)類(lèi)型支持很多命令,包括像BLPOP這樣的阻塞命令

6 Sets

Set類(lèi)似List,但是它是一個(gè)無(wú)序集合,包含的元素不重復(fù)
向集合中添加多次相同的元素,集合中只存在一個(gè)該元素。在實(shí)際應(yīng)用中,這意味著在添加一個(gè)元素前不需要先檢查元素是否存在。

支持多個(gè)服務(wù)器端命令來(lái)從現(xiàn)有集合開(kāi)始計(jì)算集合,所以執(zhí)行集合的交集,并集,差集都可以很快

set的最大長(zhǎng)度是2^32 – 1個(gè)元素(4294967295,一個(gè)set中可以有多達(dá)40多億個(gè)元素)

6.1 內(nèi)存數(shù)據(jù)結(jié)構(gòu)

Set在Redis中以intset 或 hashtable來(lái)存儲(chǔ)。Hashtable前面已經(jīng)介紹過(guò)了,對(duì)于Set,HashTable的value永遠(yuǎn)為NULL
當(dāng)Set中只包含整型數(shù)據(jù)時(shí),采用intset作為實(shí)現(xiàn)

6.1 .1 intset

intset的核心元素是一個(gè)字節(jié)數(shù)組,其中從小到大有序的存放著set的元素

typedef struct intset {
    // 編碼方式
  uint32_t enconding;
  // 集合包含的元素?cái)?shù)量
  uint32_t length;
  // 保存元素的數(shù)組    
  int8_t contents[];
} intset;
結(jié)構(gòu)圖

因?yàn)樵赜行蚺帕校許ET的獲取操作采用二分查找的方式,復(fù)雜度為O(log(N))

進(jìn)行插入操作時(shí),首先通過(guò)二分查找到要插入的位置,再對(duì)元素進(jìn)行擴(kuò)容
然后將插入位置之后的所有元素向后移動(dòng)一個(gè)位置,最后插入元素。時(shí)間復(fù)雜度為O(N)。

為了使二分查找的速度足夠快,存儲(chǔ)在content 中的元素是定長(zhǎng)的。



當(dāng)插入2018 時(shí),所有的元素向后移動(dòng),并且不會(huì)發(fā)生覆蓋的情況
并且當(dāng)Set 中存放的整型元素集中在小整數(shù)范圍[-128, 127]內(nèi)時(shí),可以大大的節(jié)省內(nèi)存空間。這里面需要注意的是:IntSet支持升級(jí),但是不支持降級(jí)。

Set 基本操作
  • 記錄唯一的事物
    比如,你想知道訪問(wèn)某個(gè)博客的IP地址,不要重復(fù)的IP,這種情況只需要在每次處理一個(gè)請(qǐng)求時(shí)簡(jiǎn)單的使用SADD命令就可以了,可以確信不會(huì)插入重復(fù)的IP

  • 表示關(guān)系
    你可以使用Redis創(chuàng)建一個(gè)標(biāo)簽系統(tǒng),每個(gè)標(biāo)簽使用一個(gè)Set來(lái)表示。然后你可以使用SADD命令把具有特定標(biāo)簽的所有對(duì)象的所有ID放在表示這個(gè)標(biāo)簽的Set中
    如果你想要知道同時(shí)擁有三個(gè)不同標(biāo)簽的對(duì)象,那么使用SINTER命令就好了

  • 你可以使用SPOP 或者 SRANDMEMBER 命令從集合中隨機(jī)的提取元素。

7 Hashes/ Maps

因?yàn)镽edis本身是一個(gè)key - valueObject的結(jié)構(gòu),Hash類(lèi)型的結(jié)構(gòu)可以理解為subkey - subvalue
這里面的subkey - subvalue只能是

  • 整型
  • 浮點(diǎn)型
  • 字符串

因?yàn)镸ap的value 可以表示整型和浮點(diǎn)型,因此Map也可以使用hincrby 對(duì)某個(gè)field的value值做自增操作

7.1 內(nèi)存數(shù)據(jù)結(jié)構(gòu)

Map可以由HashTable 和 ziplist 兩種方式來(lái)承載。對(duì)于數(shù)據(jù)量較小的Map,使用ziplist 實(shí)現(xiàn)

7.1.1 HashTable 實(shí)現(xiàn)

HashTable在Redis 中分為3 層,自底向上分別是:

  • dictEntry:管理一個(gè)field - value 對(duì),保留同一桶中相鄰元素的指針,以此維護(hù)Hash 桶中的內(nèi)部鏈
  • dictht:維護(hù)Hash表的所有桶鏈
  • dict:當(dāng)dictht需要擴(kuò)容/縮容時(shí),用戶(hù)管理dictht的遷移

dict是Hash表存儲(chǔ)的頂層結(jié)構(gòu)

// 哈希表(字典)數(shù)據(jù)結(jié)構(gòu),Redis 的所有鍵值對(duì)都會(huì)存儲(chǔ)在這里。其中包含兩個(gè)哈希表。
typedef struct dict {
    // 哈希表的類(lèi)型,包括哈希函數(shù),比較函數(shù),鍵值的內(nèi)存釋放函數(shù)
    dictType *type;
    // 存儲(chǔ)一些額外的數(shù)據(jù)
    void *privdata;
    // 兩個(gè)哈希表
    dictht ht[2];
    // 哈希表重置下標(biāo),指定的是哈希數(shù)組的數(shù)組下標(biāo)
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 綁定到哈希表的迭代器個(gè)數(shù)
    int iterators; /* number of iterators currently running */
} dict;

Hash表的核心結(jié)構(gòu)是dictht,它的table 字段維護(hù)著 Hash 桶,桶(bucket)是一個(gè)數(shù)組,數(shù)組的元素指向桶中的第一個(gè)元素(dictEntry)。

typedef struct dictht { 
    //槽位數(shù)組
    dictEntry **table; 
    //槽位數(shù)組長(zhǎng)度
    unsigned long size; 
    //用于計(jì)算索引的掩碼 
    unsigned long sizemask;
    //真正存儲(chǔ)的鍵值對(duì)數(shù)量
    unsigned long used; 
} dictht;
結(jié)構(gòu)圖

從上圖可以看出,Hash表使用的是 鏈地址法 解決Hash沖突
當(dāng)一個(gè)bucket 中的Entry 很多時(shí),Hash表的插入性能會(huì)下降,此時(shí)就需要增加bucket的個(gè)數(shù)來(lái)減少Hash 沖突

7.1.1.1 Hash表擴(kuò)容

和大多數(shù)Hash表實(shí)現(xiàn)一樣,Redis引入負(fù)載因子判定是否需要增加bucket個(gè)數(shù)
負(fù)載因子 = Hash表中已有元素 / bucket數(shù)量
擴(kuò)容之后bucket的數(shù)量是原先的2倍
目前有2 個(gè)閥值:

  • 小于1 時(shí)一定不擴(kuò)容

  • 大于5 時(shí)一定擴(kuò)容

  • 在1 ~ 5 之間時(shí),Redis 如果沒(méi)有進(jìn)行bgsave/bdrewrite 操作時(shí)則會(huì)擴(kuò)容

  • 當(dāng)key - value 對(duì)減少時(shí),低于0.1時(shí)會(huì)進(jìn)行縮容。縮容之后,bucket的個(gè)數(shù)是原先的0.5倍

7.1.2 ziplist 實(shí)現(xiàn)

這里面的ziplist 和List的ziplist實(shí)現(xiàn)類(lèi)似,都是通過(guò)Entry 存放element
和List不同的是,Map對(duì)應(yīng)的ziplist 的Entry 個(gè)數(shù)總是2的整數(shù)倍,第奇數(shù)個(gè)Entry 存放key,下一個(gè)相鄰的Entry存放value

ziplist承載時(shí),Map的大多數(shù)操作不再是O(1)了,而是由Hash表遍歷,變成了鏈表的遍歷,復(fù)雜度變?yōu)镺(N)
由于Map相對(duì)較小時(shí)采用ziplist,采用Hash表時(shí)計(jì)算hash值的開(kāi)銷(xiāo)較大,因此綜合起來(lái)ziplist的性能相對(duì)好一些





















方便單條更新,但是信息非整體,不便管理







Redis Hashes 保存String域和String值之間的映射,所以它們是用來(lái)表示對(duì)象的絕佳數(shù)據(jù)類(lèi)型(比如一個(gè)有著用戶(hù)名,密碼等屬性的User對(duì)象)

| `1` | `@cli` |

| `2` | `HMSET user:1000 username antirez password P1pp0 age 34` |

| `3` | `HGETALL user:1000` |

| `4` | `HSET user:1000 password 12345` |

| `5` | `HGETALL user:1000` |

一個(gè)有著少量數(shù)據(jù)域(這里的少量大概100上下)的hash,其存儲(chǔ)方式占用很小的空間,所以在一個(gè)小的Redis實(shí)例中就可以存儲(chǔ)上百萬(wàn)的這種對(duì)象

Hash的最大長(zhǎng)度是2^32 – 1個(gè)域值對(duì)(4294967295,一個(gè)Hash中可以有多達(dá)40多億個(gè)域值對(duì))

8 Sorted sets 類(lèi)型(有序集合類(lèi)型)

類(lèi)似于Map的key-value對(duì),但有序

  • key :key-value對(duì)中的鍵,在一個(gè)Sorted-Set中不重復(fù)
  • value : 浮點(diǎn)數(shù),稱(chēng)為 score
  • 有序 :內(nèi)部按照score 從小到大的順序排列

8.1 基本操作

由于Sorted-Set 本身包含排序信息,在普通Set 的基礎(chǔ)上,Sorted-Set 新增了一系列和排序相關(guān)的操作:


Sorted-Set的基本操作

8.2 內(nèi)部數(shù)據(jù)結(jié)構(gòu)

Sorted-Set類(lèi)型的valueObject 內(nèi)部由

  • ziplist
    作為ziplist的實(shí)現(xiàn)方式和Map類(lèi)似,由于Sorted-Set包含了Score的排序信息,ziplist內(nèi)部的key-value元素對(duì)的排序方式也是按照Score遞增排序的,意味著每次插入數(shù)據(jù)都要移動(dòng)之后的數(shù)據(jù)
    因此ziplist適用于元素個(gè)數(shù)不多,元素內(nèi)容不大的場(chǎng)景。
  • skiplist+hashtable
    對(duì)于更通用的場(chǎng)景,Sorted-Set使用sliplist來(lái)實(shí)現(xiàn)。

8.2.1 skiplist

和通用的跳表不同的是,Redis為每個(gè)level 對(duì)象增加了span 字段,表示該level 指向的forward節(jié)點(diǎn)和當(dāng)前節(jié)點(diǎn)的距離,使得getByRank類(lèi)的操作效率提升

typedef struct zskiplist {
     //表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)
     structz skiplistNode *header,*tail;
     //表中節(jié)點(diǎn)數(shù)量
     unsigned long length;
     //表中層數(shù)最大的節(jié)點(diǎn)的層數(shù)
     int level;
} zskiplist;

從上圖可以看出,每次向skiplist 中新增或者刪除一個(gè)節(jié)點(diǎn)時(shí),需要同時(shí)修改圖標(biāo)中紅色的箭頭,修改其forward和span的值
需要修改的箭頭和對(duì)skip進(jìn)行查找操作遍歷并廢棄過(guò)的路徑是吻合的
對(duì)于span的修改僅僅是+1或者-1 。skiplist 的查找復(fù)雜度平均是 O(Log(N)),因此add / remove的復(fù)雜度也是O(Log(N))。因此Redis中新增的span 提升了獲取rank(排序)操作的性能,僅需對(duì)遍歷路徑相加即可(矢量相加)

還有一點(diǎn)需要注意的是,每個(gè)skiplist的節(jié)點(diǎn)level 大小都是隨機(jī)生成的(1-32之間)。
skiplistNode的代碼結(jié)構(gòu)

typedef struct zskiplistNode {
    // 層
    struct zskiplistLevel{
        struct zskiplistNode *forward; // 前進(jìn)指針
        unsigned int span; // 跨度
    } level[];
    // 后退指針
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成員對(duì)象
    robj *obj;
}

8.2.2 hashtable

skiplist 是zset 實(shí)現(xiàn)順序相關(guān)操作比較高效的數(shù)據(jù)結(jié)構(gòu),但是對(duì)于簡(jiǎn)單的zscore操作效率并不高。Redis在實(shí)現(xiàn)時(shí),同時(shí)使用了Hashtable和skiplist,代碼結(jié)構(gòu)如下:

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

Hash表的存在使得Sorted-Set中的Map相關(guān)操作復(fù)雜度由O(N)變?yōu)镺(1)。

Redis有序集合類(lèi)型與Redis的集合類(lèi)型類(lèi)似,是非重復(fù)的String元素的集合。不同之處在于,有序集合中的每個(gè)成員都關(guān)聯(lián)一個(gè)Score,Score是在排序時(shí)候使用的,按照Score的值從小到大進(jìn)行排序。集合中每個(gè)元素是唯一的,但Score有可能重復(fù)。

使用有序集合可以很高效的進(jìn)行,添加,移除,更新元素的操作(時(shí)間消耗與元素個(gè)數(shù)的對(duì)數(shù)成比例)。由于元素在集合中的位置是有序的,使用get ranges by score或者by rank(位置)來(lái)順序獲取或者隨機(jī)讀取效率都很高。(本句不確定,未完全理解原文意思,是根據(jù)自己對(duì)Redis的淺顯理解進(jìn)行的翻譯)訪問(wèn)有序集合中間部分的元素也非常快,所以可以把有序集合當(dāng)做一個(gè)不允許重復(fù)元素的智能列表,你可以快速訪問(wèn)需要的一切:獲取有序元素,快速存在測(cè)試,快速訪問(wèn)中間的元素等等。

簡(jiǎn)短來(lái)說(shuō),使用有序集合可以實(shí)現(xiàn)很多高性能的工作,這一點(diǎn)在其他數(shù)據(jù)庫(kù)是很難實(shí)現(xiàn)的。

使用有序集合你可以:

  • 在大型在線(xiàn)游戲中創(chuàng)建一個(gè)排行榜,每次有新的成績(jī)提交,使用ZADD命令加入到有序集合中。可以使用ZRANGE命令輕松獲得成績(jī)名列前茅的玩家,你也可以使用ZRANK根據(jù)一個(gè)用戶(hù)名獲得該用戶(hù)的分?jǐn)?shù)排名。把ZRANK 和 ZRANGE結(jié)合使用你可以獲得與某個(gè)指定用戶(hù)分?jǐn)?shù)接近的其他用戶(hù)。這些操作都很高效。

  • 有序集合經(jīng)常被用來(lái)索引存儲(chǔ)在Redis中的數(shù)據(jù)。比如,如果你有很多用戶(hù),用Hash來(lái)表示,可以使用有序集合來(lái)為這些用戶(hù)創(chuàng)建索引,使用年齡作為Score,使用用戶(hù)的ID作為Value,這樣的話(huà)使用ZRANGEBYSCORE 命令可以輕松和快速的獲得某一年齡段的用戶(hù)。

有序集合可能是Redis中最高級(jí)的數(shù)據(jù)類(lèi)型了,所以請(qǐng)花一些時(shí)間查看一下 有序集合命令列表 來(lái)獲得更多信息,同時(shí)你可能也想閱讀Redis數(shù)據(jù)類(lèi)型介紹

Bitmaps and HyperLogLogs類(lèi)型(位圖類(lèi)型和HyperLogLogs類(lèi)型)

Redis 也支持位圖類(lèi)型和HyperLogLogs 類(lèi)型,他們是在String基本類(lèi)型基礎(chǔ)上建立的類(lèi)型,但有自己的語(yǔ)義。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 參考來(lái)源 Redis的內(nèi)存優(yōu)化 Redis所有的數(shù)據(jù)都在內(nèi)存中,而內(nèi)存又是非常寶貴的資源。對(duì)于如何優(yōu)化內(nèi)存使用一直...
    秦漢郵俠閱讀 1,303評(píng)論 0 2
  • Redis的內(nèi)存優(yōu)化 聲明:本文內(nèi)容來(lái)自《Redis開(kāi)發(fā)與運(yùn)維》一書(shū)第八章,如轉(zhuǎn)載請(qǐng)聲明。 Redis所有的數(shù)據(jù)都...
    meng_philip123閱讀 18,929評(píng)論 2 29
  • Redis 是一個(gè)鍵值對(duì)數(shù)據(jù)庫(kù)(key-value DB),數(shù)據(jù)庫(kù)的值可以是字符串、集合、列表等多種類(lèi)型的對(duì)象,而...
    吳昂_ff2d閱讀 3,419評(píng)論 0 5
  • 聲明:本文內(nèi)容來(lái)自《Redis開(kāi)發(fā)與運(yùn)維》一書(shū)第八章,如轉(zhuǎn)載請(qǐng)聲明。Redis所有的數(shù)據(jù)都在內(nèi)存中,而內(nèi)存又是非常...
    yoqu閱讀 1,522評(píng)論 0 2
  • 前言 Redis是目前最火爆的內(nèi)存數(shù)據(jù)庫(kù)之一,通過(guò)在內(nèi)存中讀寫(xiě)數(shù)據(jù),大大提高了讀寫(xiě)速度,可以說(shuō)Redis是實(shí)現(xiàn)網(wǎng)站...
    Java架構(gòu)閱讀 1,293評(píng)論 1 16