當你對 redis 說你中意的女孩是 Mia

語言: CN / TW / HK

作者:京東科技 周新智

一、Redis

眾所周知,Redis = Remote Dictionary Server,即遠端字典服務。

是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫,並提供多種語言的API。

二、當你對 redis 說你中意的女孩是 Mia 時

1、set myLove Mia

redis 會將 key:myLove value:Mia

包裝成一個 dictEntry 物件、一個 redisObject 物件,如下圖所示:

dictEntry:眾所周知,Redis是Key-Value資料庫,因此對每個鍵值對都會有一個dictEntry,裡面儲存了指向Key和Value的指標;next指向下一個dictEntry,與本Key-Value無關。

Key:圖中右上角可見,Key("myLove")並不是直接以字串儲存,而是儲存在SDS結構中。

redisObject:Value("Mia")既不是直接以字串儲存,也不是像Key一樣直接儲存在SDS中,而是儲存在redisObject中。實際上,不論Value是5種類型的哪一種,都是通過redisObject來儲存的;而redisObject中的type欄位指明瞭Value物件的型別,ptr欄位則指向物件所在的地址。不過可以看出,字串物件雖然經過了redisObject的包裝,但仍然需要通過SDS儲存。

1.1、對 myLove 進行物件封裝

1.1.1、dictEntry

redis內部整體的儲存結構是一個大的hashmap,內部是陣列實現的hash,key衝突通過掛連結串列去實現,每個dictEntry為一個key/value物件,value為定義的redisObject。

結構圖如下:

dictEntry是儲存key->value的地方,再讓我們看一下dictEntry結構體

/*
 * 字典
 */
