《Redis 設計與實現》的學習

《第一部分:內部數據結構》

內部數據結構:redis 和其他的no-sql[key-value]數據庫相比,redis 不僅支持簡單的字符串鍵值對,它還提供了一系列數據結構類型值:[列表,哈希,集合,有序集,string]。並在這些數據結構類型上定義了一套API.

通過對不同類型的值進行操作,Redis 可以很輕鬆的完成其他只支持字符串鍵值對的key-value 數據庫很難[或者無法]完成的任務。

在redis 內部,數據結構類型值由高效的數據結構和算法進行,並且在redis 自身的構建中,也大量用到了這些數據結構。

1.簡單動態字符串:

SDS : simple dynamic string  ,簡單動態字符串 是redis 底層所使用的字符串表示,它被用在幾乎所有的redis 模塊中。

SDS 在redis 中的主要作用有兩個:
1.實現字符串對象(String Object)
2.在redis 程序內部用作char * 類型的替代品。

redis 是一個鍵值對數據庫,數據庫的值可以是字符串,集合,列表,哈希,有序集合等多種類型的對象,二數據庫的鍵則總是字符串對象。

對於那麼包含字符串值的字符串對象來說,每個字符串對象都包含一個sds 值。

Note:包含字符串值的字符串對象。一個字符串對象除了可以保存字符串值之外,還可以保存long類型的值,所以說:當字符串對象保存的是字符串,它包含的纔是sds值,否則的話,它就是一個long類型的值。

啓動redis-server
啓動 redis-cli

示例:

設置單個值
set person "i love you 577"
get person
設置值是一個集合對象:
sadd nosql "redis" "mongodb" "neo4j"
smembers nosql  

將sds 代替C默認的char * 類型:

因爲char * 類型功能單一,抽象層次低,並且不能高效的支持一些redis 常用的操作[比如追加操作和長度計算操作],所以redis 用sds 代替char*。

重點:在redis 中,客戶端傳入服務器的協議內容,aof 緩存,返回給客戶端的回覆,都是由sds 類型來保存的。

redis 中的字符串:

在C語言中,字符串可以用一個\0 結尾的char 數據來表示。 ["hello world\0"]
這種簡單的字符串表示在大多數情況下都可以滿足要求,但是它並不能高效的支持長度計算和追加append 這兩種操作:
1.每次計算字符串長度 的複雜度爲0(N)。
2.對字符串進行N次追加,必定需要對字符串進行N次內存重新分配。

redis 除了處理C 字符串之外,還需要處理單純的字節數組,以及服務器協議等內容,所以redis 的字符串表示還應該是 二進制安全的 。所以數據可以是以\0結尾的C字符串,也可以是單純的字節數組。

redis 使用sds 類型替換了C語言的默認字符串表示:sds 既可以高效的實現追加和長度計算,並且它還是二進制安全的。

//追加字符串
append person "  do you know"

在目前的版本的redis 中,sds_max_prealloc 的值爲1024*1024 ,當字符串小宇1MB 的字符串執行追加時,sdsMakeRoomFor 就爲它們分配多於所需大小一倍的空間;當字符串大於1MB,那麼sdsMakeRoomFor就爲他們額外多分配1MB空間。

這種分配策略會浪費內存嗎?

執行過append 命令的字符串會帶有額外的預分配空間,這些預分配空間不會被釋放,除非該字符串所對應的鍵被刪除,或者等到redis關閉後,再次啓動時重新載入的字符串對象將不會有預分配空間。因爲執行append 命令的字符串數據通常不會很多,佔用內存的體積通常不打,所以一般不是問題。

SDS 模塊的API :

總結:

redis 的字符串表示爲sds ,而不是C字符串char*。
對比char * ,sds有以下特性:
1.可以高效的執行長度計算
2.可以高效的追加操作
3.二進制安全
4.sds 會爲追加操作進行優化:加快追加操作的速度,並降低內存分配的次數,代價是多佔用一些內存,且內存不會被主動釋放。

2.雙端鏈表

