《Redis 設計與實現》讀書筆記(二)


單機數據庫實現

九、數據庫

1.服務器中的數據庫

一個redis服務器保存多個數據庫。

struct redisServer {
		//一個數組,多個數據庫
	redisDb *db;
}

當執行select 1,就是切換數據庫到db[1],具體就是會修改redisClient.db指針到redisServer.db[1]

2.數據庫鍵空間

typedef struct redisDb{

dict *dict;//數據庫鍵空間
dict *expires;//過期時間

}

這里的dict就是上面說的字典數據結構。

這個字典的key就是redis里面的key,每個key都是字符串對象
值就是數據庫的值,可以是字符串對象,列表對象,哈希對象,集合對象,游戲集合對象。

3.鍵的過期時間

如果我們對一個鍵設置過期時間
redis就會在字典expires里面,加上key=過期的時間戳(精確到毫秒)。
執行ttl,redis會比較expires里面的時間戳和當前時間的差值,然后返回差值。
過期key的判定

  • 當訪問key時,redis會檢查key是否過期,如果是,刪除key,並報錯key不存在
  • 對於一直沒有訪問的key,redis會定期掃描expires里面的key,判定key是否過期,如果是,就刪除key

十、RDB持久化

存在內存中的數據,稱為數據庫狀態
持久化就是把數據庫狀態,保存為RDB文件,RDB文件是存在硬盤的。

1.客戶端發起保存

執行命令save,bgsave,可以立刻把數據庫狀態保存為RDB文件。

  • save命令是阻塞的,即執行過程中,服務器不能處理其他客戶端的請求。
  • bgsave是異步的,redis會啟動一個新進程,把內存的數據都復制到新進程,然后執行保存數據到RDB文件的工作,而原進程就繼續處理客戶端的請求

2.服務端定期保存

redis也會定期執行保存操作
服務器的配置:

save 900 1

表示服務器在900秒內,對數據庫執行了至少一次修改,服務器就會執行保存操作
如果有多個svae配置,它們直接的關系是或的關系,即滿足其中一個,就會執行保存

struct redisServer{
	long long dirty;
	time_t lastsave
}

數據庫對象中,有兩個屬性:

  • dirty,記錄距離上一次保存操作后,數據庫執行了多少次修改。
  • lastsave,上一次保存操作的時間戳

redis通過這兩個屬性來實現定期保存的機制

3.RDB文件結構

RDB文件由

REDIS db_version databases EOF cehcks_sum

構成

  • redis是一個字符串,
  • db_version是一個4字節的int類型,表示數據庫的版本號
  • databases 表示數據庫數據
  • EOF 1字節表示文件的結束
  • check_sum表示前面的數據的md5

1.數據庫數據

databases部分的構成:

SELECTDB db_number PAIRS

  • SELECTDB是1字節,常量
  • db_number 是數據庫編號
  • PAIRS是數據庫數據里面的鍵值對

2.鍵值對

鍵值對構成
EXPIRETIME_MS ms TYPE KEY VALUE

  • EXPIRETIME_MS 1字節常量,表示這個鍵有超時時間,可選
  • ms 超時時間的時間戳
  • TYPE 1字節常量,表示鍵的類型,redis會根據這個常量,來決定怎么解析后面的KEY和VALUE
  • KEY 是一個字符串對象,存儲方法和VALUE的字符串對象一樣
  • 值的對象

3.VALUE編碼

redis會根據TYPE這個常量,來決定怎么讀取VALUE的數據。
KEY肯定就是字符串類型了。

3.1字符串對象

如果TYPE=REDIS_ENCODING_STRING,表示這個對象是數值字符串對象
字符串對象有以下三種存儲類型
int類型

構成:

ENCODING  int
  • ENCODING 1字節常量,表示int類型,例如16位還是32位
  • int 數字

無壓縮字符串
如果TYPE=REDIS_ENCODING_RAW,表示這個是普通字符串

len string
  • len 字符串的長度
  • string 字符串的值

壓縮字符串
如果字符串的長度大於20字節,就會壓縮字符串。

REDIS_RDB_ENC_LZF  compressed_len origin_len compressed_string
  • REDIS_RDB_ENC_LZF 1字節常量,壓縮的算法
  • compressed_len 壓縮后的長度
  • origin_len 原字符串長度
  • compressed_string 壓縮后的內容

問題:
程序怎么知道這是int類型,還是無壓縮字符串,還是壓縮字符串的?

3.2列表對象

當TYPE=REDIS_RDB_TYPE_LIST 表示這是一個列表對象

list_length item1 item2 itemN
  • list_length 列表的長度
  • item1-N 列表的元素,都是字符串對象

3.3集合對象

如果TYPE=REDIS_RDB_TYPE_SET 那么表示這是一個集合對象

set_size elem1 ... elemN

  • set_size集合的長度
  • elem1 表示集合的元素,字符串對象

3.4哈希表對象

