共讀《redis設計與實現》-單機(一)

上一章我們講了 redis 基本類型的數據結構對象系統 ,這篇來說一下單機redis 的知識點。

一、資料庫

一個資料庫在redis中就有一個結構體,而資料庫的結構體是由redisServer這個結構體持有。
也就是redis伺服器對應一個redisService 結構體,一個redisServer結構體持有多個redisDB數組,並且存儲了數組的大小。

struct redisServer {
	...
    
    // 一個數組保存伺服器所有的資料庫
    redisDb *db;
    // 伺服器的資料庫的數量//初始默認為16
    int dbnum;
    // 過期字典// 保存鍵的過期時間
    dict *expires;
    
    // 記錄rdb 保存條件的數組
    struct saveparam *saveparam;
    
    // 修改計數器
    long long dirty;
    
    // 上一次執行保存的時間
    time_t lastsave;
    
    // aof 緩衝區
    sds aof_buf;
    
    // 一個鏈表保存了所有客戶端的狀態
    list *client;
    ...    
}

image.png

二、客戶端

然後我們再說一下客戶端,每個客戶端是也是有一個redisClient結構體

typedef struct redisClient {
	. . .

    // 客戶端的名稱,使用Client setname 命令設置
    rojb *name;
        
	// 記錄客戶端正在使用的資料庫
	redisDb *db;

    // 套接字描述符號,偽客戶端為 -1;客戶端為>-1的整數
    int fd;
    
    // 記錄了客戶端的角色,可以是單個值 也可以多個值
    int flags;
    
    // 客戶端輸入緩衝區,用來保存客戶端輸入的請求//不能超過1G,否則會關閉
    sds querbuf;
    
    // 客戶端發送服務端請求之後,
    //服務端解析請求,將命令參數保存在客戶端 argv 中,命令個數 保存在argc
    robj **argv;
    
    int argc;
	. . . 
}

image.png
客戶端的結構體中db 的指針指向 當前正在使用的資料庫的地址。
image.png
image.png
所以當我們 使用 SELECT 命令切換資料庫的時候就是將 redisClient 的db 指針切換了一個位置
image.png
注意點
image.png

三、鍵空間

我們之前看字典的時候已經講過,每個資料庫其實就是一個字典,我們平常存儲的數據在資料庫這個字典中,key是字典的key,value 是字典的value。(其實每個key 就是一個SDS結構,所以字典的key 是一個SDS 結構的存儲體,value 可能是SDS 可能是 字典、序列等其他基本結構體)
image.png

3.1 添加/更新/刪除

其實鍵的 添加/更新/刪除 就是在字典中 添加key-value 鍵值對 和 更新 刪除 鍵值對的動作。

3.2 鍵的生存時間/過期時間

我們可以給鍵 設置一個時間 ,當創建之後過多久就失效生存時間;當到達某個時間點就失效過期時間

3.2.1 SETEX/SEPIRE/PEXPIRE/EXPIREAT/PEXPIREAT

鍵盤的過期時間,我們可以從redisServer 的結構體中可以看出,其實就是對每個鍵``存儲了一個過期時間
image.png
Redis 有四個不同的命令可以用於設置鍵的生存時間(鍵可以存在名久)或過期時間
(鍵什麼時候會被刪除):

  • EXPIRE <key>sttl>命令用於將鍵key 的生存時間設置為tt1秒
  • PEXPIRE <key><tl>命令用於將鍵key 的生存時間設置為 tt1毫秒。
  • BXPIREAT Stimestamp>命令用於將鍵 key 的過期時間設置為timestamp所指定的秒數時間戳。
  • PEXPIREAT <key> <timestamp>命令用於將鍵key 的過期時間設置為 timestamp所指定的亳秒數時間戳。

雖然有多種不同單位和不同形式的設置命令,但實際上 EXPIRE、PEXPIRE、EXPIREAI
三個命令都是使用 PEXPIREAT 命令來實現的:無論客戶端執行的是以上四個命令中的哪-
個,經過轉換之後,最終的執行效果都和執行PEXPIREAT命令一樣
image.png
所以最後幹活的是PEXPIREAT ,其他的就是對於不同業務下的衍生api 而已。