鏈表作爲數組之外的一種常用序列抽象,是大多數高級語言的基本數據類型。redis 則實現了一個雙端鏈表結構。

雙端鏈表的應用:雙端鏈表它既是redis 列表結構的底層實現之一,還被大量的redis 模塊所使用,用於構建其他功能。

redis 列表使用兩種數據結構作爲底層實現:
1.雙端鏈表
2.壓縮列表

因爲雙端鏈表佔用的內存比壓縮列表要多,所以當創建新的列表鍵時,列表會優先考慮使用壓縮列表作爲底層實現,並且在需要的時候,才從壓縮列表實現轉換到雙端鏈表實現。

雙端鏈表被很多redis 內部模塊所應用:
1.事務模塊使用雙端鏈表來按順序保存輸入的命令
2.服務器模塊使用雙端鏈表保存多個客戶端
3.訂閱、發送模塊使用雙端鏈表來保存訂閱模式的多個客戶端
4.時間模塊使用雙端鏈表來保存時間事件

雙端鏈表的實現:由listNode 和 list 兩個數據結構構成。

迭代器:redis 爲雙端鏈表實現了一個迭代器,這個迭代器可以從兩個方向對雙端鏈表進行迭代:
1.沿着節點的next指針前進,從表頭向表尾迭代
2.沿着節點的prev指針前進,從表尾向表頭迭代

總結:redis 實現了自己的雙端鏈表結構。
雙端鏈表主要有兩個作用:
1.作爲redis 列表類型的底層實現之一。
2.作爲通用數據結構,被其他功能模塊所使用。

3.字典

字典,又稱映射或關聯數組。 它是一種抽象數據結構,由一集鍵值對組成,各個鍵值對的鍵各不相同,程序可以將新的鍵值對添加到字典中,或者基於鍵進行查找,更新或者刪除等操作。

字典的應用:
1.實現數據庫鍵空間
2.用作hash 類型鍵的其中一種底層實現。

字典的實現:
1.最簡單的就是使用鏈表或數組,但是這種方式只適用於元素個數不多的情況下
2.要兼顧高效和簡單性,可以使用哈希表
3.如果追求更爲穩定的性能特徵,並且希望高效的實現排序操作的做,則可以使用更爲複雜的平衡樹。

在衆多的可能實現中,redis 選擇了高效且實現簡單的哈希表作爲字典的底層實現。

添加新鍵值對時發生hash碰撞:在哈希表實現中,當兩個不同的鍵擁有相同的哈希值時,我們稱這兩個鍵發生hash碰撞,而哈希表實現必須想辦法對碰撞進行處理。字典哈希表所使用的碰撞解決方法被稱爲鏈地址法:這種方法使用鏈表將多個哈希值相同的節點串連在一起,從而解決衝突問題。

總結:
1.字典由鍵值對構成的抽象數據結構
2.redis 中的數據庫和哈希值都基於字典來實現
3.redis 字典的底層實現爲hash表,每個字典使用兩個哈希表,一般情況下只使用0號哈希表,只有在rehash 進行時,纔會同時使用0和1 哈希表。
4.rehash 可以擴展或收縮哈希表
5.對hash 表的rehash 是分多次,漸進式的進行的。

4.跳躍表

跳躍表是一種隨機化的數據,這種數據結構以有序的方式在層次化的鏈表中保存元素,它的效率可以和平衡樹媲美---查找,刪除,添加等操作都可以在對數期望時間下完成,並且比平衡樹實現要簡單直觀。

跳躍表的作用:實現有序集數據類型。跳躍表將指向有序集的score 值和member 域的指針作爲元素,並以score值爲索引,對有序集元素進行排序。

總結:
1.跳躍表是一種隨機化數據結構,它的查找,添加,刪除操作都可以在對數期望時間下完成。
2.跳躍表唯一的所用就是作爲有序集合類型的底層數據結構。
3.爲了適應自身的需求,redis 對跳躍表進行了修改

《第二部分:內存映射數據結構》

