Redis 服務(wù)器與客戶端

2021-09-18 15:34 更新

服務(wù)器與客戶端

前面的章節(jié)介紹了所有 Redis 的重要功能組件:數(shù)據(jù)結(jié)構(gòu)、數(shù)據(jù)類型、事務(wù)、Lua 環(huán)境、事件處理、數(shù)據(jù)庫、持久化,等等,但是我們還沒有對 Redis 服務(wù)器本身做任何介紹。

不過,服務(wù)器本身并沒有多少需要介紹的新東西,因?yàn)榉?wù)器除了維持服務(wù)器狀態(tài)之外,最重要的就是將前面介紹過的各個(gè)功能模塊組合起來,而這些功能模塊在前面的章節(jié)里已經(jīng)介紹過了,所以本章將焦點(diǎn)放在服務(wù)器的初始化過程,以及服務(wù)器對命令的處理過程上。

本章首先介紹服務(wù)器的初始化操作,觀察一個(gè) Redis 服務(wù)器從啟動到可以接受客戶端連接,需要經(jīng)過什么步驟。

然后介紹客戶端是如何連接到服務(wù)器的,而服務(wù)器又是如何維持多個(gè)客戶端的不同狀態(tài)的。

文章最后將介紹命令從發(fā)送到處理的整個(gè)過程,并列舉了一個(gè) SET 命令的執(zhí)行過程作為例子。

初始化服務(wù)器

從啟動 Redis 服務(wù)器,到服務(wù)器可以接受外來客戶端的網(wǎng)絡(luò)連接這段時(shí)間,Redis 需要執(zhí)行一系列初始化操作。

整個(gè)初始化過程可以分為以下六個(gè)步驟:

  1. 初始化服務(wù)器全局狀態(tài)。
  2. 載入配置文件。
  3. 創(chuàng)建 daemon 進(jìn)程。
  4. 初始化服務(wù)器功能模塊。
  5. 載入數(shù)據(jù)。
  6. 開始事件循環(huán)。

以下各個(gè)小節(jié)將介紹 Redis 服務(wù)器初始化的各個(gè)步驟。

1. 初始化服務(wù)器全局狀態(tài)

redis.h/redisServer 結(jié)構(gòu)記錄了和服務(wù)器相關(guān)的所有數(shù)據(jù),這個(gè)結(jié)構(gòu)主要包含以下信息:

  • 服務(wù)器中的所有數(shù)據(jù)庫。
  • 命令表:在執(zhí)行命令時(shí),根據(jù)字符來查找相應(yīng)命令的實(shí)現(xiàn)函數(shù)。
  • 事件狀態(tài)。
  • 服務(wù)器的網(wǎng)絡(luò)連接信息:套接字地址、端口,以及套接字描述符。
  • 所有已連接客戶端的信息。
  • Lua 腳本的運(yùn)行環(huán)境及相關(guān)選項(xiàng)。
  • 實(shí)現(xiàn)訂閱與發(fā)布(pub/sub)功能所需的數(shù)據(jù)結(jié)構(gòu)。
  • 日志(log)和慢查詢?nèi)罩荆╯lowlog)的選項(xiàng)和相關(guān)信息。
  • 數(shù)據(jù)持久化(AOF 和 RDB)的配置和狀態(tài)。
  • 服務(wù)器配置選項(xiàng):比如要?jiǎng)?chuàng)建多少個(gè)數(shù)據(jù)庫,是否將服務(wù)器進(jìn)程作為 daemon 進(jìn)程來運(yùn)行,最大連接多少個(gè)客戶端,壓縮結(jié)構(gòu)(zip structure)的實(shí)體數(shù)量,等等。
  • 統(tǒng)計(jì)信息:比如鍵有多少次命令、不命中,服務(wù)器的運(yùn)行時(shí)間,內(nèi)存占用,等等。

Note

為了簡潔起見,上面只列出了單機(jī)情況下的 Redis 服務(wù)器信息,不包含 SENTINEL 、 MONITOR 、 CLUSTER 等功能的信息。

在這一步,程序創(chuàng)建一個(gè) redisServer 結(jié)構(gòu)的實(shí)例變量 server 用作服務(wù)器的全局狀態(tài),并將 server 的各個(gè)屬性初始化為默認(rèn)值。

當(dāng) server 變量的初始化完成之后,程序進(jìn)入服務(wù)器初始化的下一步:讀入配置文件。

2. 載入配置文件

在初始化服務(wù)器的上一步中,程序?yàn)?server 變量(也即是服務(wù)器狀態(tài))的各個(gè)屬性設(shè)置了默認(rèn)值,但這些默認(rèn)值有時(shí)候并不是最合適的:

  • 用戶可能想使用 AOF 持久化,而不是默認(rèn)的 RDB 持久化。
  • 用戶可能想用其他端口來運(yùn)行 Redis ,以避免端口沖突。
  • 用戶可能不想使用默認(rèn)的 16 個(gè)數(shù)據(jù)庫,而是分配更多或更少數(shù)量的數(shù)據(jù)庫。
  • 用戶可能想對默認(rèn)的內(nèi)存限制措施和回收策略做調(diào)整。

