分佈式鍵值系統redis思考一

“Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.” —— Redis是一個開放源代碼(BSD許可)的內存中數據結構存儲,用作數據庫,緩存和消息代理。(摘自官網)
好,逼裝完了,我們依然像之前分析mysql一樣先從底層說起,那麼redis的底層的數據結構是怎樣的呢。

未安裝redis的可以參照
https://blog.csdn.net/xk4848123/article/details/105315869

redis數據結構

Redis 有 5 種基礎數據結構,它們分別是:string(字符串)、list(列表)、hash(字典)、set(集合) 和 zset(有序集合)。這 5 種是 Redis 相關知識中最基礎、最重要的部分,下面我們結合源碼以及一些實踐來給大家分別講解一下。
在這裏插入圖片描述

數據結構操作演示

1.字符串string

redis 的 string 可以包含任何數據。如數字,字符串,jpg圖片或者序列化的對象。

127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> get counter
(nil)
127.0.0.1:6379> set counter 1
OK
127.0.0.1:6379> INCRBY counter
(error) ERR wrong number of arguments for 'incrby' command
127.0.0.1:6379> INCRBY counter 1
(integer) 2
127.0.0.1:6379> INCR counter 1
(error) ERR wrong number of arguments for 'incr' command
127.0.0.1:6379> INCR counter
(integer) 3

分佈式場景中可以用incr來作計數器,因爲redis網絡處理是單線程的。

2.列表list

127.0.0.1:6379> LPUSH list 1
(integer) 1
127.0.0.1:6379> RPUSH list 2
(integer) 2
127.0.0.1:6379> LPOP list
"1"
127.0.0.1:6379> LPOP list
"2"
127.0.0.1:6379> 
127.0.0.1:6379> LPUSH list a
(integer) 1
127.0.0.1:6379> LPUSH list b
(integer) 2
127.0.0.1:6379> LPUSH list last
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1
1) "last"
2) "b"
3) "a"
127.0.0.1:6379> 

1.LPUSH,RPUSH分別是從頭入隊、從尾入隊

2.LPOP,RPOP分別是從頭出隊、從尾出隊

3.LRANGE 範圍

3.哈希

127.0.0.1:6379> HSET books java "think in java"
(integer) 1
127.0.0.1:6379> HSET books python "python cookbook"
(integer) 1
127.0.0.1:6379> HGETALL books
1) "java"
2) "think in java"
3) "python"
4) "python cookbook"
127.0.0.1:6379> 
127.0.0.1:6379> HGET books java
"think in java"
127.0.0.1:6379> HSET books java "head first java"
(integer) 0
127.0.0.1:6379> HGETALL books
1) "java"
2) "head first java"
3) "python"
4) "python cookbook"
127.0.0.1:6379> HMSET books java "effetive  java" python "learning python"
OK

4.set集合(無序去重)

127.0.0.1:6379> SADD books1 python
(integer) 1
127.0.0.1:6379> SADD books1 java
(integer) 1
127.0.0.1:6379> SADD books1 java
(integer) 0
127.0.0.1:6379> SADD books1 golang
(integer) 1
127.0.0.1:6379> SMEMBERS books1
1) "golang"
2) "java"
3) "python"
127.0.0.1:6379> SCARD books1
(integer) 3
127.0.0.1:6379> SPOP books1
"golang"

5.有序列表 zset

127.0.0.1:6379> ZADD books2 1586098912 "task1"
(integer) 1
127.0.0.1:6379> ZADD books2 1586099912 "task2"
(integer) 1
127.0.0.1:6379> ZADD books2 1586109912 "task3"
(integer) 1
127.0.0.1:6379> ZRANGE books2 0 1
1) "task1"
2) "task2"
127.0.0.1:6379> ZADD books2 1586097912 "task4"
(integer) 1
127.0.0.1:6379> ZRANGE books2 0 1
1) "task4"
2) "task1"
127.0.0.1:6379> ZRANGE books2 0 -1
1) "task4"
2) "task1"
3) "task2"
4) "task3"
127.0.0.1:6379> ZRVRANGE books2 0 -1
(error) ERR unknown command `ZRVRANGE`, with args beginning with: `books2`, `0`, `-1`, 
127.0.0.1:6379> ZREVRANGE books2 0 -1
1) "task3"
2) "task2"
3) "task1"
4) "task4"
127.0.0.1:6379> ZCARD books2
(integer) 4

ZRANGEBYSCORE命令可以用來實現延時隊列,這時score記未來想要執行的某個時間戳。

更多數據結構

6. 布隆過濾器

在https://github.com/RedisBloom/RedisBloom下載最新的release源碼,在編譯服務器進行解壓編譯:

tar zxvf RedisBloom-2.2.2.tar.gz
cd RedisBloom-2.2.2
make
cp ./redisbloom.so /usr/local/redis/src/

關閉redis-server

127.0.0.1:6379> shutdown
not connected> exit
[root@localhost bin]# ps -ef|grep redis
root       8097   6020  0 08:17 pts/0    00:00:00 grep --color=auto redis

重啓redis,指定了默認的容量與容錯率

./redis-server /usr/local/redis/conf/redis.conf --loadmodule /usr/local/redis/src/rebloom.so INITIAL_SIZE 10000000 ERROR_RATE 0.0001
127.0.0.1:6379> BF.MADD test user2 user2
1) (integer) 1
2) (integer) 0
127.0.0.1:6379> BF.MADD test user2 user3
1) (integer) 0
2) (integer) 1
127.0.0.1:6379> BF.MADD test user4 user5
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> 
127.0.0.1:6379> BF.EXISTS test user1
(integer) 1
127.0.0.1:6379> BF.EXISTS test user6
(integer) 0
127.0.0.1:6379> BF.MEXISTS test user1 user6
1) (integer) 1
2) (integer) 0

1,BF.ADD {key} {item}
單條添加元素
向Bloom filter添加一個元素,如果該key不存在,則創建該key(過濾器)。
如果項是新插入的,則爲“1”;如果項以前可能存在,則爲“0”。

2,BF.MADD {key} {item} [item…]
批量添加元素
布爾數(整數)的數組。返回值爲0或1的範圍的數據,這取決於是否將相應的輸入元素新添加到過濾器中,或者是否已經存在。

3,BF.EXISTS {key} {item}
判斷單個元素是否存在
如果存在,返回1,否則返回0

4,BF.MEXISTS {key} {item} [item…]
判斷多個元素是否存在
布爾數(整數)的數組。返回值爲0或1的範圍的數據,這取決於是否將相應的元是否已經存在於key中。

7.HyperLogLog計算基數

127.0.0.1:6379> PFADD hyperloglog a
(integer) 1
127.0.0.1:6379> PFADD hyperloglog b
(integer) 1
127.0.0.1:6379> PFADD hyperloglog c
(integer) 1
127.0.0.1:6379> PFADD hyperloglog b
(integer) 0
127.0.0.1:6379> PFADD hyperloglog c
(integer) 0
127.0.0.1:6379> PFCOUNT hyperloglog
(integer) 3
127.0.0.1:6379> PFADD hyperloglog1 d
(integer) 1
127.0.0.1:6379> PFADD hyperloglog1 c
(integer) 1
127.0.0.1:6379> PFMERGE hyper hyperloglog hyperloglog1
OK
127.0.0.1:6379> PFCOUNT hyper
(integer) 4

PFADD (新增操) 丶PFCOUNT( 計算基數,就是幾個不重複的元素)丶PFMERGE(合併)等操作

redis數據結構底層知識

OBJECT ENCODING key可以查看底層數據結構

127.0.0.1:6379> LRANGE list 0 -1
1) "last"
2) "b"
3) "a"
127.0.0.1:6379> OBJECT ENCODING list
"quicklist"
127.0.0.1:6379> OBJECT ENCODING hello
"embstr"
127.0.0.1:6379> set hello 1
OK
127.0.0.1:6379> OBJECT ENCODING hello
"int"
127.0.0.1:6379> set hello nihao
OK
127.0.0.1:6379> OBJECT ENCODING hello
"embstr"