內存映射數據結構是一系列經過特殊編碼的字節序列,創建他們所消耗的內存通常比作用類似的內部數據結構 要少的多,如果使用得當,內存映射數據結構可以爲用戶節省大量的內存。

1.整數集合
  整數集合用於有序,無重複的保存多個整數值,它會根據元素的值,自動選擇該用什麼長度的整數類型來保存元素。
  
  整數集合的應用:
  Intset 是集合鍵的底層實現之一,如果一個集合:
  1.只保存着整數元素
  2.元素的數量不多
  
  3.Intset 只支持升級,不支持降級。
  4.Intset 是有序的,程序使用二分查找算法來實現查找操作,複雜度爲

壓縮列表[ZipList]:是由一系列特殊編碼的內存塊構成的列表,它可以保存字符數組或整數值,他還是哈希鍵,列表鍵,有序集合鍵的底層實現之一。

《第三部分:不同的命令操作不同的對象》

對象處理機制:lpush llen 只能用於列表鍵,而sadd 和 srandmember 只能用於集合鍵等。del 和 ttl,type可以用於任何類型的鍵。

redisObject 是redis 類型系統的核心,數據庫中的每個鍵,值,以及redis本身處理的參數,都表示爲這種數據類型。

當執行一個處理數據類型的命令時,redis 執行以下步驟:
1.根據給定key,在數據庫字典中查找和它相對應的redisObject ,如果沒找到,就返回null。
2.檢查redisObject 的type 屬性和執行命令所需的類型是否相符,如果不相符,返回類型錯誤。
3.根據redisObject的encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構。
4.返回數據結構的操作結果作爲命令的返回值。

總結:
1.redis 使用自己實現的對象機制來實現類型判斷,命令多臺和基於引用計數的垃圾回收。
2.一種redis 類型的鍵可以有多種底層實現
3.redis會分配一些常用的數據對象,並通過共享這些對象來減少內存佔用,和避免頻繁的爲小對象分配內存。

字符串編碼:字符串類型分別使用redis_encoding_int 和 redis_encoding_raw 兩種編碼:
1.redis_encoding_int 使用long 類型來保存long類型值
2.redis_encoding_raw 則使用sdshdr 結構來保存sds,long,double類型的值。

哈希表:redis_hash 是 hset,hlen 等命令的操作對象,它使用redis_encoding_ziplist 和 redis_encoding_ht 兩種編碼方式。

列表:redis_list 是lpush ,lrange 等命令的操作對象,它使用redis_encoding_ziplist 和redis_encoding_linkedlist 兩種方式編碼。

當客戶端被阻塞之後,脫離阻塞狀態三種方式:
1.被動脫離:有其他客戶端爲造成阻塞的鍵推入了新元素
2.主動脫離:到達執行阻塞原語時設定的最大阻塞時間
3.強制脫離:客戶端強制終止和服務器的鏈接,或者服務器停機。

redis_zset 有序集是 zadd,zcount 等命令的操作對象,它使用redis_encoding_ziplist 和 redis_encoding_skiplist 兩種方式編碼。

事務:

redis 通過multi ,discard,exec,watch 四個命令來實現事務功能。

事務提供了一種將多個命令打包,然後一次性,按順序的執行 的機制,並且事務在執行的期間不會主動終端---服務器在執行完事務中的所有命令之後,纔會繼續處理其他客戶端的其他命令。

事務從開始到執行會經歷三個階段:
1.開始事務
2.命令入隊
3.執行事務
 

multi 命令的執行標記着事務的開始。該命令將客戶端的redis_multi選項打開,讓客戶端從非事務狀態切換到事務狀態。

事務狀態下的discard ,multi,watch 命令。

事務的ACID性質:
redis 事務保證了其中的一致性,和隔離性,但並不保證原子性和持久性。

原子性Atomicity: 單個Redis 命令的執行時原子性的,但是redis沒有在事務上增加任何維持原子性的機制,所以redis事務的執行並不是原子性的。

