Davids閱讀筆記:《Redis設計與實現》

閱讀筆記:《Redis設計與實現》

關注可以查看更多粉絲專享blog~

第一部分:數據結構與對象

第一章:(簡單動態字符串)

1、Redis只會使用C字符串作爲字面量,在大多數情況下,Redis使用SDS(Simple Dynamic String,簡單動態字符串)作爲字符串表示。
2、比起C字符串,SDS具有以下優點:
2.1:常數複雜度獲取字符串長度
2.2:杜絕緩衝區溢出
2.3:減少修改字符串長度時所需的內存重分配次數
2.4:二進制安全
2.5:兼容部分C字符串函數

第二章:(鏈表)

1、鏈表被廣泛用於實現Redis的各種功能,如列表鍵、發佈訂閱、慢查詢、監視器等。
2、每個鏈表節點由listNode表示,每個節點都有指向前置節點和後置節點的指針,所以Redis的鏈表是雙端鏈表。
3、每個鏈表都使用一個list來表示,這個結構帶有表頭節點指針、表尾節點指針,以及鏈表長度信息等。
4、因爲鏈表表頭節點的前置節點和表尾節點的後置節點都指向NULL,所以Redis的鏈表是無環鏈表。
5、通過爲鏈表設置不同類型的特定函數,Redis鏈表可以儲存不同類型的值。

第三章:(字典)

1、字典被廣泛用於實現Redis的各種功能,如數據庫和哈希鍵。
2、Redis中的字典使用哈希表作爲底層實現,每個字典都帶有兩個哈希表,一個平時使用,一個rehash時使用。
3、當字典被用作數據庫的底層實現,或者哈希鍵的底層實現時,Redis使用MurmurHash2算法來計算鍵的哈希值。
4、哈希表使用鏈地址法來解決鍵衝突,被分配到同一索引上的多個鍵值對會連成一個單向鏈表。
5、在對哈希表進行擴展或者收縮時程序需要將現有的哈希表的所有鍵值對rehash到新哈希表中,並且這個rehash過程並不是一次性完成的,是漸進式的。

第四章:(跳躍表)

1、跳躍表是有序集合的底層實現之一。
2、Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成,其中zskiplist用於儲存跳躍表信息(如:表頭節點、表尾節點、長度),而zskiplistNode用於儲存節點信息。
3、每個跳躍表節點的層高都是1-32之間的隨機數。
4、在同一跳躍表中,多個節點的分值可以包含相同的分值,但是每個節點的成員對象必須是唯一的。
5、跳躍表中的節點按照分值大小排序,相同分值的按照成員對象大小排序。

第五章:(整數集合)

1、整數集合是集合鍵的底層實現之一。
2、整數集合的底層實現是數組,這個數組以有序、無重複的方式保存集合元素,在有需要時,程序會根據新添加元素的類型,改變這個數組的類型。
3、升級操作爲整個集合帶來了操作上的靈活性,並且儘可能的節約了內存。(提升編碼方式)
4、整數集合只支持升級操作,不支持降級操作。

第六章:(壓縮列表)

1、壓縮列表是爲了節約內存開發的一種順序型數據結構。
2、壓縮列表被用作列表鍵和哈希鍵的底層實現之一。
3、壓縮列表可以包含多個節點,每個節點可以保存一個字節數組或者整數值。
4、添加節點到壓縮列表,或者從壓縮列表中刪除節點,可能會引發連鎖更新操作,但這種操作出現的機率並不高。(因爲添加了一個表頭節點大於254,則第二個節點的previous_entry_length需要由1字節擴展至5字節,若此時超過254則會觸發下一個節點因此產生連鎖更新,刪除節點也是一樣頭結點大於254,第二個節點小於254,若刪除第二個節點,後面的節點剛好超過254,也會引發連鎖更新)

第七章:(對象)

1、Redis數據庫中每個鍵值對的鍵和值都是一個對象。
2、Redis中有字符串、列表、哈希、集合和有序集合五中類型的對象,每種類型的對象至少有兩種及以上的編碼方式,不同編碼方式可以在不同場景上優化對象的使用效率。
3、服務器在執行某些命令前會先檢查給定鍵的類型能否執行指定的命令,而檢查鍵的類型就是檢查鍵的值對象的類型。
4、Redis的對象系統帶有引用計數實現的內存回收機制,當一個對象不再被使用時,該對象所佔有的內存就會被自動釋放。
5、Redis會共享值爲0-9999的字符串對象。
6、對象會記錄最後一次被訪問的時間,這個時間用於計算對象的空轉時間。

