Redis 對象處理機(jī)制

2018-08-02 11:48 更新

對象處理機(jī)制

在 Redis 的命令中,用于對鍵(key)進(jìn)行處理的命令占了很大一部分,而對于鍵所保存的值的類型(后簡稱“鍵的類型”),鍵能執(zhí)行的命令又各不相同。

比如說,LPUSHLLEN 只能用于列表鍵,而 SADDSRANDMEMBER 只能用于集合鍵,等等。

另外一些命令,比如 DEL 、 TTLTYPE ,可以用于任何類型的鍵,但是,要正確實(shí)現(xiàn)這些命令,必須為不同類型的鍵設(shè)置不同的處理方式:比如說,刪除一個(gè)列表鍵和刪除一個(gè)字符串鍵的操作過程就不太一樣。

以上的描述說明,Redis 必須讓每個(gè)鍵都帶有類型信息,使得程序可以檢查鍵的類型,并為它選擇合適的處理方式。

另外,在前面介紹各個(gè)底層數(shù)據(jù)結(jié)構(gòu)時(shí)有提到,Redis 的每一種數(shù)據(jù)類型,比如字符串、列表、有序集,它們都擁有不只一種底層實(shí)現(xiàn)(Redis 內(nèi)部稱之為編碼,encoding),這說明,每當(dāng)對某種數(shù)據(jù)類型的鍵進(jìn)行操作時(shí),程序都必須根據(jù)鍵所采取的編碼,進(jìn)行不同的操作。

比如說,集合類型就可以由字典和整數(shù)集合兩種不同的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),但是,當(dāng)用戶執(zhí)行 ZADD 命令時(shí),他/她應(yīng)該不必關(guān)心集合使用的是什么編碼,只要 Redis 能按照 ZADD 命令的指示,將新元素添加到集合就可以了。

這說明,操作數(shù)據(jù)類型的命令除了要對鍵的類型進(jìn)行檢查之外,還需要根據(jù)數(shù)據(jù)類型的不同編碼進(jìn)行多態(tài)處理。

為了解決以上問題,Redis 構(gòu)建了自己的類型系統(tǒng),這個(gè)系統(tǒng)的主要功能包括:

  • redisObject 對象。
  • 基于 redisObject 對象的類型檢查。
  • 基于 redisObject 對象的顯式多態(tài)函數(shù)。
  • redisObject 進(jìn)行分配、共享和銷毀的機(jī)制。

以下小節(jié)將分別介紹類型系統(tǒng)的這幾個(gè)方面。

Note

因?yàn)?C 并不是面向?qū)ο笳Z言,這里將 redisObject 稱呼為對象一是為了講述的方便,二是希望通過模仿 OOP 的常用術(shù)語,讓這里的內(nèi)容更容易被理解,redisObject 實(shí)際上是只是一個(gè)結(jié)構(gòu)類型。

redisObject 數(shù)據(jù)結(jié)構(gòu),以及 Redis 的數(shù)據(jù)類型

redisObject 是 Redis 類型系統(tǒng)的核心,數(shù)據(jù)庫中的每個(gè)鍵、值,以及 Redis 本身處理的參數(shù),都表示為這種數(shù)據(jù)類型。

redisObject 的定義位于 redis.h

/*
 * Redis 對象
 */
typedef struct redisObject {

    // 類型
    unsigned type:4;

    // 對齊位
    unsigned notused:2;

    // 編碼方式
    unsigned encoding:4;

    // LRU 時(shí)間(相對于 server.lruclock)
    unsigned lru:22;

    // 引用計(jì)數(shù)
    int refcount;

    // 指向?qū)ο蟮闹?    void *ptr;

} robj;

type 、 encodingptr 是最重要的三個(gè)屬性。

type 記錄了對象所保存的值的類型,它的值可能是以下常量的其中一個(gè)(定義位于 redis.h):

/*
 * 對象類型
 */
#define REDIS_STRING 0  // 字符串
#define REDIS_LIST 1    // 列表
#define REDIS_SET 2     // 集合
#define REDIS_ZSET 3    // 有序集
#define REDIS_HASH 4    // 哈希表

encoding 記錄了對象所保存的值的編碼,它的值可能是以下常量的其中一個(gè)(定義位于 redis.h):

/*
 * 對象編碼
 */
#define REDIS_ENCODING_RAW 0            // 編碼為字符串
#define REDIS_ENCODING_INT 1            // 編碼為整數(shù)
#define REDIS_ENCODING_HT 2             // 編碼為哈希表
#define REDIS_ENCODING_ZIPMAP 3         // 編碼為 zipmap
#define REDIS_ENCODING_LINKEDLIST 4     // 編碼為雙端鏈表
#define REDIS_ENCODING_ZIPLIST 5        // 編碼為壓縮列表
#define REDIS_ENCODING_INTSET 6         // 編碼為整數(shù)集合
#define REDIS_ENCODING_SKIPLIST 7       // 編碼為跳躍表

ptr 是一個(gè)指針,指向?qū)嶋H保存值的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)由 type 屬性和 encoding 屬性決定。

舉個(gè)例子,如果一個(gè) redisObjecttype 屬性為 REDIS_LISTencoding 屬性為 REDIS_ENCODING_LINKEDLIST ,那么這個(gè)對象就是一個(gè) Redis 列表,它的值保存在一個(gè)雙端鏈表內(nèi),而 ptr 指針就指向這個(gè)雙端鏈表;