在這裏插入圖片描述
Redis使用前面說的五大數據類型來表示鍵和值,每次在Redis數據庫中創建一個鍵值對時,至少會創建兩個對象,一個是鍵對象,一個是值對象,而Redis中的每個對象都是由 redisObject 結構來表示:

typedef struct redisObject{
     //類型
     unsigned type:4;
     //編碼
     unsigned encoding:4;
     //指向底層數據結構的指針
     void *ptr;
     //引用計數
     int refcount;
     //記錄最後一次被程序訪問的時間
     unsigned lru:22;
 
}rob

j

1.SDS

Redis 是用 C 語言寫的,但是對於Redis的字符串,卻不是 C 語言中的字符串(即以空字符’\0’結尾的字符數組),它是自己構建了一種名爲 簡單動態字符串(simple dynamic string,SDS)的抽象類型,並將 SDS 作爲 Redis的默認字符串表示。
在這裏插入圖片描述
圖片來源:《Redis設計與實現》

上面對於 SDS 數據類型的定義:
1、len 保存了SDS保存字符串的長度
2、buf[] 數組用來保存字符串的每個元素
3、free j記錄了 buf 數組中未使用的字節數量

好處:
1、常數複雜度獲取字符串長度
2、杜絕緩衝區溢出( len 屬性檢查內存空間是否滿足需求,如果不滿足,會進行相應的空間擴展)
3、減少修改字符串的內存重新分配次數(空間預分配和惰性空間釋放)
4、二進制安全(len 屬性表示的長度來判斷字符串是否結束而不依賴空字符,因爲二進制文件可能包括空字符)

SDS 除了保存數據庫中的字符串值以外,SDS 還可以作爲緩衝區(buffer):包括 AOF 模塊中的AOF緩衝區以及客戶端狀態中的輸入緩衝區。

2.鏈表

在這裏插入圖片描述
Redis鏈表特性:
1、雙端(有head、tail指針,獲取這兩個節點時間複雜度都爲O(1))
2、無環
3、帶鏈表長度計數器( len 屬性獲取鏈表長度的時間複雜度爲 O(1))
4、多態(鏈表節點使用void*指針來保存節點值,可以保存各種不同類型的值)

3.字典

字典又稱爲符號表或者關聯數組、或映射(map),是一種用於保存鍵值對的抽象數據結構。字典中的每一個鍵 key 都是唯一的,通過 key 可以對值來進行查找或修改。
Redis 中的字典相當於 Java 中的 HashMap,內部實現也差不多類似,都是通過 “數組 + 鏈表” 的鏈地址法來解決部分 哈希衝突
源碼定義如 dict.h/dictht 定義:

typedef struct dictht {
    // 哈希表數組
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩碼,用於計算索引值,總是等於 size - 1
    unsigned long sizemask;
    // 該哈希表已有節點的數量
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    // 內部有兩個 dictht 結構
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

table屬性是一個數組,數組中的每個元素都是一個指向dict.h/dictEntry 結構的指針,而每個dictEntry結構保存着一個鍵值對

typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下個哈希表節點,形成鏈表
    struct dictEntry *next;
} dictEntry;

實際上字典結構的內部包含兩個hashtable,通常情況下只有一個 hashtable是有值的,但是在字典擴容縮容時,需要分配新的 hashtable,然後進行漸進式搬遷。
1丶漸進式rehash
什麼叫漸進式 rehash?也就是說擴容和收縮操作不是一次性、集中式完成的,而是分多次、漸進式完成的。如果保存在Redis中的鍵值對只有幾個幾十個,那麼 rehash 操作可以瞬間完成,但是如果鍵值對有幾百萬,幾千萬甚至幾億,那麼要一次性的進行 rehash,勢必會造成Redis一段時間內不能進行別的操作。所以Redis採用漸進式 rehash,這樣在進行漸進式rehash期間,字典的刪除查找更新等操作可能會在兩個哈希表上進行,第一個哈希表沒有找到,就會去第二個哈希表上進行查找。但是進行 增加操作,一定是在新的哈希表上進行的。