第二部分:單機數據庫的實現

第一章:(數據庫)

1、Redis服務器所有數據庫都保存在redisServer.db,而數據庫數量則由redisServer.dbnum屬性保存。
2、客戶端通過修改目標數據庫指針,讓它指向redisServer.db中的不同元素來切換數據庫。
3、數據庫主要由dict和expires兩個字典構成,其中dict負責保存鍵值對,expires負責保存鍵的過期時間。(兩個字典的key指向的是同一份數據,節約內存!)
4、因爲數據庫是由字典構成,所以對數據庫的操作都是建立在字典操作之上的。
5、數據庫鍵總是一個字符串對象,而值則可以是任意一種Redis對象,字符串對象(字符串鍵),列表對象(鍵),哈希對象(哈希鍵)、集合對象(集合鍵)、有序集合對象(有序集合鍵)。
6、expires字典的鍵指向了數據庫中的某個鍵,值則記錄了數據庫鍵的過期時間,過期時間是一個以毫秒爲單位的UNIX時間戳。
7、Redis使用惰性刪除和定期刪除兩種策略來處理過期鍵:惰性刪除是隻在碰到過期鍵時才進行刪除操作;定期刪除則每隔一段時間主動查找並刪除。
8、執行SAVE和BGSAVE命令所產生的的RDB文件不會包含過期的鍵。
9、執行BGREWRITEAOF命令時所產生的AOF文件不會包含過期的鍵。
10、當一個過期鍵被刪除之後,服務器會追加一條DEL命令到現有的AOF文件的末尾,顯式的刪除過期鍵。
11、當主服務器刪除一個過期鍵之後,它會向所有的從服務器發送一條DEL命令,顯式的刪除過期鍵。
12、從服務器發現過期鍵也不會自作主張地刪除它,而是等主服務器發來DEL命令,這種統一、中心化的過期鍵刪除策略可以保證主從服務器數據的一致性。
13、當Redis命令對數據庫進行修改之後,服務器會根據配置向客戶端發送數據庫通知。(如:訂閱數據庫鍵變化的場景)。

第二章:(RDB持久化)

1、RDB文件用於保存和還原Redis服務器中所有數據庫中的所有鍵值對數據。
2、SAVE命令由服務器進程直接執行保存操作,所以該命令會阻塞服務器。
3、BGSAVE命令由子進程執行保存操作,所以該命令不會阻塞服務器。
4、服務器狀態中會保存所有用save選項設置的保存條件,當任意一個條件滿足時就會執行BGSAVE命令。(如:save 900 1,save 300 10, save 60 10000)
5、RDB文件是一個經過壓縮的二進制文件,由多部分組成。(REDIS db_version databases EOF check_num)
6、對於不同類型的鍵值對,RDB文件會使用不同的方式來保存它們。

第三章:(AOF持久化)

1、AOF文件通過保存所有修改數據庫的寫命令請求來記錄服務器的數據庫狀態。
2、AOF文件中所有命令都以Redis命令請求協議的格式保存。
3、命令請求會先保存到AOF緩衝區裏面,之後再定期寫入並同步到AOF文件。
4、appendfsync選項的不同值對AOF持久化功能的安全性以及服務器性能有很大的影響。
5、服務器只要載入並重新執行AOF文件中的命令,就可以還原數據庫本來的狀態。
6、AOF重寫可以產生一個新的AOF文件,這個新的AOF文件和原有的AOF文件所保存的數據庫狀態是一致的,但體積更小。
7、AOF重寫是一個有歧義的名稱,該功能是通過讀取數據庫中的鍵值對來實現的,程序無須對現有的AOF文件進行任何讀取、分析或者寫入操作。
8、在執行BGREWRITEAOF命令時,Redis服務器會維護一個AOF重寫緩衝區,該緩衝區會在子進程創建新的AOF文件期間,記錄服務器執行的所有寫命令,當子進程完成創建新AOF文件的工作後,服務器會將重寫緩衝區中所有內容追加到新AOF文件的末尾,使得新舊兩個AOF文件所保存的數據庫狀態一致。最後,服務器用新的AOF文件替換舊的AOF文件,以此來完成AOF文件重寫操作。