等等。

為了讓使用者按自己的要求配置服務(wù)器,Redis 允許用戶在運(yùn)行服務(wù)器時(shí),提供相應(yīng)的配置文件(config file)或者顯式的選項(xiàng)(option),Redis 在初始化完 server 變量之后,會讀入配置文件和選項(xiàng),然后根據(jù)這些配置來對 server 變量的屬性值做相應(yīng)的修改:

  1. 如果單純執(zhí)行 redis-server 命令,那么服務(wù)器以默認(rèn)的配置來運(yùn)行 Redis 。

  2. 另一方面, 如果給 Redis 服務(wù)器送入一個(gè)配置文件, 那么 Redis 將按配置文件的設(shè)置來更新服務(wù)器的狀態(tài)。

比如說, 通過命令 redis-server /etc/my-redis.conf , Redis 會根據(jù) my-redis.conf 文件的內(nèi)容來對服務(wù)器狀態(tài)做相應(yīng)的修改。

  1. 除此之外, 還可以顯式地給服務(wù)器傳入選項(xiàng), 直接修改服務(wù)器配置。

舉個(gè)例子, 通過命令 redis-server --port 10086 , 可以讓 Redis 服務(wù)器端口變更為 10086

  1. 當(dāng)然, 同時(shí)使用配置文件和顯式選項(xiàng)也是可以的, 如果文件和選項(xiàng)有沖突的地方, 那么優(yōu)先使用選項(xiàng)所指定的配置值。

舉個(gè)例子, 如果運(yùn)行命令 redis-server /etc/my-redis.conf --port 10086 , 并且 my-redis.conf 也指定了 port 選項(xiàng), 那么服務(wù)器將優(yōu)先使用 --port 10086 (實(shí)際上是選項(xiàng)指定的值覆蓋了配置文件中的值)。

3. 創(chuàng)建 daemon 進(jìn)程

Redis 默認(rèn)以 daemon 進(jìn)程的方式運(yùn)行。

當(dāng)服務(wù)器初始化進(jìn)行到這一步時(shí),程序?qū)?chuàng)建 daemon 進(jìn)程來運(yùn)行 Redis ,并創(chuàng)建相應(yīng)的 pid 文件。

4. 初始化服務(wù)器功能模塊

在這一步,初始化程序完成兩件事:

  • server 變量的數(shù)據(jù)結(jié)構(gòu)子屬性分配內(nèi)存。
  • 初始化這些數(shù)據(jù)結(jié)構(gòu)。

為數(shù)據(jù)結(jié)構(gòu)分配內(nèi)存,并初始化這些數(shù)據(jù)結(jié)構(gòu),等同于對相應(yīng)的功能進(jìn)行初始化。

比如說,當(dāng)為訂閱與發(fā)布所需的鏈表分配內(nèi)存之后,訂閱與發(fā)布功能就處于就緒狀態(tài),隨時(shí)可以為 Redis 所用了。

在這一步,程序完成的主要?jiǎng)幼魅缦拢?/p>

  • 初始化 Redis 進(jìn)程的信號功能。
  • 初始化日志功能。
  • 初始化客戶端功能。
  • 初始化共享對象。
  • 初始化事件功能。
  • 初始化數(shù)據(jù)庫。
  • 初始化網(wǎng)絡(luò)連接。
  • 初始化訂閱與發(fā)布功能。
  • 初始化各個(gè)統(tǒng)計(jì)變量。
  • 關(guān)聯(lián)服務(wù)器常規(guī)操作(cron job)到時(shí)間事件,關(guān)聯(lián)客戶端應(yīng)答處理器到文件事件。
  • 如果 AOF 功能已打開,那么打開或創(chuàng)建 AOF 文件。
  • 設(shè)置內(nèi)存限制。
  • 初始化 Lua 腳本環(huán)境。
  • 初始化慢查詢功能。
  • 初始化后臺操作線程。

完成這一步之后,服務(wù)器打印出 Redis 的 ASCII LOGO 、服務(wù)器版本等信息,表示所有功能模塊已經(jīng)就緒,可以等待被使用了:

               _._
          _.-``__ ''-._
     _.-``    `.  `_.  ''-._           Redis 2.9.7 (7a47887b/1) 32 bit
 .-`` .-```.  ```\/    _.,_ ''-._