typedef struct dictEntry {
    // 鍵
    void *key;
    // 值
    union {
        // 指向具體redisObject
        void *val;
        // 
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下個雜湊表節點,形成連結串列
    struct dictEntry *next;
} dictEntry;

1.1.2、物件封裝 redisObject

我們接著再往下看redisObject究竟是什麼結構的

/*
 * Redis 物件
 */
typedef struct redisObject {
    // 型別 4bits
    unsigned type:4;
    // 編碼方式 4bits
    unsigned encoding:4;
    // LRU 時間(相對於 server.lruclock) 24bits
    unsigned lru:22;
    // 引用計數 Redis裡面的資料可以通過引用計數進行共享 32bits
    int refcount;
    // 指向物件的值 64-bit
    void *ptr;
} robj;

*ptr指向具體的資料結構的地址;type表示該物件的型別,即String,List,Hash,Set,Zset中的一個,但為了提高儲存效率與程式執行效率,每種物件的底層資料結構實現都可能不止一種,encoding 表示物件底層所使用的編碼。

redis物件底層的八種資料結構:

 REDIS_ENCODING_INT(long 型別的整數)
 REDIS_ENCODING_EMBSTR embstr (編碼的簡單動態字串)
 REDIS_ENCODING_RAW (簡單動態字串)
 REDIS_ENCODING_HT (字典)
 REDIS_ENCODING_LINKEDLIST (雙端連結串列)
 REDIS_ENCODING_ZIPLIST (壓縮列表)
 REDIS_ENCODING_INTSET (整數集合)
 REDIS_ENCODING_SKIPLIST (跳躍表和字典)

檢視 redisObject 詳細資訊 :

# 檢視 key對應value的 redisObject 型別
type key
    type myLove
    
# 檢視 key對應value的redisObject 詳細資訊
debug object key
     debug object myLove

value 為 string 、int 型別是 redisObject 中的 type、encoding 不同表現形式

Value 為 string 型別時:

Value 為 int型別時:

以上兩種不同 value 型別,type 相同,encoding 不同

1.2、對 myLove 進行持久化

1.2.1、rdb 檔案寫入

1.2.2、aof 快取寫入 檔案儲存

預設情況下 沒有開啟 AOF ( append only file)

開啟 AOF 持久化後,每執行一條會更改 redis 資料的命令,redis就會將寫入、修改、刪除命令寫入到硬碟中的 AOF 檔案(當然並不是立即寫入檔案,而是立即寫入aof快取中,再根據aof配置的資料持久化條件進行寫入),這一過程顯然會降低 redis 的效能,但大部分情況下這個影響是能夠接受的,

另外使用快的硬碟可以提高 AOF 的效能。

配置 redis.conf

# 可以通過修改redis.conf配置檔案中的appendonly引數開啟 
    appendonly yes
# AOF檔案的儲存位置和RDB檔案的位置相同,都是通過dir引數設定的。 dir ./
# 預設的檔名是appendonly.aof,可以通過appendfilename引數修改 appendfilename appendonly.aof

AOF檔案中儲存的是redis的命令 原理

Redis 將所有對資料庫進行過寫入的命令(及其引數)記錄到 AOF 檔案, 以此達到記錄資料庫狀態的 目的, 為了方便起見, 我們稱呼這種記錄過程為同步。

同步命令到 AOF 檔案的整個過程可以分為三個階段:

命令傳播:Redis 將執行完的命令、命令的引數、命令的引數個數等資訊傳送到 AOF 程式中。 快取追 加:AOF 程式根據接收到的命令資料,將命令轉換為網路通訊協議 RESP 的格式,然後將協議內容追加到伺服器的 AOF 快取中。 檔案寫入和儲存: AOF 快取中的內容被寫入到 AOF 檔案末尾,如果設定的 AOF 儲存條件被滿足的話, fsync 函式或者 fdatasync 函式會被呼叫,將寫入的內容真正地儲存到磁碟中。

命令傳播:

當一個 Redis 客戶端需要執行命令時, 它通過網路連線, 將協議文字傳送給 Redis 伺服器。伺服器在 接到客戶端的請求之後, 它會根據協議文字的內容, 選擇適當的命令函式, 並將各個引數從字串文 本轉換為 Redis 字串物件( StringObject )。每當命令函式成功執行之後, 命令引數都會被傳播到 AOF 程式。

快取追加:

當命令被傳播到 AOF 程式之後, 程式會根據命令以及命令的引數, 將命令從字串物件轉換回原來的 協議文字。協議文字生成之後, 它會被追加到 redis.h/redisServer 結構的 aof_buf 末尾。

redisServer 結構維持著 Redis 伺服器的狀態, aof_buf 域則儲存著所有等待寫入到 AOF 檔案的協 議文字。

RESP 協議:

Redis客戶端使用RESP(Redis的序列化協議)協議與Redis的伺服器端進行通訊。 雖然該協議是專門為 Redis設計的,但是該協議也可以用於其他 客戶端-伺服器 (Client-Server)軟體專案。

可以通過特殊符號來區分出資料的型別:

單行回覆:以+號開頭。

錯誤回覆:以-號開頭。

整數回覆:以:號開頭。

批量回復:以$號開頭。

多條批量回復:以*號開頭。

1、間隔符號,在Linux下是\r\n,在Windows下是\n

2、簡單字串 Simple Strings, 以 "+"加號 開頭

3、錯誤 Errors, 以"-"減號 開頭

4、整數型 Integer, 以 ":" 冒號開頭

5、大字串型別 Bulk Strings, 以 "$"美元符號開頭,長度限制512M 6、陣列型別 Arrays,以 "*"星號開頭 用SET命令來舉例說明RESP協議的格式。

實際傳送的請求資料:

redis> SET myLove "Mia"
"OK"
*3\r\n$3\r\nSET\r\n$6\r\nmyLove\r\n$3\r\nMia\r\n
*3
$3
SET
$5
mykey
$5
Hello

實際收到的響應資料:

+OK\r\n

檔案寫入和儲存:

每當伺服器常規任務函式被執行、 或者事件處理器被執行時, aof.c/flushAppendOnlyFile 函式都會被 呼叫, 這個函式執行以下兩個工作:

WRITE:根據條件,將 aof_buf 中的快取寫入到 AOF 檔案。 SAVE:根據條件,呼叫 fsync 或 fdatasync 函式,將 AOF 檔案儲存到磁碟中。

2、給你的愛一個期限 expire myLove 999999999

從圖可知,在redis的資料庫中,redisDb結構中的expires字典中儲存了資料庫中所有鍵的過期時間,所以叫過期字典。

過期字典的key是一個指標,指向鍵空間的某個鍵物件(就是資料庫鍵)

過期字典的value是一個long型別的整數,這個整數儲存了鍵所指向的資料庫鍵的過期時間,一個毫秒精度的UNIX時間戳

過期鍵判定

通過過期字典,我們可以得到一個key是否過期:

判斷key是否存在於過期字典中

通過過期字典拿到key的過期時間,判斷當前UNIX時間戳是否大於key時間

過期key如何刪除

惰性刪除策略

過期鍵的惰性刪除策略由db.c/expireIfNeeded函式實現,所有讀寫資料庫的Redis命令在執行之前都會呼叫expireIfNeeded函式對輸入鍵進行檢查:

如果輸入鍵已經過期,那麼expireIfNeeded函式將輸入鍵從資料庫中刪除。

如果輸入鍵未過期,那麼expireIfNeeded函式不做動作。

expireIfNeeded函式就像一個過濾器,它可以在命令真正執行之前,過濾掉過期的輸入鍵,從而避免命令接觸到過期鍵。

另外,因為每個被訪問的鍵都可能因為過期而被expireIfNeeded函式刪除,所以每個命令的實現函式都必須能同時處理鍵存在以及鍵不存在這兩種情況:

當鍵存在時,命令按照鍵存在的情況執行。

當鍵不存在或者鍵因為過期而被expireIfNeeded函式刪除時,命令按照鍵不存在的情況執行。

定期刪除策略的實現

過期鍵的定期刪除策略由redis.c/activeExpireCycle函式實現,每當Redis的伺服器週期性操作redis.c/serverCron函式執行時,activeExpireCycle函式就會被呼叫,它在規定的時間內,分多次遍歷伺服器中的各個資料庫,從資料庫的expires字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。

3、del myLove

不好意思,哥們的愛無法刪除!