另一方面,如果一個(gè) redisObjecttype 屬性為 REDIS_HASH , encoding 屬性為 REDIS_ENCODING_ZIPMAP ,那么這個(gè)對象就是一個(gè) Redis 哈希表,它的值保存在一個(gè) zipmap 里,而 ptr 指針就指向這個(gè) zipmap ;諸如此類。

下圖展示了 redisObject 、Redis 所有數(shù)據(jù)類型、以及 Redis 所有編碼方式(底層實(shí)現(xiàn))三者之間的關(guān)系:

digraph datatype {    rankdir=LR;    node[shape=plaintext, style = filled];    edge [style = bold];    // obj    redisObject [label= REDIS_STRING; redisObject -> REDIS_LIST; redisObject -> REDIS_SET; redisObject -> REDIS_ZSET; redisObject -> REDIS_HASH; REDIS_STRING -> REDIS_ENCODING_RAW; REDIS_STRING -> REDIS_ENCODING_INT; REDIS_LIST -> REDIS_ENCODING_LINKEDLIST; REDIS_LIST -> REDIS_ENCODING_ZIPLIST; REDIS_SET -> REDIS_ENCODING_HT; REDIS_SET -> REDIS_ENCODING_INTSET; REDIS_ZSET -> REDIS_ENCODING_SKIPLIST; REDIS_ZSET -> REDIS_ENCODING_ZIPLIST; REDIS_HASH -> REDIS_ENCODING_HT; REDIS_HASH -> REDIS_ENCODING_ZIPLIST;}" />

這個(gè)圖展示了 Redis 各種數(shù)據(jù)類型,以及它們的編碼方式。

Note

REDIS_ENCODING_ZIPMAP 沒有出現(xiàn)在圖中,因?yàn)閺?Redis 2.6 開始,它不再是任何數(shù)據(jù)類型的底層結(jié)構(gòu)。

命令的類型檢查和多態(tài)

有了 redisObject 結(jié)構(gòu)的存在,在執(zhí)行處理數(shù)據(jù)類型的命令時(shí),進(jìn)行類型檢查和對編碼進(jìn)行多態(tài)操作就簡單得多了。

當(dāng)執(zhí)行一個(gè)處理數(shù)據(jù)類型的命令時(shí),Redis 執(zhí)行以下步驟:

  1. 根據(jù)給定 key ,在數(shù)據(jù)庫字典中查找和它相對應(yīng)的 redisObject ,如果沒找到,就返回 NULL
  2. 檢查 redisObjecttype 屬性和執(zhí)行命令所需的類型是否相符,如果不相符,返回類型錯(cuò)誤。
  3. 根據(jù) redisObjectencoding 屬性所指定的編碼,選擇合適的操作函數(shù)來處理底層的數(shù)據(jù)結(jié)構(gòu)。
  4. 返回?cái)?shù)據(jù)結(jié)構(gòu)的操作結(jié)果作為命令的返回值。

作為例子,以下展示了對鍵 key 執(zhí)行 LPOP 命令的完整過程:

digraph command_poly {    node [shape=plaintext, style = filled];    edge [style = bold];    lpop [label= get_key_obj_from_db; get_key_obj_from_db -> is_obj_nil_or_not; is_obj_nil_or_not -> return_nil [label="是"]; is_obj_nil_or_not -> is_type_list_or_not [label="否"]; is_type_list_or_not -> call_poly_pop_function [label="是"]; is_type_list_or_not -> return_type_error [label="否"]; call_poly_pop_function -> pop_from_ziplist [label="對象的編碼為\nZIPLIST"]; call_poly_pop_function -> pop_from_linkedlist [label="對象的編碼為\nLINKEDLIST"]; pop_from_ziplist -> return_pop_item; pop_from_linkedlist -> return_pop_item;}" />

對象共享

有一些對象在 Redis 中非常常見,比如命令的返回值 OK 、 ERROR 、 WRONGTYPE 等字符,另外,一些小范圍的整數(shù),比如個(gè)位、十位、百位的整數(shù)都非常常見。

為了利用這種常見情況,Redis 在內(nèi)部使用了一個(gè) Flyweight 模式 :通過預(yù)分配一些常見的值對象,并在多個(gè)數(shù)據(jù)結(jié)構(gòu)之間共享這些對象,程序避免了重復(fù)分配的麻煩,也節(jié)約了一些 CPU 時(shí)間。

Redis 預(yù)分配的值對象有如下這些:

  • 各種命令的返回值,比如執(zhí)行成功時(shí)返回的 OK ,執(zhí)行錯(cuò)誤時(shí)返回的 ERROR ,類型錯(cuò)誤時(shí)返回的 WRONGTYPE ,命令入隊(duì)事務(wù)時(shí)返回的 QUEUED ,等等。
  • 包括 0 在內(nèi),小于 redis.h/REDIS_SHARED_INTEGERS 的所有整數(shù)(REDIS_SHARED_INTEGERS 的默認(rèn)值為 10000

因?yàn)槊畹幕貜?fù)值直接返回給客戶端,所以它們的值無須進(jìn)行共享;另一方面,如果某個(gè)命令的輸入值是一個(gè)小于 REDIS_SHARED_INTEGERS 的整數(shù)對象,那么當(dāng)這個(gè)對象要被保存進(jìn)數(shù)據(jù)庫時(shí),Redis 就會(huì)釋放原來的值,并將值的指針指向共享對象。

作為例子,下圖展示了三個(gè)列表,它們都帶有指向共享對象數(shù)組中某個(gè)值對象的指針:

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)