(    '      ,       .-`  | `,    )     Running in stand alone mode
|`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
|    `-._   `._    /     _.-'    |     PID: 6717
 `-._    `-._  `-./  _.-'    _.-'
|`-._`-._    `-.__.-'    _.-'_.-'|
|    `-._`-._        _.-'_.-'    |           http://redis.io
 `-._    `-._`-.__.-'_.-'    _.-'
|`-._`-._    `-.__.-'    _.-'_.-'|
|    `-._`-._        _.-'_.-'    |
 `-._    `-._`-.__.-'_.-'    _.-'
     `-._    `-.__.-'    _.-'
         `-._        _.-'
             `-.__.-'

雖然所有功能已經(jīng)就緒,但這時(shí)服務(wù)器的數(shù)據(jù)庫還是一片空白,程序還需要將服務(wù)器上一次執(zhí)行時(shí)記錄的數(shù)據(jù)載入到當(dāng)前服務(wù)器中,服務(wù)器的初始化才算真正完成。

5. 載入數(shù)據(jù)

在這一步,程序需要將持久化在 RDB 或者 AOF 文件里的數(shù)據(jù),載入到服務(wù)器進(jìn)程里面。

如果服務(wù)器有啟用 AOF 功能的話,那么使用 AOF 文件來還原數(shù)據(jù);否則,程序使用 RDB 文件來還原數(shù)據(jù)。

當(dāng)執(zhí)行完這一步時(shí),服務(wù)器打印出一段載入完成信息:

[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds

6. 開始事件循環(huán)

到了這一步,服務(wù)器的初始化已經(jīng)完成,程序打開事件循環(huán),開始接受客戶端連接。

以下是服務(wù)器在這一步打印的信息:

[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379

以下是初始化完成之后,服務(wù)器狀態(tài)和各個(gè)模塊之間的關(guān)系圖:


客戶端連接到服務(wù)器

當(dāng) Redis 服務(wù)器完成初始化之后,它就準(zhǔn)備好可以接受外來客戶端的連接了。

當(dāng)一個(gè)客戶端通過套接字函數(shù) connect 到服務(wù)器時(shí),服務(wù)器執(zhí)行以下步驟:

  1. 服務(wù)器通過文件事件無阻塞地 accept 客戶端連接,并返回一個(gè)套接字描述符 fd 。
  2. 服務(wù)器為 fd 創(chuàng)建一個(gè)對應(yīng)的 redis.h/redisClient 結(jié)構(gòu)實(shí)例,并將該實(shí)例加入到服務(wù)器的已連接客戶端的鏈表中。
  3. 服務(wù)器在事件處理器為該 fd 關(guān)聯(lián)讀文件事件。

完成這三步之后,服務(wù)器就可以等待客戶端發(fā)來命令請求了。

Redis 以多路復(fù)用的方式來處理多個(gè)客戶端,為了讓多個(gè)客戶端之間獨(dú)立分開、不互相干擾,服務(wù)器為每個(gè)已連接客戶端維持一個(gè) redisClient 結(jié)構(gòu),從而單獨(dú)保存該客戶端的狀態(tài)信息。

redisClient 結(jié)構(gòu)主要包含以下信息:

  • 套接字描述符。
  • 客戶端正在使用的數(shù)據(jù)庫指針和數(shù)據(jù)庫號碼。
  • 客戶端的查詢緩沖(query buffer)和回復(fù)緩存(reply buffer)。
  • 一個(gè)指向命令函數(shù)的指針,以及字符串形式的命令、命令參數(shù)和命令個(gè)數(shù),這些屬性會在命令執(zhí)行時(shí)使用。
  • 客戶端狀態(tài):記錄了客戶端是否處于 SLAVE 、 MONITOR 或者事務(wù)狀態(tài)。
  • 實(shí)現(xiàn)事務(wù)功能(比如 MULTIWATCH )所需的數(shù)據(jù)結(jié)構(gòu)。
  • 實(shí)現(xiàn)阻塞功能(比如 BLPOPBRPOPLPUSH )所需的數(shù)據(jù)結(jié)構(gòu)。
  • 實(shí)現(xiàn)訂閱與發(fā)布功能(比如 PUBLISHSUBSCRIBE )所需的數(shù)據(jù)結(jié)構(gòu)。
  • 統(tǒng)計(jì)數(shù)據(jù)和選項(xiàng):客戶端創(chuàng)建的時(shí)間,客戶端和服務(wù)器最后交互的時(shí)間,緩存的大小,等等。

Note

為了簡潔起見,上面列出的客戶端結(jié)構(gòu)信息不包含復(fù)制(replication)的相關(guān)屬性。

命令的請求、處理和結(jié)果返回

當(dāng)客戶端連上服務(wù)器之后,客戶端就可以向服務(wù)器發(fā)送命令請求了。

從客戶端發(fā)送命令請求,到命令被服務(wù)器處理、并將結(jié)果返回客戶端,整個(gè)過程有以下步驟:

  1. 客戶端通過套接字向服務(wù)器傳送命令協(xié)議數(shù)據(jù)。
  2. 服務(wù)器通過讀事件來處理傳入數(shù)據(jù),并將數(shù)據(jù)保存在客戶端對應(yīng) redisClient 結(jié)構(gòu)的查詢緩存中。
  3. 根據(jù)客戶端查詢緩存中的內(nèi)容,程序從命令表中查找相應(yīng)命令的實(shí)現(xiàn)函數(shù)。
  4. 程序執(zhí)行命令的實(shí)現(xiàn)函數(shù),修改服務(wù)器的全局狀態(tài) server 變量,并將命令的執(zhí)行結(jié)果保存到客戶端 redisClient 結(jié)構(gòu)的回復(fù)緩存中,然后為該客戶端的 fd 關(guān)聯(lián)寫事件。
  5. 當(dāng)客戶端 fd 的寫事件就緒時(shí),將回復(fù)緩存中的命令結(jié)果傳回給客戶端。至此,命令執(zhí)行完畢。

命令請求實(shí)例: SET 的執(zhí)行過程

為了更直觀地理解命令執(zhí)行的整個(gè)過程,我們用一個(gè)實(shí)際執(zhí)行 SET 命令的例子來講解命令執(zhí)行的過程。

假設(shè)現(xiàn)在客戶端 C1 是連接到服務(wù)器 S 的一個(gè)客戶端,當(dāng)用戶執(zhí)行命令 SET YEAR 2013 時(shí),客戶端調(diào)用寫入函數(shù),將協(xié)議內(nèi)容 *3\r\n$3\r\nSET\r\n$4\r\nYEAR\r\n$4\r\n2013\r\n" 寫入連接到服務(wù)器的套接字中。

當(dāng) S 的文件事件處理器執(zhí)行時(shí),它會察覺到 C1 所對應(yīng)的讀事件已經(jīng)就緒,于是它將協(xié)議文本讀入,并保存在查詢緩存。

通過對查詢緩存進(jìn)行分析(parse),服務(wù)器在命令表中查找 SET 字符串所對應(yīng)的命令實(shí)現(xiàn)函數(shù),最終定位到 t_string.c/setCommand 函數(shù),另外,兩個(gè)命令參數(shù) YEAR2013 也會以字符串的形式保存在客戶端結(jié)構(gòu)中。

接著,程序?qū)⒖蛻舳恕⒁獔?zhí)行的命令、命令參數(shù)等送入命令執(zhí)行器:執(zhí)行器調(diào)用 setCommand 函數(shù),將數(shù)據(jù)庫中 YEAR 鍵的值修改為 2013 ,然后將命令的執(zhí)行結(jié)果保存在客戶端的回復(fù)緩存中,并為客戶端 fd 關(guān)聯(lián)寫事件,用于將結(jié)果回寫給客戶端。

因?yàn)?YEAR 鍵的修改,其他和數(shù)據(jù)庫命名空間相關(guān)程序,比如 AOF 、REPLICATION 還有事務(wù)安全性檢查(是否修改了被 WATCH 監(jiān)視的鍵?)也會被觸發(fā),當(dāng)這些后續(xù)程序也執(zhí)行完畢之后,命令執(zhí)行器退出,服務(wù)器其他程序(比如時(shí)間事件處理器)繼續(xù)運(yùn)行。