一致性Consistency: redis 的一致性問題可以分爲三部分:入隊錯誤,執行錯誤,redis 進程被終結。

   1.入隊錯誤:在命令入隊的過程中,如果客戶端向服務器發送了錯誤的命令,比如命令的參數數量不對,那麼服務器將向客戶端返回一個錯誤信息,並且將客戶端的事務狀態設爲redis_ditry_exec。當客戶端執行exec 命令時,redis 會拒絕執行狀態爲redis_dirty_exec的事務,並返回失敗信息。
 
   註解:帶有不正確入隊命令的事務不會被執行,也不會影響數據庫的一致性。

  2.執行錯誤:如果命令在事務執行的過程中發生錯誤,比如說:對一個不同類型的key執行了錯誤的操作,那麼redis 只會將錯誤包含在事務的結果中,這不會引起事務中斷或整個失敗,不會影響已執行事務命令的結果,也不會影響後面要執行的事務命令,所以它對事務的一致性也沒有影響。

  3.redis 進程被終結: 如果redis 服務器進程在執行事務的過程中被其他進程終結,或者管理員強制kill,那麼根據redis 所使用持久化的模式,可能會出現:
     1.內存模式:如果redis 沒有采取任何持久化機制,那麼重啓之後的數據庫總是空白的,所以數據總是一致的。
     2.RDB模式:在執行事務時,redis 不會中斷事務去執行保存RDB的工作,只有在事務執行中途被kill ,事務內執行的命令,不管成功了多少,都不會被保存到RDB文件中。恢復數據庫需要使用現有的RDB文件,而這個RDB文件的數據保存的是最近一次的數據庫快照,所以它的數據可能不是最新的,但只要RDB文件本身沒有因爲其他問題而出錯,那麼還原後的數據庫就是一致性的。

    3.AOF模式:因爲保存AOF文件的工作在後臺線程進行,所以即使是在事務執行的中途,保存AOF文件的工作也可以繼續進行,因此事務是否被寫入並保存到AOF文件,會發生兩種情況。

隔離性 isolation: redis 是單進程程序,並且它保證在執行事務時,不會對事務進行中斷,事務可以運行直到執行完所有事務隊列中的命令爲止。

持久性:因爲事務不過是用隊列包裹起了一組redis 命令,並沒有提供任何額外的持久性功能,所以事務的持久性由redis 所使用的持久化模式決定的。

   1.在淡出的內存模式下,事務肯定不持久的。
   2在RDB模式下,服務器可能是在事務執行後,RDB文件更新之前的這段時間失敗,所以RDB模式下的redis 事務也是不持久的。
   3.在AOF的總是SYNC 模式下,事務的每條命令在執行成功之後,都會立即調用fsync 或者fdatasync 將事務數據寫入到AOF文件。但是這種保存是由後臺線程進行的,主線程不會阻塞直到保存成功,所以命令執行成功到數據保存到硬盤之間,還有一段非常小的間隔,所以這種模式下的事務也是不持久的。

Lua 腳本:lua 腳本功能是redis 2.6 版本的功能,通過內嵌對lua 環境的支持,redis 解決了不能高效的處理cas (check-and-set)命令的缺點,並且可以通過組合多個命令,輕鬆實現以前不能或者不能高效實現的模式。

初始化lua 環境:

在初始化redis 服務器時,對lua 環境的初始化也會一併進行。

爲了讓lua 環境符合redis 腳本功能的需求,redis 對lua環境進行了一系列的修改,包括添加函數庫,更換隨機函數,保護全局變量等。

整個初始化lua環境的步驟:
1.調用lua_open 函數,創建一個新的lua 環境。
2.載入指定的lua函數庫,包括:
  1.基礎庫base lib
  2.表格庫 table lib
  3.字符串庫 string lib
  4.數學庫 math lib
  5.調試庫 debug lib
  6.用於處理json 對象的cjson庫
  7.在lua 值和C結構(struct) 之間進行轉換的struct庫
  8.處理messagePack 數據的cmsgpack庫