關於TTL/PTTL 命令
image.png
image.png

3.2.2 過期鍵的刪除策略

  • 定時刪除:在設置鍵的過期時間的同時,創建一個定時器(timer),讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
  • 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
  • 定期刪除:每隔一段時間,程式就對資料庫進行一次檢查,刪除裡面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個資料庫,則由演算法決定。

在這三種策略中,第一種和第三種為主動刪除策略,而第二種則為被動刪除策略。

三種策略優缺點

定時刪除 能夠及時釋放 記憶體空間,但是如果遇到大量對鍵 過期,那麼會佔用很大對cpu 資源
惰性刪除 能夠解決cpu 資源對問題,但是 會浪費大量對存儲空間,有 記憶體泄漏 的風險
定期刪除 相當於平衡 前兩種的優缺點。

一般我們將惰性刪除定期刪除``配合使用
具體使用:
惰性:所有讀寫庫的redis 資料庫執行 命令之前 都會調用expireIfveeded 函數檢查過期時間,如果過期,那麼就刪除
image.png
定期:redis 周期性 函數Servercron執行 的時候 會調用activeExpireCycle函數,在 規定時間內 遍歷一次資料庫,隨機的訪問一些鍵 查看過期時間,過期刪除

3.3 RDB/AOF 持久化時 過期鍵處理

RDB