第四章:(事件)

1、Redis服務器是一個事件驅動程序,服務器處理的事件分爲時間事件和文件事件兩類。
2、文件事件處理器是基於Reactor模式實現的網絡通信程序。
3、文件事件是對套接字事件的抽象:每次套接字變爲可應答(acceptable)、可讀(writable)或者可讀(readable),相應的文件事件就會產生
4、文件事件分爲AE_READABLE事件(讀事件)和AE_WRITABLE(寫事件),兩類。(acceptable爲可讀事件)
5、時間事件分爲定時事件和週期性事件:定時事件是隻在指定時間到達一次;週期性事件是每隔一段時間到達一次。
6、服務器在一般情況下只執行serverCron一個時間事件,而且這是一個週期性事件。(默認每100ms運行一次,Redis 2.8版本之後可通過redis.conf關於hz選項來設置)
6.1:更新服務器的各類統計信息,比如時間、內存佔用、數據庫佔用情況等。
6.2:清理數據庫過期的鍵值對。
6.3:關閉和清理連接失敗的客戶端。
6.4:嘗試進行AOF和RDB操作。
6.5:如果服務器是主服務器,那麼對從服務器進行定期同步。
6.6:如果處於集羣模式,對集羣進行定期同步和連接測試。
7、文件事件和時間事件是合作關係,服務器會輪流執行這兩種事件,並且處理過程中也不會出現搶佔。
8、時間事件實際處理時間通常會比設定的到達時間晚一些。(原因見第二部分第四章第7節)

第五章:(客戶端)

1、服務器狀態結構用clients鏈表連接起多個客戶端狀態,新添加的客戶端狀態會被放到鏈表的末尾。
2、客戶端狀態的flags屬性使用不同標誌來表示客戶端的角色,以及客戶端當前所處的狀態。
3、輸入緩衝區記錄了客戶端發送的命令請求,這個緩衝區大小不能超過1GB。
4、命令的參數和參數個數會被記錄在客戶端狀態的argv和argc屬性裏面,而cmd屬性則記錄了客戶端要執行命令的實現函數。
5、客戶端有固定大小緩衝區和可變大小緩衝區,固定大小緩衝區最大大小爲16KB,而可變大小緩衝區最大大小不能超過服務器設置的硬性限制值。
6、輸出緩衝區的限制有兩種,如果輸出緩衝區的大小超過了服務器的硬性限制,那麼客戶端會被立即關閉;除此之外,如果客戶端在一定時間內,一直超過服務器的軟性限制,那麼客戶端也會被關閉(無限制:client-output-buffer-limit normal 0 0 0 設置從服務器限制:client-output-buffer-limit slave 256m 64mb 60 設置發佈訂閱客戶端限制:client-output-buffer-limit pubsub 32m 8mb 60)
7、當一個客戶端通過網絡連接上服務器時,服務器會爲這個客戶端創建相應的客戶端狀態,網絡連接關閉、發送了不合協議格式的命令請求、成爲CLIENT KILL命令的目標、空轉時間超時、輸出緩衝區大小超出限制,以上原因都會造成客戶端被關閉。
8、處理Lua腳本的僞客戶端在服務器初始化時創建,這個客戶端會一直存在,直到服務器關閉。
9、載入AOF文件時使用的僞客戶端在載入工作開始時動態創建,載入工作完畢之後關閉。

第六章:(服務器)

1、一個命令請求從發送到完成包括以下步驟:
1.1:客戶端將命令請求發送給服務器。
1.2:服務器讀取命令請求,並分析出命令參數。
1.3:命令執行器根據參數查找命令的實現函數,然後執行實現函數並得出命令回覆。
1.4:服務器將命令回覆返回給客戶端。
2、serverCron每隔100ms執行一次,它的工作主要包括更新服務器狀態信息,處理服務器接受SIGTERM信號,管理客戶端資源和數據庫狀態,檢查並執行持久化操作。
3、服務器從啓動到能夠執行客戶端命令需要執行以下步驟:
3.1:初始化服務器狀態。
3.2:載入服務器配置。
3.3:初始化服務器數據結構。
3.4:還原數據庫狀態。
3.5:執行事件循環。