3.屏蔽一些可能對lua 環境產生安全問題的函數,比如loadfile
4.創建一個redis 字典,保存lua 腳本,並在複製replication 腳本時使用。字典的鍵爲sha1校驗和,字典的值爲lua 腳本。

5.創建一個redis 全局表格到lua 環境,表格中包換 了對各種redis 進行操作的函數,包括:
  1.用於執行redis 命令的redis.call 和redis.pcall 函數
  2.用於發送日誌的redis.log 函數,以及相應的日誌級別level

6.用redis 自己定義的隨機生成函數,替換math表原有的math.random函數和math.randomseed函數,新的函數具有:每次執行lua 腳本時,除非顯式的調用math.randomseed,否則math.random 生成的僞隨機數序列總是相同的。

7.創建一個隊redis 多批量回復進行排序的輔助函數。

8.對lua環境的全局變量進行保護,以免被傳入的腳本修改。
 

redis 分別提供了RDB和AOF 兩種持久化機制:

1.RDB

RDB 將數據庫的快照以二進制的方式保存到磁盤中。
2.AOF AOF 則以協議文本的方式,將所有對數據庫進行過寫入的命令及其參數記錄到AOF 文件,以此達到記錄數據庫狀態的目的。

同步命令到AOF 文件的整個過程可以分爲三個階段:
1.命令傳播:redis 將執行完的命令,命令的參數,命令參數個數等信息發送到AOF程序中。
2.緩存追加:AOF 程序根據接收到的命令數據,將命令轉換爲網絡通訊協議的格式,然後將協議內容追加到服務器AOF緩存中。
3.文件寫入和保存:AOF 緩存中的內容被寫入到AOF文件末尾,如果設定的AOF保存條件被滿足的話,fsync 函數或者fdatasync 函數會被調用,將寫入的內容真正的保存到磁盤中。

命令傳播:當一個redis 客戶端需要執行命令時,它通過網絡連接,將協議文本發送給redis 服務器。

緩存追加:

  當命令被傳播到AOF程序之後,程序會根據命令以及命令的參數,將命令從字符串對象轉換會原來的協議文本。

緩存追加可以分爲三步
  1.接受命令,命令的參數,以及參數的個數,所使用的數據庫等信息。
 2.將命令還原成redis 網絡通訊協議
 3.將協議文本追加到aof_buf末尾。
AOF 保存模式:redis 目前支持三種AOF 保存模式,他們分別是
 1.AOF_FSYNC_NO:不保存
2.AOF_FSYNC_EVERYSEC: 每一秒鐘保存一次
3.AOF_FSYNC_ALWAYS: 每執行一個命令保存一次

不保存:

在這種模式下,每次調用flushAppendOnlyFile 函數,write 都會被執行,但 save 會被忽略掉。

在這種模式下,save 只會在以下任意一種情況中被執行。
1.redis 被關閉
2.AOF 功能被關閉
3.系統的寫緩存被刷新(緩存已經被寫滿,或者定期保存操作被執行)。

總結:這三種情況下的save 操作都會引起redis 主進程阻塞。

每一秒鐘保存一次

在這種情況下,save原則上每隔一秒鐘都會執行一次,因爲save 操作是由後臺子線程調用的,所以不會引起服務器主線程阻塞。[注意:實際和原則略有不同,程序在這種模式下對fsync 或者fdatasync 的調用並不是每秒執行一次,它和調用flushApeendOnlyFile 函數時redis 所處的狀態有關]。

每當flushAppendOnlyFile函數被調用時,可能會出現以下四種情況:

1.子線程正在執行save ,並且:

   1.這個save的執行時間未超過2秒,那麼程序直接返回,並不執行write 或者新的save。
   2.這個save的執行時間已超過2秒,那麼程序執行write,但不執行新的save。因爲write
     的寫入必須等待子線程先完成save,因此這裏write 會比平時阻塞時間更長。

2.子線程沒有在執行save ,並且:
   3.上次執行成功save距今不超過1秒,那麼程序執行write,但不執行save。
   4.上次執行成功save距今已超過1秒,那麼程序執行write 和save 。

總結:如果程序宕機,那麼也只會丟失2秒的數據而已。

每執行一個命令保存一次

在這種模式下,每次執行完一個命令之後,write和save都會被執行。

總結:save 是由redis主進程執行的,所以在save 執行期間,主進程會被阻塞,不能接受命令請求。

AOF 保存模式對性能和安全性的影響

性能:

1.不保存:寫入和保存都由主進程執行,兩個操作都會阻塞主進程。
2.每秒鐘保存一次:寫入由主進程執行,阻塞主進程。保存由子線程執行,不直接阻塞主進程,但保存操作完成的快慢會影響寫入操作的阻塞時長。
3.每執行一個命令保存一次:寫入和保存都由主進程執行,兩個操作都會阻塞主進程。

總結:因爲阻塞操作會讓redis 主進程無法持續處理請求,所以,阻塞操作執行的越少,完成的越快,redis 的性能就越好。

安全:

1.不保存的操作只會在AOF關閉或redis 關閉時執行,或者由操作系統觸發,一般情況下,只需要寫入阻塞,所以它的寫入性能要比後面兩種模式要高。但一旦宕機,那麼丟失數據的數量由操作系統的緩存沖洗策略決定。

2.每秒一次,性能上高於3,並且最多丟失2秒的數據,安全性高於1。

3.每執行一個命令保存一次。 安全性最高,性能最差,因爲服務器必須阻塞直到命令信息被寫入並保存到磁盤後,才能繼續處理請求。

AOF 文件的讀取和數據還原:

AOF 文件保存了redis 的數據庫狀態,而文件裏面包含的都是符合redis 通信協議格式的命令文本。即只要根據AOF文件裏的協議,重新執行一遍裏面指示的所有指令,就可以還原redis 的數據庫狀態了。

redis 讀取AOF 文件並還原數據庫的詳細步驟如下:

1.創建一個不帶網絡連接的僞客戶端(fake client)
2.讀取AOF 所保存的文本,並根據內容還原出命令,命令的參數以及命令的個數。
3.根據命令,命令的參數和命令的個數,使用僞客戶端執行該命令。
4.執行2和3,直到AOF 文件中的所有命令執行完畢。

結果:[完成第四步之後,AOF 文件所保存的數據庫就會被完整的還原出來]。

注意:因爲redis 的命令只能在客戶端的上下文中被執行,而AOF 還原時所使用的命令來自於AOF文件,而不是網絡,所以程序使用了一個沒有網絡連接的僞客戶端來執行命令。僞客戶端執行命令的結果,和帶網絡連接的客戶端執行命令的效果完全一樣。
 

重點:爲了避免對數據的完整性產生影響,在服務器載入數據的過程中,只有和數據庫無關的訂閱和發佈功能可以正常使用,其他命令一律返回錯誤。

AOF 重寫:AOF 文件通過同步redis 服務器所執行的命令,從而實現了數據庫狀態的記錄,但是這種同步方式會造成一個問題:隨着運行時間的流失,AOF文件會變得越來越大。爲了解決這一問題,redis 需要對AOF文件進行重寫:創建一個新的AOF 文件來代替原有的AOF文件,新AOF文件和原有的AOF文件保持的數據庫狀態完全一致,但新的AOF 文件的體積小宇等於原有AOF文件的體積。

AOF後臺重寫:redis 將AOF 重寫程序放到後臺子進程裏執行,好處:

1.子進程進行AOF重寫期間,主進程可以繼續處理命令請求。
2.子進程帶有主進程的數據副本,使用子進程而不是線程,可以在避免鎖的情況下,保證數據的安全性。

好處:

1.子進程進行AOF 重寫期間,主進程可以繼續處理命令請求。
2.子進程帶有主進程的數據副本,使用子進程而不是線程,可以在避免鎖的情況下,保證數據的安全性。

問題:使用子進程有一個問題需要解決:因爲子進程在進行AOF重寫期間,主進程還需要繼續處理命令,而新的命令可能對現有的數據進行修改,這會讓當前數據庫的數據和重寫後的AOF 文件的數據不一致。