生成
對於RDB 快照類型的,如果是過期了,那麼下一次生成快照的時候就不會記錄在RDB文件中
載入
如果是主伺服器 載入RDB文件,那麼會對鍵的過期時間進行檢查,如果是過期了(在生成RDB文件和載入RDB文件之間的時間段內過期),那就不會被載入。
如果是從伺服器 載入RDB 文件,那麼不會檢查過期鍵,全部載入。(但是和主伺服器``同步的時候,從伺服器的資料庫都會被清空

AOF

生成:
只要還沒有被刪除,那麼不會對AOF產生影響,AOF會全部記錄,如果執行期間被刪除 會增加一條DELE命令
載入:
如果是過期了,那麼不會被載入

3.4 複製期間過期間處理

主伺服器 刪除的時候 會向 從伺服器 發送刪除命令
從伺服器 遇到過期鍵 不會處理,和平常的鍵一樣,即使客戶端查詢也會返回。
image.png
image.png
我們知道 redis 是一個記憶體資料庫,也就是所有的數據都在記憶體中,cpu 取數據的時候直接從記憶體中查找,不用在調用系統io 從磁碟載入數據了。這也就是為何redis 比其他 存儲磁碟的資料庫快的原因。
但是在記憶體中的數據有個極大的缺點:如果伺服器一旦關閉,那麼數據就不在了,因為記憶體的數據並沒有寫到磁碟上,所以redis 需要提供一個能夠寫入磁碟的機制。
redis 提供了兩種持久化機制:rdb持久化 aof持久化

鍵空間的維護操作

當redis 命令對於資料庫的讀寫時,伺服器不僅會對鍵空間進行客戶端的請求命令,還會執行一些額外的操作

  1. 命中率:讀取一個鍵之後,伺服器會依據進鍵是否存在來更新 鍵空間命中率 和 不命中率
  2. 閑置時間:就是鍵多久沒有訪問過了,等到命中這個鍵時會更新這個值。
  3. 過期鍵
  4. watch 命令:使用warch 命令監控一個鍵,那麼鍵修改時會將這個鍵置成 臟
  5. 臟數據:伺服器每修改一個鍵,會對 臟鍵 計數器的值+1;
  6. 資料庫通知功能:如果開啟這個功能,那麼修改redis 會通知資料庫

四、RDB 持久化

對於RDB 持久化,我們可以認為就是一個記憶體的快照,也就是將某一瞬間redis 的數據給存儲下來。
使用這個持久化有兩種命令:SAVE / BGSAVE

SAVE

這個命令持久化的時候,redis 處於阻塞狀態,也就是redis 不接受客戶端發過來的任何請求,全力的去處理這個請求。

BGSAVE

對於save 來說,我們要是因為持久化導致redis 不能使用,這個顯然會有問題。因為如果數據量特別多,那麼我們為了持久化,消耗的時間也就很多,業務阻塞了。
和save 不同,為了能夠使得持久化同時也能運行客戶端請求,redis 對於bgsave 分出一個執行緒去處理 持久化,這樣就不是阻塞的了。

save 和 bgsave 不能夠同時執行,考慮防止競爭問題、同時操作io的效率問題。

載入

因為RDB存儲的是redis 的快照,所以redis 沒有載入 rdb文件的命令,程式啟動的時候會自動載入rdb文件。

另外一點需要注意的是:因為aof文件比rdb 更新的更頻繁,也就是數據更新,那麼存在aof文件,就會載入aof文件而不會載入rdb文件了,可以使用配置將aof文件讀取關閉
image.png

自動保存條件

redis 對於bgsave 可以設置每隔多久進行一次rdb 保存的,可以通過啟動是save 參數進行設置。
我們可以從redisServer 的結構中看出,這個自動保存的條件其實是存儲起來的,也就是redisServer持有這個自動存儲條件,並在規定條件下進行一次調用BGSAVE命令
image.png

dirty 計數器 和lastsave 屬性

伺服器在每執行一次操作都會更新一次dirty計數器,比如dirty 計數器為123,那麼說明距離上次保存,伺服器執行了123次命令。

伺服器 之所以可以 自動保存,是因為 時間事件 不斷的去掃描 redisServer 然後看 saveparams 屬性 是否滿足自動保存。然後在調用bgsave

image.png
serverCrom 函數會 遍歷 saveparams ,看其中的條件是不是被滿足了

RDB文件結構

rdb文件以二進位形式存儲,我們可以通過 od 命令來解析 rdb文件

文件結構

image.png
說明
我們用全大寫表示 常量標識;使用全小寫標識 變數

  1. REDIS:常量標識符(5位元組)
  2. db_version:版本號(4位元組)
  3. 資料庫:也就是存儲的具體數據,具體長度由保存的數據來說明,如果沒有數據,那就沒有這個欄位
  4. EOF:結束標識位(1位元組),也就是如果讀到這個地方表示 rdb文件正文讀取完畢
  5. check_sum:校驗位置,就是前面的數字長度;

image.png
SELECTDB:常量(1位元組),標識為這裡是資料庫
db_number:資料庫號碼(1-5位元組不等)
key_value_pairs:資料庫中具體存儲的值
image.png

rdb文件中 資料庫中 數據的值(k-v結構)

image.png
EXPIRETIME_MS:常量,標識帶有過期鍵
過期鍵的時間
KEYTYPE:上圖寫的是REDIS_RDB_TYPE_SET,這個是 存儲的類型,方便讀取 value 的值
key: 存儲的key
value :存儲的value 可能是 SDS、HASH 之類的。

五、AOF 持久化

image.png
AOF 持久化功能的實現可以分為命令追加(append )文件寫人文件同步 (sync)``三個步驟。

命令追加

當 AOF 持久化功能處於打開狀態時,伺服器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到redisServer 結構體中 的 aof buf 緩衝區末尾如果在來一條命令,那麼在向緩衝區末尾添加。

Redis 的伺服器進程就是一個事件循環(1oop),這個循環中的文件事件負責接收客戶端命令請求,以及向客戶端發送命令回復,而時間事件則負責執行像servercron 函數這樣需要定時運行的函數。—這段看不懂就略過,其實是 下面要說 的事件

因為伺服器在處理文件事件時可能會執行寫命令,使得一些內容被追加到aof buf 緩衝區裡面,所以在伺服器每次結束一個事件循環之前,它都會調用 f1ushAppendonlyFile函數,考慮是否需要將 aof buf 緩衝區中的內容寫人和保存到 AOF 文件裡面。
image.png
image.png
image.png

載入/數據還原

image.png
創建一個不帶網路連接偽客戶端(fake client )是因為 Redis 的命令只能客戶端上下文執行,而載人 AOF 文件時所使用的命令直接來源於 AOF 文件而不是網路連接,所以伺服器使用了一個沒有網路連接偽客戶,和客戶端效果是一樣的。

AOF 重寫

如果我們redis 運行的事件長了,那麼就會使得aof 文件變得很大,而且這個文件中很多命令是浪費空間的,比如 push key v1;push key v2… 所以,redis 對aof 文件進行了重寫,讓這些命令合併為一條命令,減少aof 的空間

重寫原理:

aof 重寫 不需要 進行 讀取/寫入 原 aof 文件,也就是 完全 不操作原文件
他主要是看資料庫中 數據的狀態,使用命令將 資料庫中的數據 寫入文件

比如:
資料庫中有個 numbers:one,two,three
這樣的結構,之前是
push numbers one;
push numbers two;
push numbers three;
三個命令
我們直接讀取資料庫,我們不清楚過程,所以我們將之前的三個命令變成一個:
push numbers one two three
這樣就是壓縮了

之前重寫aof 文件的時候都是 不接受新的命令,為了不影響 使用,所以使用了後台重寫 命令。

後台重寫,為了保證數據的一致性,使用了aof 重寫緩衝區。
image.png

image.png
image.png
image.png

文件寫入和同步

image.png

六、事件

Redis 伺服器是一個事件驅動程式,伺服器需要處理以下兩類事件:

  • 文件事件(file event ):Redis 伺服器通過套接字(含義就是通過網路鏈接)與客戶端(或者其他 Redis 伺服器)進行連接,而文件事件就是伺服器套接字操作的抽象。伺服器與客戶端(或者其他伺服器)的通訊會產生相應的文件事件,而伺服器則通過監聽並處理這些事件來完成一系列網路通訊操作。
  • 時間事件(time event ):Redis 伺服器中的一些操作(比如servercron 兩數)需要在給定時間點執行,而時間事件就是伺服器對這類定時操作的抽象。

文本事件

Redis 基於 Reactor模式開發了自己的網路事件處理器:這個處理器被稱為文件事件處理器(file event handler):

  • 文件事件處理器使用 I/0 多路復用(multiplexing)【//www.cnblogs.com/zhangxiaoji/p/16152141.html】程式來同時監聽多個套接宇,並根據套接字目前執行的任務來為套接字關聯不同的事件處理器。

image.png

  • 當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫人(write入關閉(close)等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件雖然文件事件處理器以單執行緒方式運行,但通過使用 IO 多路復用程式來監聽多個套接宇,文件事件處理器既實現了高性能的網路通訊模型(可以理解為 一個服務端使用了一個執行緒(或者少量的執行緒)來處理多個客戶端請求),又可以很好地與 Redis 伺服器中其他同樣以單執行緒方式運行的模組進行對接,這保持了 Redis 內部單執行緒設計的簡單性

image.png
注意:io 多路復用和 文本事件分派器 中間的隊列是 單個的,也就是文件事件分派器一次只處理一個事件。
image.png
image.png

時間事件

我們從之前的講述中可以看到,和時間相關的都是由 redisCron 函數進行處理的,那麼它就是我們的 時間事件應用實例了。
Redis 的時間事件分為以下兩類:

  • 定時事件:讓一段程式在指定的時間之後執行一次。比如說,讓程式× 在當前時間
    的 30毫秒之後執行一次。
  • 周期性事件:讓一段程式每隔指定時間就執行一次。比如說,讓程式Y每隔 30毫秒就執行-
    一次。

個時間事件主要由以下三個屬性組成:

  1. id:伺服器為時間事件創建的全局唯一四D(標識號)。1D號按從小到大的順序遞增,
    新事件的1D 號比舊事件的1D 號要大。
  2. when:毫秒精度的 UNIX 時間戳,記錄了時間事件的到達(arrive)時間。
  3. timeProc:時間事件處理器,一個兩數。當時間事件到達時,伺服器就會調用相
    應的處理器來處理事件。

事件的調度與執行

因為redis 存在兩種事件類型,所以 redis 必須有個調度器去解決何時處理 文本事件 何時 處理 時間事件
這個是 aeProcessEvents函數來進行的
image.png

後面對 伺服器 和 客戶端 在進行詳細的研究。

參考資料#
《Redis設計與實現》-黃健宏
部分圖片來與百度搜索