2、觸發擴容的條件
1、服務器目前沒有執行 BGSAVE 命令或者 BGREWRITEAOF 命令,並且負載因子大於等於1。
2、服務器目前正在執行 BGSAVE 命令或者 BGREWRITEAOF 命令,並且負載因子大於等於5。

大家思考一個文件,redis是單線程的,那麼它是在什麼時候漸進式rehash的呢。

4.跳躍表

跳躍表(skiplist)是一種有序數據結構,它通過在每個節點中維持多個指向其它節點的指針,從而達到快速訪問節點的目的(O(n/2))。
在這裏插入圖片描述
1、有很多層
2、上一層有的元素,下一層必有元素
3、最底層有所有的元素

更多關於跳錶的知識可以參考:
https://blog.csdn.net/sunxianghuang/article/details/52221913

5.整數集合

整數集合(intset)是Redis用於保存整數值的集合抽象數據類型,它可以保存類型爲int16_t、int32_t 或者int64_t 的整數值,並且保證集合中不會出現重複元素。

typedef struct intset{
     //編碼方式
     uint32_t encoding;
     //集合包含的元素數量
     uint32_t length;
     //保存元素的數組
     int8_t contents[];
 
}intset;

需要注意的是雖然 contents 數組聲明爲 int8_t 類型,但是實際上contents 數組並不保存任何 int8_t 類型的值,其真正類型有 encoding 來決定。
1、升級(當我們新增的元素類型比原集合元素類型的長度要大時,需要對整數集合進行升級,才能將新元素放入整數集合中)
2、降級(升級後不可降級)

6.壓縮列表

壓縮列表(ziplist)是Redis爲了節省內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構,一個壓縮列表可以包含任意多個節點(entry),每個節點可以保存一個字節數組或者一個整數值。
  壓縮列表的原理:壓縮列表並不是對數據利用某種算法進行壓縮,而是將數據按照一定規則編碼在一塊連續的內存區域,目的是節省內存。
  在這裏插入圖片描述
壓縮列表的每個節點構成如下:
在這裏插入圖片描述
1、previous_entry_length
節點的previous_entry_length屬性以字節爲單位,記錄了壓縮列表中前一個節點的長度。previous_entry_length屬性的長度可以是1字節或者5字節:
如果前一節點的長度小於254字節,那麼previous_entry_length屬性的長度爲1字節:前一節點的長度就保存在這一個字節裏面
如果前一個節點的長度大於254字節,那麼previous_entry_length屬性的長度爲5字節:其中屬性的第一個字節會被設置爲oxFE,而之後的四個字節則用於保存前一個節點的長度。
因爲節點的previous_entry_length屬性記錄了前一個節點的長度,所以程序可以通過指針運算,根據當前節點的起始地址計算出前一個節點的起始地址。
壓縮列表從表尾向表頭遍歷操作就是使用這一原理實現的,只要我們擁有一個指向某個節點起始地址的指針,那麼通過這個指針以及這個節點的previous_entry_length屬性,程序就可以一直向前一個節點回溯,最終到達壓縮列表的表頭節點。
2、encoding
節點的encoding屬性記錄了節點的content屬性所保存數據的類型以及長度
3、content
節點的content屬性負責保存節點的值,節點值可以是一個字節數組或者整數,值的類型和長度由節點的encoding屬性決定。

注意壓縮列表可能會出連鎖更新問題,大家可以去思考一下爲什麼,着重看下previous_entry_length的機制。

總結

以上介紹的簡單字符串、鏈表、字典、跳躍表、整數集合、壓縮列表等數據結構就是Redis底層的一些數據結構。
小夥伴們不管是後面講到的線程模型也好,分佈式鎖或是lua腳本,還是高可用架構,沒有這些底層數據作支撐都是扯犢子,可謂是redis的基石~謝謝大家,動手點個讚唄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章