單機數據庫實現
九、數據庫
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
可以查看所有連接的客戶端。
客戶端的屬性有:
- flags,使用不同的標志來表示客戶端的角色,例如是普通的客戶端,還是主從同步的客戶端等等
- 輸入緩沖區,客戶端發來的數據,首先放到這里,然后再進行處理
- argv,argc,命令的參數值,和數量
- 輸出緩沖區,有兩個,一個是固定大小的16KB,一個是可變大小的
- 套接字ID,
- 身份驗證,記錄客戶端是否進行了身份驗證,如果否,不能執行除auth外的其他命令
- 命令實現函數,命令名和實現函數的映射,這個應該是全局統一的,不是每個客戶端都有一個的
- 名字,客戶端可以自己設置名字,如果沒有設置,為NULL
- 創建時間,客戶端的創建時間戳
- 上一次執行命令的時間,用於計算空轉時間
客戶端的生命周期:
- 創建,當客戶端連接上服務端后,服務端就會創建一個客戶端實例,添加到clients列表的后面
- 客戶端發送命令給服務端
- 發送的命令首先存儲到輸入緩沖區,然后生成文件事件E1
- 服務端處理命令
- 當Redis主進程執行該文件事件E1時,就會解析輸入緩沖區,解析里面的參數,和參數個數,存儲到argv和argc。例如如果執行命令
set test aa
,參數就是['set','test','aa']
,個數是3
- 解析后,根據argv[0],在命令實現函數里面尋找set命令對應的執行函數,命令部分大小寫
- 調用執行函數,傳入client對象
- 執行函數里面,執行相應的操作,把返回結果,存儲到輸出緩沖區。根據返回結果的大小,決定存儲到哪個緩沖區,如果大小超出服務器的限制,就直接關閉客戶端。
- 生成套接字可寫的文件事件E2
- 結束當前的文件事件
- 當Redis主進程執行該文件事件E1時,就會解析輸入緩沖區,解析里面的參數,和參數個數,存儲到argv和argc。例如如果執行命令
- 服務端返回命令的結果
- 當服務端執行文件事件E2時,把輸出緩沖區的數據,傳輸給客戶端
- 關閉客戶端
- 當出現以下情況,會關閉客戶端
- 客戶端進程退出或者殺死,這樣網絡連接(Socket)就會被關閉,客戶端也會被關閉
- 客戶端發送不符合協議格式的請求
- 客戶端成功CLIENT KILL命令的目標
- 如果服務器設置了timeout屬性(客戶端的超時時間),而客戶端的空轉時間超過timeout。如果客戶端在執行BLPOP,訂閱等命令,就不會被關閉。
- 發送的數據或者返回的數據超出緩沖區的限制
- 當出現以下情況,會關閉客戶端
十四、服務器
1. 執行set命令的整個流程
參考上面的客戶端生命周期
2.serverCron函數
參考時間事件里面的serverCron說明。
除此之外還有:
- 更新時間緩存,Redis里面有很多地方要用到服務器時間,對於時間精度要求不高的地方,Redis會使用時間緩存,而不是再去調用系統函數。
- 更新LRU時鍾,
- 更新服務器每秒執行命令次數
- 更新服務器內存峰值
- 處理SIGTERM信號。啟動服務器的時候,Redis會監聽SIGTERM信號,當收到信息后,會把shutdown_asap屬性置為1,在serverCront中,如果這個屬性是1,就會關閉服務器
- 管理客戶端資源
- 如果客戶端連接超時,關閉客戶端
- 管理數據庫資源,例如刪除過期的鍵,對字典進行收縮操作
- 執行被延遲的BGREWRITEAOF操作
- 檢查持久化操作的狀態
- 將AOF緩沖區的內容寫入AOF文件
- 增加cronloops計數器,這個計數器記錄了serverCron被執行的次數
3.初始化服務器
當啟動服務器的時候,服務器會做以下操作
- 初始化狀態結構
- 載入配置選項
- 初始化服務器數據結構,例如服務器實例,例如共享內存的數據
- 還原數據庫狀態,即載入RDB文件或者AOF文件
- 執行事件循環,就是事件章節中的死循環