第三部分:多機數據庫的實現

第一章:(複製)

1、Redis 2.8以前的複製功能不能高效的處理斷線後的重複制情況,但是Redis 2.8新添加了部分重同步功能可以解決這個問題。
2、部分重同步通過複製偏移量、複製積壓緩衝區、服務器運行ID三個部分來實現。
3、在複製操作剛開始的時候,從服務器會成爲主服務器的客戶端,並通過向主服務器發送命令請求來執行復制步驟,而在複製操作後期,主從服務器會互相成爲對方的客戶端(執行同步時如果PSYNC爲部分重同步,那麼主服務器需要成爲從服務器的客戶端,才能向從服務器發送保存在複製積壓緩衝區裏面的寫命令)。
4、主服務器通過向從服務器傳播命令來更新從服務器的狀態,保持主從服務器一致,而從服務器則通過向主服務器發送命令來進行心跳檢測,以及命令丟失檢測(發送當前偏移量)。

第二章:(Sentinel)

1、Sentinel是運行在特殊模式下的Redis服務器,它使用了和普通模式不同的命令表,所以Sentinel模式能夠執行的命令和普通Redis服務器能夠使用的命令不同。
2、Sentinel會讀入用戶指定的配置文件,爲每個要被監視的主服務器創建相應的實例結構,並創建連向主服務器的命令連接和訂閱連接,其中命令連接用於向主服務器發送命令請求,而訂閱連接用於接收指定頻道消息。
3、Sentinel通過向主服務器發送INFO命令獲取主服務器屬下所有的從服務器信息,併爲這些從服務器創建相應的實例結構,以及連向這些從服務器的命令連接和訂閱連接。
4、在一般情況下,Sentinel以每十秒一次的頻率向被監視的主服務器和從服務器發送INFO命令,當主服務器處於下線狀態,或者Sentinel正在對主服務器進行故障轉移的時候,Sentinel向從服務器發送INFO命令的頻率將會改成每秒一次。
5、對於監視同一主服務器和從服務器的Sentinel來說,它們會以每兩秒一次的頻率,通過向被監視服務器的__Sentinel__:hello頻道發送消息來向其它Sentinel宣告自己的存在。
6、每個Sentinel也會從__Sentinel__:hello頻道中接收其他Sentinel發來的消息,並根據這些信息爲其他Sentinel創建相應的數據結構,以及命令連接。
7、Sentinel只會與主服務器和從服務器創建命令連接和訂閱連接,Sentinel與Sentinel之間則只會創建命令連接(Sentinel之間不會創建訂閱連接,因爲Sentinel需要通過接收主服務器或者從服務器發來的頻道信息來發現未知的新Sentinel,所以才需要建立訂閱連接,而互相已知的Sentinel只需要使用命令連接來進行通信就足夠了)。
8、Sentinel以每秒一次的頻率向實例(包括主服務器、從服務器、其他Sentinel)發送PING命令,並根據實例對PING命令的回覆判斷是否在線,當一個實例在指定時長內連續向Sentinel發送無效回覆時,Sentinel會將這個實例判斷爲主觀下線。
9、當Sentinel將一個主服務器判斷爲主觀下線時,它會向同樣監視這個主服務器的其他Sentinel進行詢問,看它們是否同意這個主服務器進入主觀下線狀態。
10、當Sentinel收集到足夠多的主觀下線投票之後,它會將主服務器判斷爲客觀下線狀態,並對該主服務器進行故障轉移操作。

第三章:(集羣)

1、節點通過握手來將其他節點添加到自己所處的集羣之中。
2、集羣中有16384個槽可以分別指派給集羣中的各個節點,每個節點都會記錄哪些槽指派給了自己,哪些槽指派給了其他節點。
3、節點接收到一個命令請求時,會先檢查這個命令請求要處理的鍵所在的槽是否由自己負責,如果不是的話,節點將向客戶端返回一個MOVED錯誤,MOVED錯誤攜帶的信息可以指引客戶端轉向至正在負責相關槽節點。
4、對Redis集羣的重新分片工作由redis-trib負責執行的,重新分片的關鍵是將屬於某個槽的所有鍵值對從一個節點轉移至另一個節點。
5、如果節點A正在遷移槽i至節點B,那麼當節點A沒能在自己的數據庫中找到命令指定的數據庫鍵值對時,節點A會向客戶端返回一個ASK錯誤,指引客戶端到節點B繼續查找指定的數據庫鍵。
6、MOVED錯誤標識槽的負責權已經從一個節點轉移到另一個節點了,而ASK錯誤只是兩個節點在遷移槽的過程中使用的一種臨時措施。
7、集羣中的從節點用於複製主節點,並在主節點下線時,代替主節點繼續處理命令請求。
8、集羣中的節點通過發送和接受消息來進行通信,常見的消息包括MEET、PING、PONG、PUBLISH、FAIL五種。