解決方案: redis 增加了一個AOF 重寫緩存,這個緩存在fork 出子進程之後開始啓用,redis 主進程在接到新的寫命令之後,除了會將這個寫命令的協議內容追加到現有的AOF文件之外,還會追加到這個緩存中。


當子進程在執行AOF 重寫時,主進程需要執行以下三個工作:

1.處理命令請求
2.將寫命令追加到現有的AOF文件中
3.將寫命令追加到AOF重寫緩存中

實現了:

1.現有的AOF 功能會繼續執行,即使在AOF 重寫期間發生停機,也不會有任何數據丟失。

2.所有對數據庫進行修改的

總結:
1.AOF文件通過保存所有修改數據庫的命令來記錄數據庫的狀態。
2.AOF文件中的所有命令都以redis 通信協議的格式保存。
3.不同的AOF保存模式對數據的安全性,以及redis 的性能有很大的影響。
4.AOF重寫的目的是用更小的體積來保存數據庫狀態,整個重新過程基本上不影響redis主進程處理命令請求。
5.AOF 重寫是一個有歧義的名字,實際的重寫工作是針對數據庫的當前值來進行的,程序既不讀寫,也不使用原有的AOF文件。
6.AOF可以由用戶手動觸發,也可以由服務器自動觸發。

事件:
   事件是redis 服務器的核心,它處理兩項重要的任務:
   1.處理文件事件:在多個客戶端中實現多路複用,接受它們發來的命令請求,並將命令的執行結果返回給客戶端。
   2.時間事件:實現服務器常規操作(server cron job)。

文件事件:
   redis 服務器通過多個客戶端至今進行多路複用,從而實現高效的命令請求處理:多個客戶端通過套接字連接到redis 服務器中,但只有在套接字可以無阻塞的進行讀或者寫時,服務器纔會和這些客戶端進行交互。redis 將這類因爲對套接字進行多路複用而產生的事件成爲文件事件,文件事件可以分爲讀事件和寫事件兩類。

讀事件:讀事件標誌着客戶端命令請求的發送狀態。當一個新的客戶端連接到服務器時,服務器會爲該客戶端綁定讀事件,直到客戶端斷開連接之後,這個讀事件纔會被移除。
 

讀事件在整個網絡連接的生命週期內,都會在等待和就緒兩種狀態之間切換:

1.當客戶端只是連接到服務器,並沒有向服務器發送命令時,該客戶端的讀事件就處於等待狀態。
2.當客戶端給 服務器發送命令請求,並且請求已到達時,該客戶端的讀事件處於就緒狀態。

寫事件: 寫事件標誌着可達護短對命令結果的接收狀態。和客戶端自始至終都關聯着讀事件不同,服務器只會在有命令結果要傳回給客戶端時,纔會爲客戶端關聯寫事件,並且在命令結果傳送完畢之後,客戶端和寫事件的關聯就會被移除。

一個寫事件會在兩種狀態之間切換:
1.當服務器有命令結果需要返回給客戶端,但客戶端還未能執行無阻塞寫,那麼事件處於等待狀態。
2.
當服務器有命令結果需要返回給客戶端,並且客戶端可以進行無阻塞寫,那麼事件處於就緒狀態。

寫事件:寫事件標誌着客戶端對命令結果的接收狀態。

和客戶端自始至終都關聯着讀事件不同,服務器只在有命令結果要傳回客戶端時,纔會爲客戶端關聯寫事件,並且在命令結果傳送完畢後,客戶端和寫事件的關聯就會被移除。

一個寫事件會在兩種狀態之間切換:
1.當服務器有命令結果需要返回給客戶端,但客戶端還未能執行無阻塞寫,那麼寫事件處於等待狀態。
2.當服務器有命令結果需要返回給客戶端,並且客戶端可以進行無阻塞寫,那麼寫事件處於就緒狀態。

當客戶端向服務器發送命令請求,並且請求被接受並執行之後,服務器就需要將保存在緩存內的命令執行結果返回給客戶端,這時服務器就會爲客戶端關聯寫事件。


重要:在同一次文件事件處理器的調用中,單個客戶端只能執行其中一種事件(要麼讀,要麼寫,但不能同時執行),當出現讀寫同時就緒的情況時,事件處理器優先處理讀事件。

時間事件:時間事件記錄着那些要在指定時間點運行的事件,多個時間事件以無序鏈表的形式保存在服務器狀態中。

每個時間事件主要由三個屬性組成:
1.when:以毫秒格式的UNIX 時間戳爲單位,記錄了應該在什麼時間點執行事件處理函數。

2.timeProc: 事件處理函數

3.next: 指向下一個時間事件,形成鏈表。

時間事件應用實例:服務器常規操作。對於持續運行的服務器來說,服務器需要定期對自身資源和狀態進行必要的檢查和整理,從而讓服務器維持在一個健康穩定的狀態,這類操作被統稱爲常規操作(cron job)。

在redis 中,常規操作由redis/serverCron 實現,它主要執行以下操作:

1.更新服務器的各類統計信息,比如時間,內存佔用,數據庫佔用情況等。
2.清理數據庫中的過期鍵值對。
3.對不合理的數據庫進行大小調整。
4.關閉和清理連接失效的客戶端。
5.嘗試進行AOF或RDB持久化操作。
6.如果服務器是主節點的話,對附屬節點進行定期同步。
7.如果處於集羣模式的話,對集羣進行定期同步和連接測試。

redis 將serverCron 作爲時間事件來運行,從而確保它每隔一段時間就會自動運行一次,又因爲serverCron需要在redis 服務器運行期間一直定期運行,所以它是一個循環時間事件:serverCron 會一直定期執行,直到服務器關閉爲止。

redis2.6版本中,程序規定serverCron 每隔10毫秒就會被運行一次。從redis 2.8開始,10毫秒是serverCron 運行的默認間隔,而具體的間隔可以由用戶自己調整。

事件的執行與調度:文件事件和時間事件呈合作關係,它們之間包含以下三種屬性:

1.一種事件等待另一種事件執行完畢之後,纔開始執行,事件之間不會出現搶佔。
2.事件處理器先處理文件事件,再執行時間事件(調用serverCron)
3.文件事件的等待事件,由距離到達時間最短的時間事件決定。

總結:

1.redis 的事件分爲時間事件和文件事件兩類。
2.文件事件分爲讀事件和寫事件兩類:讀事件實現了命令請求的接收,寫事件實現了命令結果的返回。
3.時間事件分爲單次執行事件和循環執行事件,服務器常規操作serverCron 就是循環事件。
4.文件事件和時間事件之間是合作關係:一種事件會等待另一種事件完成之後再執行,不會出現搶佔情況。
5.時間事件的實際執行時間通常會比預定時間晚一些。

服務器與客戶端:

初始化服務器:從啓動redis 服務器,到服務器可以接收外來客戶端的網絡的網絡連接的這段時間,redis 需要執行一系列初始化操作。

整個初始化過程可以分爲以下六個步驟:
1.初始化服務器全局狀態
2.載入配置文件
3.創建daemon 進程
4.初始化服務器功能模塊
5.開始事件循環

初始化服務器全局狀態:

1.服務器中的所有數據庫
2.命令表:在執行命令時,根據字符來查找相應命令的實現函數
3.事件狀態
4.服務器的網絡連接信息:套接字地址,端口,以及套接字描述符
5.所有已連接客戶端的信息
6.Lua 腳本的運行環境及相關選項
7.實現訂閱和發佈功能所需的數據結構
8.日誌log 和查詢日誌slowlog 的選項和相關信息
9.數據持久化aof 和rdb 的配置和狀態
10.服務器配置選項:比如要創建多少個數據庫,是否將服務器進程作爲daemon 進程來運行,最大連接多少個客戶端,壓縮結構zip structure 的實體數量等。
11.統計信息:比如鍵有多少次命令,不命中,服務器的運行時間,內存佔用等。


 

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