當(dāng) C1 對應(yīng)的寫事件就緒時(shí),程序就會將保存在客戶端結(jié)構(gòu)回復(fù)緩存中的數(shù)據(jù)回寫給客戶端,當(dāng)客戶端接收到數(shù)據(jù)之后,它就將結(jié)果打印出來,顯示給用戶看。

以上就是 SET YEAR 2013 命令執(zhí)行的整個(gè)過程。

小結(jié)

  • 服務(wù)器經(jīng)過初始化之后,才能開始接受命令。
  • 服務(wù)器初始化可以分為六個(gè)步驟:
  1. 初始化服務(wù)器全局狀態(tài)。
  2. 載入配置文件。
  3. 創(chuàng)建 daemon 進(jìn)程。
  4. 初始化服務(wù)器功能模塊。
  5. 載入數(shù)據(jù)。
  6. 開始事件循環(huán)。
  • 服務(wù)器為每個(gè)已連接的客戶端維持一個(gè)客戶端結(jié)構(gòu),這個(gè)結(jié)構(gòu)保存了這個(gè)客戶端的所有狀態(tài)信息。
  • 客戶端向服務(wù)器發(fā)送命令,服務(wù)器接受命令然后將命令傳給命令執(zhí)行器,執(zhí)行器執(zhí)行給定命令的實(shí)現(xiàn)函數(shù),執(zhí)行完成之后,將結(jié)果保存在緩存,最后回傳給客戶端。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號