第四部分:獨立功能實現

第一章:(發佈與訂閱)

1、服務器狀態在pubsub_channels字典中保存了所有頻道的訂閱關係:SUBSCRIBE命令負責將客戶端和被訂閱的頻道關聯到這個字典裏面,而UNSUBSCRIBE命令則負責解除客戶端和被訂閱頻道之間的關聯。(類似於Java HashMap的結構,數組+鏈表 key是頻道 value是客戶端以鏈表結構存儲)
2、服務器狀態在pubsub_patterns鏈表中保存了所有模式的訂閱關係:PSUBSCRIBE命令負責將客戶端和被訂閱的模式記錄到鏈表中,而PUNSUBSCRIBE命令負責移除客戶端和被退訂模式在鏈表中的記錄。
3、PUBLISH命令通過訪問pubsub_channels字典來向頻道的所有訂閱者發送消息,通過訪問pubsub_patterns鏈表來向所有匹配頻道的模式的訂閱者發送消息。
4、PUBSUB命令的三個子命令(PUBSUB CHANNELS [pattern]返回服務器當前被訂閱的頻道、PUBSUB NUMSUB [channel-1 channel-2…channel-n]接收任意多個頻道作爲輸入參數,並返回這些頻道的訂閱者數量、PUBSUB NUMPAT返回服務器當前被訂閱模式的數量)

第二章:(事務)

1、事務提供了一種將多個命令打包,然後一次性、有序的執行的機制。
2、多個命令會被入隊到事務隊列中,然後按先進先出(FIFO)的順序執行。
3、事務在執行過程中不會被中斷,當事務隊列中所有命令都被執行完畢後,事務纔會結束。
4、帶有WATCH命令的事務會將客戶端和被監視的鍵在數據庫的watched_keys字典中進行關聯(數組加鏈表,key是鍵,value是客戶端信息),當鍵被修改時,程序會將所有監視被修改的客戶端REDIS_DIRTY_CAS標誌打開。
5、只有在REDIS_DIRTY_CAS標誌未打開時,服務器纔會執行客戶端提交的事務,否則的話,服務器將拒絕執行客戶端提交的事務。
6、Redis的事務總是具有ACID中的原子性、一致性和隔離性的,當服務器運行在AOF持久化模式下,並且appendfsync選項的值爲always時,事務也具有耐久性。(不支持錯誤回滾功能,Redis的原則是簡單高效!)

第三章:(Lua腳本)

1、Redis服務器在啓動時,會對內嵌的Lua環境執行一系列的修改操作,從而確保內嵌的Lua環境滿足Redis在功能性、安全性等方面的需求。
2、Redis服務器專門使用一個僞客戶端來執行Lua腳本中包含的Redis命令。
3、Redis使用腳本字典保存所有被EVAL執行過,或者被SCRIPT LOAD命令載入過的Lua腳本,這些腳本可以用於實現SCRIPT EXISTS命令,以及實現腳本複製功能。
4、EVAL命令爲客戶端輸入的腳本在Lua環境中定義一個函數,並通過調用這個函數來執行腳本。
5、EVALSHA命令通過直接調用Lua環境中已定義的函數來執行腳本。
6、SCRIPT FLUSH命令會清空服務器lua_scripts字典中保存的腳本,並重置Lua環境。
7、SCRIPT EXISTS接受一個或多個SHA1校驗和參數,並通過檢查lua_scripts字典來確認校驗和對應的腳本是否存在。
8、SCRIPT LOAD命令接收一個Lua腳本,爲該腳本在Lua環境中創建函數,並將腳本保存到lua_scripts字典中。
9、服務器在執行Lua腳本之前,會爲Lua環境設置一個超時處理的鉤子,當腳本出現超時運行情況時,客戶端可以通過向服務器發送SCRIPT KILL命令來讓鉤子停止正在執行的腳本,或者發送SHUTDOWN、 nosave命令來讓鉤子關閉整個服務器。
10、主服務器複製EVAL、SCRIPT FLUSH、SCRIPT LOAD三個命令的方法和複製普通的Redis命令一樣,只要將相同的命令傳播給從服務器就可以了。
11、主服務器在服務之EVALSHA命令時,必須確保所有從服務器都已經載入EVALSHA命令指定的SHA1校驗和所對應的Lua腳本,如果不能確保這一點的話,主服務器會將EVALSHA命令轉換成等效的EVAL命令,並通過傳播EVAL命令來獲得相同的腳本執行效果。(通過repl_scriptcache_dict字典來實現,如果存在則可以傳播EVALSHA命令,反之亦然,當有新的從節點加入時則會清空該字典。)

第四章:(排序)

1、SORT命令通過將被排序件包含的元素載入到數組裏面,然後對數組進行排序來完成對鍵進行排序工作。
2、在默認情況下,SORT命令假設被排序鍵包含的都是數字值,並且以數字值的方式進行排序。
3、如果SORT命令使用了ALPHA選項,那麼SORT命令假設被排序鍵包含的都是字符串值,並且以字符串的方式來進行排序。
4、SORT的排序操作由快速排序算法實現。
5、SORT命令會支持DESC,升序從小到大,降序從大到小
6、當SORT命令使用了BY選項時,命令使用其他鍵的值作爲權重來進行排序操作。
7、當SORT命令使用了LIMIT選項時,命令只保留排序結果集中LIMIT選項指定的元素。
8、當SORT命令使用了GET選項時,命令會根據排序結果集中的元素,以及GET選項給定的模式,查找並返回其他鍵的值,而不是返回被排序的元素。(先取出指定鍵的值,進行排序,然後根據鍵的值,匹配上GET選項指定的規則,當做鍵去獲取值,然後返回取到的鍵的值返回給客戶端,如SORT students ALPHA GET -name,則會先取出所有的students的鍵值對,然後對值進行快排排序,最終用取出的值匹配-name,然後當做鍵再次去取值,將再次取到的值一一對應的返回給客戶端。)
9、當SORT命令使用了STORE選項時,命令會將排序結果集保存在指定的鍵裏面。(用於不常變化的排序場景SORT students ALPHA STORE sorted_students)
10、當SORT命令同時使用多個選項時,命令限制性排序選項(ALPHA、ASC、DESC、BY),然後執行LIMIT選項,之後執行GET選項,在之後執行STORE選項,最後纔將排序結果返回給客戶端。
11、除了GET選項以外,調整選項的擺放位置不會影響SORT命令的排序結果。

第五章:(二進制位數組)

1、Redis使用SDS來保存數組。
2、SDS使用逆序來保存位數字,這種保存順序簡化了SETBIT命令的實現,是的SETBIT命令可以在不移動現有二進制的情況下,對位數組進行空間擴展。
3、BITCOUNT命令使用了查表算法和variable-precision SWAR算法來優化命令的執行效率。
4、BITOP命令的所有操作都使用C語言內置的位操作來實現。

第六章:(慢查詢日誌)

1、Redis的慢查詢日誌功能用於記錄執行時間超過指定時長的命令。
2、Redis服務器將所有的慢查詢日誌保存在服務器狀態的slowlog鏈表中,每個鏈表節點都包含一個slowlogEntry結構,每個slowlogEntry結構都代表一條慢查詢日誌。
3、打印和刪除慢查詢日誌可以通過遍歷slowlog鏈表來完成。
4、新的慢查詢日誌會被添加在slowlog鏈表的表頭,如果日誌的數量超過slowlog-max-len選項的值,那麼多出來的日誌會被刪除。

第七章:(監視器)

1、客戶端可以通過執行MONITOR命令,將客戶端轉換成監視器,接受並打印服務器處理的每個命令請求的相關信息。
2、當一個客戶端從普通客戶端變成監視器時,該客戶端的REDIS_MONITOR標識會被打開。
3、服務器將所有的監視器都記錄在monitors鏈表中。
4、每次處理命令請求時,服務器都會遍歷monitors鏈表,將相關信息發送給監視器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章