如果TYPE=REDIS_RDB_TYPE_HASH 那么表示這是一個哈希表對象

hash_size key_value_pair1 。。。。。。key_value_pairN
  • hash_size 哈希表的鍵值對數量
  • key_value_pair1 鍵值對的值

鍵值對的構成

key1 value1 key2 value2
  • key1 鍵值對的鍵,字符串對象
  • value1 鍵值對的值,字符串對象

3.5 有序集合對象

如果TYPE=REDIS_RDB_TYPE_ZSET,表示這是一個有序集合對象

sorted_set_size element1 。。。。  elementN
  • sorted_set_size元素的數量
  • element1 元素

每個元素的構成

member1 score1

  • member1 元素的內容,字符串對象
  • score1 元素的分值,redis會把int類型或者float類型轉換為字符串類型保存

4.RDB文件例子

REDIS 0 0 0 6 376\0\0 003 MSG 005 HELLO 377 207z=304fTL
343

  • REDIS
  • 0006是版本號
  • 376是SELECTDB常量
  • \0是db 0
  • \0是字符串類型
  • 003 MSG表示字符串MSG
  • 005 HELLO 表示字符串HELLO
  • 377是EOF
  • 后面是md5

5.讀入RDB文件

在Redis啟動的時候,會自動加載RDB文件,加載成功后,服務器才處理客戶端的請求。

十一、AOF持久化

不同於RDB一次存儲整個數據庫狀態
AOF(Append Only File)是每次執行寫命令,就append一條指令到文件。

1.命令追加

struct redisServer {

	sds aof_buf;
}

在redis 服務器對象中,有一個aof_buf屬性,用於存儲AOF命令
每次服務器執行寫命令的時候,都會往這個緩沖區寫AOF命令。
在Redis的時間事件中,會調用flshAppendOnlyFile函數,決定是否吧緩沖區的數據flush到文件。

例如如果執行命令 set key value
就會產生下面的AOF命令:

*3\4\n$3\r\nSET\4\n$3\r\nKEY\r\n$5\r\nVALUE\r\n

2.AOF文件載入

在啟動REDIS的時候,會加載AOF文件

3.AOF重寫

當寫命令很多的時候,甚至都是操作很少的幾個鍵的話,例如不斷地修改一個key的值,這樣就會有很多命令。
這時候就可以執行重寫命令,來把AOF命令壓縮。
例如命令:

set test a
set test b
set test c

會被壓縮為

set test c

因為上面的兩條命令就沒意義了。

實際的重寫進程,不會去掃描AOF文件,而是會掃描數據庫的鍵值對,然后執行SET或者push等命令。

4.AOF后台重寫

當執行BGREWRITEAOF命令,Redis就會執行后台重寫

  • 啟動一個新的進程,把數據庫狀態復制過去,執行重寫操作,也就是掃描所有數據庫,里面的所有key
  • 原進程繼續處理客戶端請求,寫操作寫入到緩沖區
  • 新進程執行完重寫操作后,生成新的AOF文件,發送信號給原進程
  • 原進程收到信號后,把把緩沖區的命令append到新的AOF文件,然后當前的AOF文件切換為新的AOF文件

十二、事件

redis是兩種事件

  • 文件事件,就是處理客戶端的請求
  • 時間時間,處理redis自身的一些定時任務

1.文件事件

Redis使用IO多路復用的方式,當有客戶端的請求(例如Socket有數據可以讀取,有數據可以寫),就會生成一個事件,塞到處理隊列里面
每當Redis執行文件事件的時候,就從中取一個事件來執行。
事件包含

  • Socket號
  • 客戶端實例

2.時間事件

redis里面會有一個時間事件隊列

一個時間事件包含

  • id 事件的id
  • when 什么時候執行,時間精確到毫秒
  • timeproc 執行函數

具體實現就是,redis會每個100毫秒,就去這個隊列里面遍歷,看哪個時間可以執行(when小於當前時間),如果可以就執行。

  • 如果是定期事件,執行完,就會從隊列里面刪除
  • 如果是周期性事件,執行完,刪除事件后,會創建一個新的事件插入到隊列

其實現在Redis只有一個事件事件,就是serverCron

這個事件會執行:

  • 更新服務器的各類統計信息
  • 清理數據庫中過期的鍵值對
  • 關閉和清理失效的客戶端
  • 嘗試進行RDB和AOF的持久化操作
  • 如果是主服務器,對從服務器進行定期同步
  • 如果是集群模式,對集群進行定期同步和連接測試

serverCron每秒運行10次,也就是每100毫秒一次。

3. 事件的調度

當Redis服務器啟動后,跑完了初始化的任務,就會死循環得跑下面這個流程:

def aeProcessEvents():
	time_event=aeSearchNearestTimer() #尋找最近的時間事件
	remaind_ms=time_event.when-unix_ts_now() #計算事件要在多少毫秒后執行
	if remaind_ms<0:
		remaind_ms=0
	timeval=create_timeval_with_ms(remaind_ms) #通過remaind_ms計算最長阻塞時間
	aeApiPoll(timeval) #等待文件事件,超時時間為最長阻塞時間,如果remaind_ms=0,就馬上返回,不阻塞
	processFileEvents() #執行文件事件
	processTimeEvents() #執行時間事件

所以總的來說

  • 會阻塞進程來等待文件事件
  • 阻塞的時間不會超過下個時間事件的執行時間

這樣可以保證

  • 時間事件可以盡量准時(不是完全准時)地被執行
  • 文件事件也可以及時處理

十三、客戶端

Redis的服務端對象有一個列表,保存所有的客戶端對象,每個連接過來的客戶端,都會創建一個客戶端實例。

struct redisServer {

	list *clients;
	
}

redis命令 client list 可以查看所有連接的客戶端。

客戶端的屬性有:

  1. flags,使用不同的標志來表示客戶端的角色,例如是普通的客戶端,還是主從同步的客戶端等等
  2. 輸入緩沖區,客戶端發來的數據,首先放到這里,然后再進行處理
  3. argv,argc,命令的參數值,和數量
  4. 輸出緩沖區,有兩個,一個是固定大小的16KB,一個是可變大小的
  5. 套接字ID,
  6. 身份驗證,記錄客戶端是否進行了身份驗證,如果否,不能執行除auth外的其他命令
  7. 命令實現函數,命令名和實現函數的映射,這個應該是全局統一的,不是每個客戶端都有一個的
  8. 名字,客戶端可以自己設置名字,如果沒有設置,為NULL
  9. 創建時間,客戶端的創建時間戳
  10. 上一次執行命令的時間,用於計算空轉時間

客戶端的生命周期:

  1. 創建,當客戶端連接上服務端后,服務端就會創建一個客戶端實例,添加到clients列表的后面
  2. 客戶端發送命令給服務端
    1. 發送的命令首先存儲到輸入緩沖區,然后生成文件事件E1
  3. 服務端處理命令
    1. 當Redis主進程執行該文件事件E1時,就會解析輸入緩沖區,解析里面的參數,和參數個數,存儲到argv和argc。例如如果執行命令 set test aa,參數就是['set','test','aa'],個數是3
    2. 解析后,根據argv[0],在命令實現函數里面尋找set命令對應的執行函數,命令部分大小寫
    3. 調用執行函數,傳入client對象
    4. 執行函數里面,執行相應的操作,把返回結果,存儲到輸出緩沖區。根據返回結果的大小,決定存儲到哪個緩沖區,如果大小超出服務器的限制,就直接關閉客戶端。
    5. 生成套接字可寫的文件事件E2
    6. 結束當前的文件事件
  4. 服務端返回命令的結果
    1. 當服務端執行文件事件E2時,把輸出緩沖區的數據,傳輸給客戶端
  5. 關閉客戶端
    1. 當出現以下情況,會關閉客戶端
      1. 客戶端進程退出或者殺死,這樣網絡連接(Socket)就會被關閉,客戶端也會被關閉
      2. 客戶端發送不符合協議格式的請求
      3. 客戶端成功CLIENT KILL命令的目標
      4. 如果服務器設置了timeout屬性(客戶端的超時時間),而客戶端的空轉時間超過timeout。如果客戶端在執行BLPOP,訂閱等命令,就不會被關閉。
      5. 發送的數據或者返回的數據超出緩沖區的限制

十四、服務器

1. 執行set命令的整個流程

參考上面的客戶端生命周期

2.serverCron函數

參考時間事件里面的serverCron說明。
除此之外還有:

  1. 更新時間緩存,Redis里面有很多地方要用到服務器時間,對於時間精度要求不高的地方,Redis會使用時間緩存,而不是再去調用系統函數。
  2. 更新LRU時鍾,
  3. 更新服務器每秒執行命令次數
  4. 更新服務器內存峰值
  5. 處理SIGTERM信號。啟動服務器的時候,Redis會監聽SIGTERM信號,當收到信息后,會把shutdown_asap屬性置為1,在serverCront中,如果這個屬性是1,就會關閉服務器
  6. 管理客戶端資源
    1. 如果客戶端連接超時,關閉客戶端
  7. 管理數據庫資源,例如刪除過期的鍵,對字典進行收縮操作
  8. 執行被延遲的BGREWRITEAOF操作
  9. 檢查持久化操作的狀態
  10. 將AOF緩沖區的內容寫入AOF文件
  11. 增加cronloops計數器,這個計數器記錄了serverCron被執行的次數

3.初始化服務器

當啟動服務器的時候,服務器會做以下操作

  1. 初始化狀態結構
  2. 載入配置選項
  3. 初始化服務器數據結構,例如服務器實例,例如共享內存的數據
  4. 還原數據庫狀態,即載入RDB文件或者AOF文件
  5. 執行事件循環,就是事件章節中的死循環


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM