共讀《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; ... }
二、客戶端
然後我們再說一下客戶端,每個客戶端是也是有一個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; . . . }
客戶端的結構體中db 的指標指向 當前正在使用的資料庫的地址。
所以當我們 使用 SELECT 命令切換資料庫的時候就是將 redisClient 的db 指標切換了一個位置
注意點
三、鍵空間
我們之前看字典的時候已經講過,每個資料庫其實就是一個字典
,我們平常儲存的資料在資料庫這個字典中,key是字典的key,value 是字典的value。(其實每個key 就是一個SDS結構,所以字典的key 是一個SDS 結構的儲存體,value 可能是SDS 可能是 字典、序列等其他基本結構體
)
3.1 新增/更新/刪除
其實鍵的 新增/更新/刪除 就是在字典中 新增key-value 鍵值對 和 更新 刪除 鍵值對的動作。
3.2 鍵的生存時間/過期時間
我們可以給鍵 設定一個時間 ,當建立之後過多久就失效
為 生存時間
;當到達某個時間點就失效
是 過期時間
3.2.1 SETEX/SEPIRE/PEXPIRE/EXPIREAT/PEXPIREAT
鍵盤的過期時間,我們可以從redisServer 的結構體中可以看出,其實就是對每個鍵``儲存
了一個過期時間
。
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
命令一樣
所以最後幹活的是PEXPIREAT ,其他的就是對於不同業務下的衍生api 而已。
關於TTL/PTTL 命令
3.2.2 過期鍵的刪除策略
- 定時刪除:在設定鍵的過期時間的同時,建立一個定時器(timer),讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
- 惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
- 定期刪除:每隔一段時間,程式就對資料庫進行一次檢查,刪除裡面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫,則由演算法決定。
在這三種策略中,第一種和第三種為主動刪除策略,而第二種則為被動刪除策略。
三種策略優缺點
定時刪除 能夠及時釋放 記憶體空間,但是如果遇到大量對鍵 過期,那麼會佔用很大對cpu 資源
惰性刪除 能夠解決cpu 資源對問題,但是 會浪費大量對儲存空間,有 記憶體洩漏 的風險
定期刪除 相當於平衡 前兩種的優缺點。
一般我們將惰性刪除
和定期刪除``配合
使用
具體使用:
惰性:所有讀寫庫的redis 資料庫執行 命令之前 都會呼叫expireIfveeded 函式檢查過期時間,如果過期,那麼就刪除
定期:redis 週期性
函式Servercron
執行 的時候 會呼叫activeExpireCycle
函式,在 規定時間
內 遍歷一次資料庫,隨機
的訪問一些鍵 檢視過期時間,過期刪除
。
3.3 RDB/AOF 持久化時 過期鍵處理
RDB
生成:
對於RDB 快照型別的,如果是過期了,那麼下一次生成快照的時候就不會記錄在RDB檔案中
載入:
如果是主伺服器
載入RDB
檔案,那麼會對鍵的過期時間進行檢查
,如果是過期了(在生成RDB檔案和載入RDB檔案之間的時間段內過期),那就不會被載入。
如果是從伺服器
載入RDB
檔案,那麼不會
檢查過期鍵,全部載入
。(但是和主伺服器``同步
的時候,從伺服器的資料庫都會被清空
)
AOF
生成:
只要還沒有被刪除,那麼不會對AOF產生影響,AOF會全部記錄,如果執行期間被刪除 會增加一條DELE命令
載入:
如果是過期了,那麼不會被載入
3.4 複製期間過期間處理
主伺服器 刪除的時候 會向 從伺服器 傳送刪除命令
從伺服器 遇到過期鍵 不會處理,和平常的鍵一樣,即使客戶端查詢也會返回。
我們知道 redis 是一個記憶體資料庫,也就是所有的資料都在記憶體中,cpu 取資料的時候直接從記憶體中查詢,不用在呼叫系統io 從磁碟載入資料了。這也就是為何redis 比其他 儲存磁碟的資料庫快的原因。
但是在記憶體中的資料有個極大的缺點:如果伺服器一旦關閉,那麼資料就不在了,因為記憶體的資料並沒有寫到磁碟上,所以redis 需要提供一個能夠寫入磁碟的機制。
redis 提供了兩種持久化機制:rdb持久化 aof持久化
鍵空間的維護操作
當redis 命令對於資料庫的讀寫時,伺服器不僅會對鍵空間進行客戶端的請求命令,還會執行一些額外的操作
- 命中率:讀取一個鍵之後,伺服器會依據進鍵是否存在來更新 鍵空間命中率 和 不命中率
- 閒置時間:就是鍵多久沒有訪問過了,等到命中這個鍵時會更新這個值。
- 過期鍵
- watch 命令:使用warch 命令監控一個鍵,那麼鍵修改時會將這個鍵置成 髒
- 髒資料:伺服器每修改一個鍵,會對 髒鍵 計數器的值+1;
- 資料庫通知功能:如果開啟這個功能,那麼修改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檔案讀取關閉
自動儲存條件
redis 對於bgsave 可以設定每隔多久進行一次rdb 儲存的,可以通過啟動是save 引數進行設定。
我們可以從redisServer 的結構中看出,這個自動儲存的條件其實是儲存起來的,也就是redisServer持有這個自動儲存條件,並在規定條件下進行一次呼叫BGSAVE命令
dirty 計數器 和lastsave 屬性
伺服器在每執行一次操作都會更新一次dirty計數器,比如dirty 計數器為123,那麼說明距離上次儲存,伺服器執行了123次命令。
伺服器 之所以可以 自動儲存
,是因為 時間事件
不斷的去掃描 redisServer
然後看 saveparams 屬性
是否滿足自動儲存。然後在呼叫bgsave
serverCrom 函式會 遍歷 saveparams ,看其中的條件是不是被滿足了
RDB檔案結構
rdb檔案以二進位制形式儲存,我們可以通過 od 命令來解析 rdb檔案
檔案結構
說明:
我們用全大寫表示 常量標識;使用全小寫標識 變數
- REDIS:常量識別符號(5位元組)
- db_version:版本號(4位元組)
- 資料庫:也就是儲存的具體資料,具體長度由儲存的資料來說明,如果沒有資料,那就沒有這個欄位
- EOF:結束標識位(1位元組),也就是如果讀到這個地方表示 rdb檔案正文
讀取
完畢 - check_sum:校驗位置,就是前面的數字長度;
SELECTDB:常量(1位元組),標識為這裡是資料庫
db_number:資料庫號碼(1-5位元組不等)
key_value_pairs:資料庫中具體儲存的值
rdb檔案中 資料庫中 資料的值(k-v結構)
EXPIRETIME_MS:常量,標識帶有過期鍵
過期鍵的時間
KEYTYPE:上圖寫的是REDIS_RDB_TYPE_SET,這個是 儲存的型別,方便讀取 value 的值
key: 儲存的key
value :儲存的value 可能是 SDS、HASH 之類的。
五、AOF 持久化
AOF 持久化功能的實現可以分為命令追加(append )
、檔案寫人
、檔案同步 (sync)``三個
步驟。
命令追加
當 AOF 持久化功能處於開啟狀態時,伺服器在執行完一個寫命令之後,會以協議格式將被執行的寫命令追加到redisServer 結構體中 的 aof buf
緩衝區
的末尾
如果在來一條命令,那麼在向緩衝區末尾新增。
Redis 的伺服器程序就是一個事件迴圈
(1oop),這個迴圈中的檔案事件
負責接收客戶端
的命令請求
,以及向客戶端傳送命令回覆,而時間事件
則負責執行像servercron 函式這樣需要定時執行
的函式。---這段看不懂就略過,其實是 下面要說 的事件
因為伺服器在處理檔案事件時可能會執行寫命令,使得一些內容被追加到aof buf 緩衝區裡面,所以在伺服器每次結束一個事件迴圈之前,它都會呼叫 f1ushAppendonlyFile函式,考慮是否需要將 aof buf 緩衝區中的內容寫人和儲存到 AOF 檔案裡面。
載入/資料還原
建立一個不帶網路連線
的偽客戶端
(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 重寫緩衝區。
檔案寫入和同步
六、事件
Redis 伺服器是一個事件驅動程式
,伺服器需要處理以下兩類
事件:
-
檔案事件
(file event ):Redis伺服器
通過套接字
(含義就是通過網路連結)與客戶端(或者其他 Redis 伺服器)進行連線,而檔案事件
就是伺服器
對套接字
操作的抽象。伺服器與客戶端(或者其他伺服器)的通訊會產生相應的檔案事件,而伺服器則通過監聽並處理這些事件來完成一系列網路通訊操作。 -
時間事件
(time event ):Redis 伺服器中的一些操作(比如servercron 兩數)需要在給定
的時間點
執行,而時間事件
就是伺服器對這類定時操作的抽象。
文字事件
Redis 基於 Reactor模式
開發了自己的網路事件處理器
:這個處理器被稱為檔案事件處理器
(file event handler):
- 檔案事件處理器使用
I/0 多路複用
(multiplexing)【https://www.cnblogs.com/zhangxiaoji/p/16152141.html】程式來同時監聽多個套接宇,並根據套接字目前執行的任務來為套接字關聯不同的事件處理器。
- 當被監聽的套接字準備好執行連線應答(accept)、讀取(read)、寫人(write入關閉(close)等操作時,與操作相對應的檔案事件就會產生,這時檔案事件處理器就會呼叫套接字之前關聯好的事件處理器來處理這些事件雖然檔案事件處理器以單執行緒方式執行,但通過使用 IO 多路複用程式來監聽多個套接宇,檔案事件處理器既實現了高效能的網路通訊模型(可以理解為 一個服務端使用了一個執行緒(或者少量的執行緒)來處理多個客戶端請求),又可以很好地與 Redis 伺服器中其他同樣以單執行緒方式執行的模組進行對接,這保持了 Redis 內部單執行緒設計的簡單性
注意:io 多路複用和 文字事件分派器 中間的佇列是 單個的,也就是檔案事件分派器一次只處理一個事件。
時間事件
我們從之前的講述中可以看到,和時間相關的都是由 redisCron 函式進行處理的,那麼它就是我們的 時間事件應用例項了。
Redis 的時間事件分為以下兩類:
- 定時事件:讓一段程式在指定的時間之後執行一次。比如說,讓程式× 在當前時間
的 30毫秒之後執行一次。 - 週期性事件:讓一段程式每隔指定時間就執行一次。比如說,讓程式Y每隔 30毫秒就執行-
一次。
個時間事件主要由以下三個屬性組成:
- id:伺服器為時間事件建立的全域性唯一四D(標識號)。1D號按從小到大的順序遞增,
新事件的1D 號比舊事件的1D 號要大。 - when:毫秒精度的 UNIX 時間戳,記錄了時間事件的到達(arrive)時間。
- timeProc:時間事件處理器,一個兩數。當時間事件到達時,伺服器就會呼叫相
應的處理器來處理事件。
事件的排程與執行
因為redis 存在兩種事件型別,所以 redis 必須有個排程器去解決何時處理 文字事件 何時 處理 時間事件
這個是 aeProcessEvents函式來進行的
後面對 伺服器 和 客戶端 在進行詳細的研究。
參考資料#
《Redis設計與實現》-黃健巨集
部分圖片來與百度搜索