国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇)

這篇具有很好參考價(jià)值的文章主要介紹了中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請(qǐng)大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問(wèn)。

前言

  1. 學(xué)習(xí)視頻: 黑馬程序員Redis入門到實(shí)戰(zhàn)教程,深度透析redis底層原理+redis分布式鎖+企業(yè)解決方案+黑馬點(diǎn)評(píng)實(shí)戰(zhàn)項(xiàng)目

  2. 中間件系列 - Redis入門到實(shí)戰(zhàn)

  3. 本內(nèi)容僅用于個(gè)人學(xué)習(xí)筆記,如有侵?jǐn)_,聯(lián)系刪除

  4. 學(xué)習(xí)目標(biāo)

    • Redis數(shù)據(jù)結(jié)構(gòu)
    • Redis網(wǎng)絡(luò)模型
    • Redis通信協(xié)議-RESP協(xié)議

1 Redis數(shù)據(jù)結(jié)構(gòu)

1.1 動(dòng)態(tài)字符串

我們都知道Redis中保存的Key是字符串,value往往是字符串或者字符串的集合??梢娮址荝edis中最常用的一種數(shù)據(jù)結(jié)構(gòu)。

不過(guò)Redis沒有直接使用C語(yǔ)言中的字符串,因?yàn)镃語(yǔ)言字符串存在很多問(wèn)題:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

  • 獲取字符串長(zhǎng)度的需要通過(guò)運(yùn)算
  • 非二進(jìn)制安全
  • 不可修改

Redis構(gòu)建了一種新的字符串結(jié)構(gòu),稱為簡(jiǎn)單動(dòng)態(tài)字符串(Simple Dynamic String),簡(jiǎn)稱SDS。

例如,我們執(zhí)行命令:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
那么Redis將在底層創(chuàng)建兩個(gè)SDS,其中一個(gè)是包含“name”的SDS,另一個(gè)是包含“虎哥”的SDS。

Redis是C語(yǔ)言實(shí)現(xiàn)的,其中SDS是一個(gè)結(jié)構(gòu)體,源碼如下:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
例如,一個(gè)包含字符串“name”的sds結(jié)構(gòu)如下:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

SDS之所以叫做動(dòng)態(tài)字符串,是因?yàn)樗邆鋭?dòng)態(tài)擴(kuò)容的能力,例如一個(gè)內(nèi)容為“hi”的SDS:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

假如我們要給SDS追加一段字符串“,Amy”,這里首先會(huì)申請(qǐng)新內(nèi)存空間:

如果新字符串小于1M,則新空間為擴(kuò)展后字符串長(zhǎng)度的兩倍+1;

如果新字符串大于1M,則新空間為擴(kuò)展后字符串長(zhǎng)度+1M+1。稱為內(nèi)存預(yù)分配
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

1.2 intset

IntSet是Redis中set集合的一種實(shí)現(xiàn)方式,基于整數(shù)數(shù)組來(lái)實(shí)現(xiàn),并且具備長(zhǎng)度可變、有序等特征。
結(jié)構(gòu)如下:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

其中的encoding包含三種模式,表示存儲(chǔ)的整數(shù)大小不同:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

現(xiàn)在,數(shù)組中每個(gè)數(shù)字都在int16_t的范圍內(nèi),因此采用的編碼方式是INTSET_ENC_INT16,每部分占用的字節(jié)大小為:
encoding:4字節(jié)
length:4字節(jié)
contents:2字節(jié) * 3 = 6字節(jié)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

我們向該其中添加一個(gè)數(shù)字:50000,這個(gè)數(shù)字超出了int16_t的范圍,intset會(huì)自動(dòng)升級(jí)編碼方式到合適的大小。
以當(dāng)前案例來(lái)說(shuō)流程如下:

  • 升級(jí)編碼為INTSET_ENC_INT32, 每個(gè)整數(shù)占4字節(jié),并按照新的編碼方式及元素個(gè)數(shù)擴(kuò)容數(shù)組
  • 倒序依次將數(shù)組中的元素拷貝到擴(kuò)容后的正確位置
  • 將待添加的元素放入數(shù)組末尾
  • 最后,將inset的encoding屬性改為INTSET_ENC_INT32,將length屬性改為4
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

源碼如下:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

小總結(jié):

Intset可以看做是特殊的整數(shù)數(shù)組,具備一些特點(diǎn):

  • Redis會(huì)確保Intset中的元素唯一、有序
  • 具備類型升級(jí)機(jī)制,可以節(jié)省內(nèi)存空間
  • 底層采用二分查找方式來(lái)查詢

1.3 Dict

我們知道Redis是一個(gè)鍵值型(Key-Value Pair)的數(shù)據(jù)庫(kù),我們可以根據(jù)鍵實(shí)現(xiàn)快速的增刪改查。而鍵與值的映射關(guān)系正是通過(guò)Dict來(lái)實(shí)現(xiàn)的。

Dict由三部分組成,分別是:哈希表(DictHashTable)、哈希節(jié)點(diǎn)(DictEntry)、字典(Dict)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

當(dāng)我們向Dict添加鍵值對(duì)時(shí),Redis首先根據(jù)key計(jì)算出hash值(h),然后利用 h & sizemask來(lái)計(jì)算元素應(yīng)該存儲(chǔ)到數(shù)組中的哪個(gè)索引位置。我們存儲(chǔ)k1=v1,假設(shè)k1的哈希值h =1,則1&3 =1,因此k1=v1要存儲(chǔ)到數(shù)組角標(biāo)1位置。

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

Dict由三部分組成,分別是:哈希表(DictHashTable)、哈希節(jié)點(diǎn)(DictEntry)、字典(Dict)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

Dict的擴(kuò)容

Dict中的HashTable就是數(shù)組結(jié)合單向鏈表的實(shí)現(xiàn),當(dāng)集合中元素較多時(shí),必然導(dǎo)致哈希沖突增多,鏈表過(guò)長(zhǎng),則查詢效率會(huì)大大降低。
Dict在每次新增鍵值對(duì)時(shí)都會(huì)檢查負(fù)載因子(LoadFactor = used/size) ,滿足以下兩種情況時(shí)會(huì)觸發(fā)哈希表擴(kuò)容:
哈希表的 LoadFactor >= 1,并且服務(wù)器沒有執(zhí)行 BGSAVE 或者 BGREWRITEAOF 等后臺(tái)進(jìn)程;
哈希表的 LoadFactor > 5 ;

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

Dict的rehash

不管是擴(kuò)容還是收縮,必定會(huì)創(chuàng)建新的哈希表,導(dǎo)致哈希表的size和sizemask變化,而key的查詢與sizemask有關(guān)。因此必須對(duì)哈希表中的每一個(gè)key重新計(jì)算索引,插入新的哈希表,這個(gè)過(guò)程稱為rehash。過(guò)程是這樣的:

  • 計(jì)算新hash表的realeSize,值取決于當(dāng)前要做的是擴(kuò)容還是收縮:

    • 如果是擴(kuò)容,則新size為第一個(gè)大于等于dict.ht[0].used + 1的2^n
    • 如果是收縮,則新size為第一個(gè)大于等于dict.ht[0].used的2^n (不得小于4)
  • 按照新的realeSize申請(qǐng)內(nèi)存空間,創(chuàng)建dictht,并賦值給dict.ht[1]

  • 設(shè)置dict.rehashidx = 0,標(biāo)示開始rehash

  • 將dict.ht[0]中的每一個(gè)dictEntry都rehash到dict.ht[1]

  • 將dict.ht[1]賦值給dict.ht[0],給dict.ht[1]初始化為空哈希表,釋放原來(lái)的dict.ht[0]的內(nèi)存

  • 將rehashidx賦值為-1,代表rehash結(jié)束

  • 在rehash過(guò)程中,新增操作,則直接寫入ht[1],查詢、修改和刪除則會(huì)在dict.ht[0]和dict.ht[1]依次查找并執(zhí)行。這樣可以確保ht[0]的數(shù)據(jù)只減不增,隨著rehash最終為空

整個(gè)過(guò)程可以描述成:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
小總結(jié):

Dict的結(jié)構(gòu):

  • 類似java的HashTable,底層是數(shù)組加鏈表來(lái)解決哈希沖突
  • Dict包含兩個(gè)哈希表,ht[0]平常用,ht[1]用來(lái)rehash

Dict的伸縮:

  • 當(dāng)LoadFactor大于5或者LoadFactor大于1并且沒有子進(jìn)程任務(wù)時(shí),Dict擴(kuò)容
  • 當(dāng)LoadFactor小于0.1時(shí),Dict收縮
  • 擴(kuò)容大小為第一個(gè)大于等于used + 1的2^n
  • 收縮大小為第一個(gè)大于等于used 的2^n
  • Dict采用漸進(jìn)式rehash,每次訪問(wèn)Dict時(shí)執(zhí)行一次rehash
  • rehash時(shí)ht[0]只減不增,新增操作只在ht[1]執(zhí)行,其它操作在兩個(gè)哈希表

1.4 ZipList

ZipList 是一種特殊的“雙端鏈表” ,由一系列特殊編碼的連續(xù)內(nèi)存塊組成??梢栽谌我庖欢诉M(jìn)行壓入/彈出操作, 并且該操作的時(shí)間復(fù)雜度為 O(1)。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

屬性 類型 長(zhǎng)度 用途
zlbytes uint32_t 4 字節(jié) 記錄整個(gè)壓縮列表占用的內(nèi)存字節(jié)數(shù)
zltail uint32_t 4 字節(jié) 記錄壓縮列表表尾節(jié)點(diǎn)距離壓縮列表的起始地址有多少字節(jié),通過(guò)這個(gè)偏移量,可以確定表尾節(jié)點(diǎn)的地址。
zllen uint16_t 2 字節(jié) 記錄了壓縮列表包含的節(jié)點(diǎn)數(shù)量。 最大值為UINT16_MAX (65534),如果超過(guò)這個(gè)值,此處會(huì)記錄為65535,但節(jié)點(diǎn)的真實(shí)數(shù)量需要遍歷整個(gè)壓縮列表才能計(jì)算得出。
entry 列表節(jié)點(diǎn) 不定 壓縮列表包含的各個(gè)節(jié)點(diǎn),節(jié)點(diǎn)的長(zhǎng)度由節(jié)點(diǎn)保存的內(nèi)容決定。
zlend uint8_t 1 字節(jié) 特殊值 0xFF (十進(jìn)制 255 ),用于標(biāo)記壓縮列表的末端。

ZipListEntry

ZipList 中的Entry并不像普通鏈表那樣記錄前后節(jié)點(diǎn)的指針,因?yàn)橛涗泝蓚€(gè)指針要占用16個(gè)字節(jié),浪費(fèi)內(nèi)存。而是采用了下面的結(jié)構(gòu):
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

  • previous_entry_length:前一節(jié)點(diǎn)的長(zhǎng)度,占1個(gè)或5個(gè)字節(jié)。

    • 如果前一節(jié)點(diǎn)的長(zhǎng)度小于254字節(jié),則采用1個(gè)字節(jié)來(lái)保存這個(gè)長(zhǎng)度值
    • 如果前一節(jié)點(diǎn)的長(zhǎng)度大于254字節(jié),則采用5個(gè)字節(jié)來(lái)保存這個(gè)長(zhǎng)度值,第一個(gè)字節(jié)為0xfe,后四個(gè)字節(jié)才是真實(shí)長(zhǎng)度數(shù)據(jù)
  • encoding:編碼屬性,記錄content的數(shù)據(jù)類型(字符串還是整數(shù))以及長(zhǎng)度,占用1個(gè)、2個(gè)或5個(gè)字節(jié)

  • contents:負(fù)責(zé)保存節(jié)點(diǎn)的數(shù)據(jù),可以是字符串或整數(shù)

ZipList中所有存儲(chǔ)長(zhǎng)度的數(shù)值均采用小端字節(jié)序,即低位字節(jié)在前,高位字節(jié)在后。例如:數(shù)值0x1234,采用小端字節(jié)序后實(shí)際存儲(chǔ)值為:0x3412

Encoding編碼

ZipListEntry中的encoding編碼分為字符串和整數(shù)兩種:
字符串:如果encoding是以“00”、“01”或者“10”開頭,則證明content是字符串

編碼 編碼長(zhǎng)度 字符串大小
|00pppppp| 1 bytes <= 63 bytes
|01pppppp|qqqqqqqq| 2 bytes <= 16383 bytes
|10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| 5 bytes <= 4294967295 bytes

例如,我們要保存字符串:“ab”和 “bc”
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

ZipListEntry中的encoding編碼分為字符串和整數(shù)兩種:

  • 整數(shù):如果encoding是以“11”開始,則證明content是整數(shù),且encoding固定只占用1個(gè)字節(jié)
編碼 編碼長(zhǎng)度 整數(shù)類型
11000000 1 int16_t(2 bytes)
11010000 1 int32_t(4 bytes)
11100000 1 int64_t(8 bytes)
11110000 1 24位有符整數(shù)(3 bytes)
11111110 1 8位有符整數(shù)(1 bytes)
1111xxxx 1 直接在xxxx位置保存數(shù)值,范圍從0001~1101,減1后結(jié)果為實(shí)際值

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

1.5 ZipList的連鎖更新問(wèn)題

ZipList的每個(gè)Entry都包含previous_entry_length來(lái)記錄上一個(gè)節(jié)點(diǎn)的大小,長(zhǎng)度是1個(gè)或5個(gè)字節(jié):
如果前一節(jié)點(diǎn)的長(zhǎng)度小于254字節(jié),則采用1個(gè)字節(jié)來(lái)保存這個(gè)長(zhǎng)度值
如果前一節(jié)點(diǎn)的長(zhǎng)度大于等于254字節(jié),則采用5個(gè)字節(jié)來(lái)保存這個(gè)長(zhǎng)度值,第一個(gè)字節(jié)為0xfe,后四個(gè)字節(jié)才是真實(shí)長(zhǎng)度數(shù)據(jù)
現(xiàn)在,假設(shè)我們有N個(gè)連續(xù)的、長(zhǎng)度為250~253字節(jié)之間的entry,因此entry的previous_entry_length屬性用1個(gè)字節(jié)即可表示,如圖所示:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

ZipList這種特殊情況下產(chǎn)生的連續(xù)多次空間擴(kuò)展操作稱之為連鎖更新(Cascade Update)。新增、刪除都可能導(dǎo)致連鎖更新的發(fā)生。

小總結(jié):

ZipList特性:

  • 壓縮列表的可以看做一種連續(xù)內(nèi)存空間的"雙向鏈表"
  • 列表的節(jié)點(diǎn)之間不是通過(guò)指針連接,而是記錄上一節(jié)點(diǎn)和本節(jié)點(diǎn)長(zhǎng)度來(lái)尋址,內(nèi)存占用較低
  • 如果列表數(shù)據(jù)過(guò)多,導(dǎo)致鏈表過(guò)長(zhǎng),可能影響查詢性能
  • 增或刪較大數(shù)據(jù)時(shí)有可能發(fā)生連續(xù)更新問(wèn)題

1.6 QuickList

問(wèn)題1:ZipList雖然節(jié)省內(nèi)存,但申請(qǐng)內(nèi)存必須是連續(xù)空間,如果內(nèi)存占用較多,申請(qǐng)內(nèi)存效率很低。怎么辦?

? 答:為了緩解這個(gè)問(wèn)題,我們必須限制ZipList的長(zhǎng)度和entry大小。

問(wèn)題2:但是我們要存儲(chǔ)大量數(shù)據(jù),超出了ZipList最佳的上限該怎么辦?

? 答:我們可以創(chuàng)建多個(gè)ZipList來(lái)分片存儲(chǔ)數(shù)據(jù)。

問(wèn)題3:數(shù)據(jù)拆分后比較分散,不方便管理和查找,這多個(gè)ZipList如何建立聯(lián)系?

? 答:Redis在3.2版本引入了新的數(shù)據(jù)結(jié)構(gòu)QuickList,它是一個(gè)雙端鏈表,只不過(guò)鏈表中的每個(gè)節(jié)點(diǎn)都是一個(gè)ZipList。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

為了避免QuickList中的每個(gè)ZipList中entry過(guò)多,Redis提供了一個(gè)配置項(xiàng):list-max-ziplist-size來(lái)限制。
如果值為正,則代表ZipList的允許的entry個(gè)數(shù)的最大值
如果值為負(fù),則代表ZipList的最大內(nèi)存大小,分5種情況:

  • -1:每個(gè)ZipList的內(nèi)存占用不能超過(guò)4kb
  • -2:每個(gè)ZipList的內(nèi)存占用不能超過(guò)8kb
  • -3:每個(gè)ZipList的內(nèi)存占用不能超過(guò)16kb
  • -4:每個(gè)ZipList的內(nèi)存占用不能超過(guò)32kb
  • -5:每個(gè)ZipList的內(nèi)存占用不能超過(guò)64kb

其默認(rèn)值為 -2:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

以下是QuickList的和QuickListNode的結(jié)構(gòu)源碼:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

我們接下來(lái)用一段流程圖來(lái)描述當(dāng)前的這個(gè)結(jié)構(gòu)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

總結(jié):

QuickList的特點(diǎn):

  • 是一個(gè)節(jié)點(diǎn)為ZipList的雙端鏈表
  • 節(jié)點(diǎn)采用ZipList,解決了傳統(tǒng)鏈表的內(nèi)存占用問(wèn)題
  • 控制了ZipList大小,解決連續(xù)內(nèi)存空間申請(qǐng)效率問(wèn)題
  • 中間節(jié)點(diǎn)可以壓縮,進(jìn)一步節(jié)省了內(nèi)存

1.7 SkipList

SkipList(跳表)首先是鏈表,但與傳統(tǒng)鏈表相比有幾點(diǎn)差異:
元素按照升序排列存儲(chǔ)
節(jié)點(diǎn)可能包含多個(gè)指針,指針跨度不同。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

SkipList(跳表)首先是鏈表,但與傳統(tǒng)鏈表相比有幾點(diǎn)差異:
元素按照升序排列存儲(chǔ)
節(jié)點(diǎn)可能包含多個(gè)指針,指針跨度不同。

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
SkipList(跳表)首先是鏈表,但與傳統(tǒng)鏈表相比有幾點(diǎn)差異:
元素按照升序排列存儲(chǔ)
節(jié)點(diǎn)可能包含多個(gè)指針,指針跨度不同。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
小總結(jié):

SkipList的特點(diǎn):

  • 跳躍表是一個(gè)雙向鏈表,每個(gè)節(jié)點(diǎn)都包含score和ele值
  • 節(jié)點(diǎn)按照score值排序,score值一樣則按照ele字典排序
  • 每個(gè)節(jié)點(diǎn)都可以包含多層指針,層數(shù)是1到32之間的隨機(jī)數(shù)
  • 不同層指針到下一個(gè)節(jié)點(diǎn)的跨度不同,層級(jí)越高,跨度越大
  • 增刪改查效率與紅黑樹基本一致,實(shí)現(xiàn)卻更簡(jiǎn)單

1.8 RedisObject

Redis中的任意數(shù)據(jù)類型的鍵和值都會(huì)被封裝為一個(gè)RedisObject,也叫做Redis對(duì)象,源碼如下:

1、什么是redisObject:
從Redis的使用者的角度來(lái)看,?個(gè)Redis節(jié)點(diǎn)包含多個(gè)database(非cluster模式下默認(rèn)是16個(gè),cluster模式下只能是1個(gè)),而一個(gè)database維護(hù)了從key space到object space的映射關(guān)系。這個(gè)映射關(guān)系的key是string類型,?value可以是多種數(shù)據(jù)類型,比如:
string, list, hash、set、sorted set等。我們可以看到,key的類型固定是string,而value可能的類型是多個(gè)。
?從Redis內(nèi)部實(shí)現(xiàn)的?度來(lái)看,database內(nèi)的這個(gè)映射關(guān)系是用?個(gè)dict來(lái)維護(hù)的。dict的key固定用?種數(shù)據(jù)結(jié)構(gòu)來(lái)表達(dá)就夠了,這就是動(dòng)態(tài)字符串sds。而value則比較復(fù)雜,為了在同?個(gè)dict內(nèi)能夠存儲(chǔ)不同類型的value,這就需要?個(gè)通?的數(shù)據(jù)結(jié)構(gòu),這個(gè)通用的數(shù)據(jù)結(jié)構(gòu)就是robj,全名是redisObject。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

Redis的編碼方式

Redis中會(huì)根據(jù)存儲(chǔ)的數(shù)據(jù)類型不同,選擇不同的編碼方式,共包含11種不同類型:

編號(hào) 編碼方式 說(shuō)明
0 OBJ_ENCODING_RAW raw編碼動(dòng)態(tài)字符串
1 OBJ_ENCODING_INT long類型的整數(shù)的字符串
2 OBJ_ENCODING_HT hash表(字典dict)
3 OBJ_ENCODING_ZIPMAP 已廢棄
4 OBJ_ENCODING_LINKEDLIST 雙端鏈表
5 OBJ_ENCODING_ZIPLIST 壓縮列表
6 OBJ_ENCODING_INTSET 整數(shù)集合
7 OBJ_ENCODING_SKIPLIST 跳表
8 OBJ_ENCODING_EMBSTR embstr的動(dòng)態(tài)字符串
9 OBJ_ENCODING_QUICKLIST 快速列表
10 OBJ_ENCODING_STREAM Stream流

五種數(shù)據(jù)結(jié)構(gòu)

Redis中會(huì)根據(jù)存儲(chǔ)的數(shù)據(jù)類型不同,選擇不同的編碼方式。每種數(shù)據(jù)類型的使用的編碼方式如下:

數(shù)據(jù)類型 編碼方式
OBJ_STRING int、embstr、raw
OBJ_LIST LinkedList和ZipList(3.2以前)、QuickList(3.2以后)
OBJ_SET intset、HT
OBJ_ZSET ZipList、HT、SkipList
OBJ_HASH ZipList、HT

1.9 String

String是Redis中最常見的數(shù)據(jù)存儲(chǔ)類型:

其基本編碼方式是RAW,基于簡(jiǎn)單動(dòng)態(tài)字符串(SDS)實(shí)現(xiàn),存儲(chǔ)上限為512mb。

如果存儲(chǔ)的SDS長(zhǎng)度小于44字節(jié),則會(huì)采用EMBSTR編碼,此時(shí)object head與SDS是一段連續(xù)空間。申請(qǐng)內(nèi)存時(shí)

只需要調(diào)用一次內(nèi)存分配函數(shù),效率更高。

(1)底層實(shí)現(xiàn)?式:動(dòng)態(tài)字符串sds 或者 long
String的內(nèi)部存儲(chǔ)結(jié)構(gòu)?般是sds(Simple Dynamic String,可以動(dòng)態(tài)擴(kuò)展內(nèi)存),但是如果?個(gè)String類型的value的值是數(shù)字,那么Redis內(nèi)部會(huì)把它轉(zhuǎn)成long類型來(lái)存儲(chǔ),從?減少內(nèi)存的使用。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

如果存儲(chǔ)的字符串是整數(shù)值,并且大小在LONG_MAX范圍內(nèi),則會(huì)采用INT編碼:直接將數(shù)據(jù)保存在RedisObject的ptr指針位置(剛好8字節(jié)),不再需要SDS了。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

確切地說(shuō),String在Redis中是??個(gè)robj來(lái)表示的。

用來(lái)表示String的robj可能編碼成3種內(nèi)部表?:OBJ_ENCODING_RAW,OBJ_ENCODING_EMBSTR,OBJ_ENCODING_INT。
其中前兩種編碼使?的是sds來(lái)存儲(chǔ),最后?種OBJ_ENCODING_INT編碼直接把string存成了long型。
在對(duì)string進(jìn)行incr, decr等操作的時(shí)候,如果它內(nèi)部是OBJ_ENCODING_INT編碼,那么可以直接行加減操作;如果它內(nèi)部是OBJ_ENCODING_RAW或OBJ_ENCODING_EMBSTR編碼,那么Redis會(huì)先試圖把sds存儲(chǔ)的字符串轉(zhuǎn)成long型,如果能轉(zhuǎn)成功,再進(jìn)行加減操作。對(duì)?個(gè)內(nèi)部表示成long型的string執(zhí)行append, setbit, getrange這些命令,針對(duì)的仍然是string的值(即?進(jìn)制表示的字符串),而不是針對(duì)內(nèi)部表?的long型進(jìn)?操作。比如字符串”32”,如果按照字符數(shù)組來(lái)解釋,它包含兩個(gè)字符,它們的ASCII碼分別是0x33和0x32。當(dāng)我們執(zhí)行命令setbit key 7 0的時(shí)候,相當(dāng)于把字符0x33變成了0x32,這樣字符串的值就變成了”22”。?如果將字符串”32”按照內(nèi)部的64位long型來(lái)解釋,那么它是0x0000000000000020,在這個(gè)基礎(chǔ)上執(zhí)?setbit位操作,結(jié)果就完全不對(duì)了。因此,在這些命令的實(shí)現(xiàn)中,會(huì)把long型先轉(zhuǎn)成字符串再進(jìn)行相應(yīng)的操作。

2.0 List

Redis的List類型可以從首、尾操作列表中的元素:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

哪一個(gè)數(shù)據(jù)結(jié)構(gòu)能滿足上述特征?

  • LinkedList :普通鏈表,可以從雙端訪問(wèn),內(nèi)存占用較高,內(nèi)存碎片較多
  • ZipList :壓縮列表,可以從雙端訪問(wèn),內(nèi)存占用低,存儲(chǔ)上限低
  • QuickList:LinkedList + ZipList,可以從雙端訪問(wèn),內(nèi)存占用較低,包含多個(gè)ZipList,存儲(chǔ)上限高

Redis的List結(jié)構(gòu)類似一個(gè)雙端鏈表,可以從首、尾操作列表中的元素:

在3.2版本之前,Redis采用ZipList和LinkedList來(lái)實(shí)現(xiàn)List,當(dāng)元素?cái)?shù)量小于512并且元素大小小于64字節(jié)時(shí)采用ZipList編碼,超過(guò)則采用LinkedList編碼。

在3.2版本之后,Redis統(tǒng)一采用QuickList來(lái)實(shí)現(xiàn)List:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.1 Set

Set是Redis中的單列集合,滿足下列特點(diǎn):

  • 不保證有序性
  • 保證元素唯一
  • 求交集、并集、差集
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

可以看出,Set對(duì)查詢?cè)氐男室蠓浅8?,思考一下,什么樣的?shù)據(jù)結(jié)構(gòu)可以滿足?
HashTable,也就是Redis中的Dict,不過(guò)Dict是雙列集合(可以存鍵、值對(duì))

Set是Redis中的集合,不一定確保元素有序,可以滿足元素唯一、查詢效率要求極高。
為了查詢效率和唯一性,set采用HT編碼(Dict)。Dict中的key用來(lái)存儲(chǔ)元素,value統(tǒng)一為null。
當(dāng)存儲(chǔ)的所有數(shù)據(jù)都是整數(shù),并且元素?cái)?shù)量不超過(guò)set-max-intset-entries時(shí),Set會(huì)采用IntSet編碼,以節(jié)省內(nèi)存
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

結(jié)構(gòu)如下:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.2 ZSET

ZSet也就是SortedSet,其中每一個(gè)元素都需要指定一個(gè)score值和member值:

  • 可以根據(jù)score值排序后
  • member必須唯一
  • 可以根據(jù)member查詢分?jǐn)?shù)

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

因此,zset底層數(shù)據(jù)結(jié)構(gòu)必須滿足鍵值存儲(chǔ)、鍵必須唯一、可排序這幾個(gè)需求。之前學(xué)習(xí)的哪種編碼結(jié)構(gòu)可以滿足?

  • SkipList:可以排序,并且可以同時(shí)存儲(chǔ)score和ele值(member)
  • HT(Dict):可以鍵值存儲(chǔ),并且可以根據(jù)key找value

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

當(dāng)元素?cái)?shù)量不多時(shí),HT和SkipList的優(yōu)勢(shì)不明顯,而且更耗內(nèi)存。因此zset還會(huì)采用ZipList結(jié)構(gòu)來(lái)節(jié)省內(nèi)存,不過(guò)需要同時(shí)滿足兩個(gè)條件:

  • 元素?cái)?shù)量小于zset_max_ziplist_entries,默認(rèn)值128
  • 每個(gè)元素都小于zset_max_ziplist_value字節(jié),默認(rèn)值64

ziplist本身沒有排序功能,而且沒有鍵值對(duì)的概念,因此需要有zset通過(guò)編碼實(shí)現(xiàn):

  • ZipList是連續(xù)內(nèi)存,因此score和element是緊挨在一起的兩個(gè)entry, element在前,score在后
  • score越小越接近隊(duì)首,score越大越接近隊(duì)尾,按照score值升序排列
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.3 Hash

Hash結(jié)構(gòu)與Redis中的Zset非常類似:

  • 都是鍵值存儲(chǔ)
  • 都需求根據(jù)鍵獲取值
  • 鍵必須唯一

區(qū)別如下:

  • zset的鍵是member,值是score;hash的鍵和值都是任意值
  • zset要根據(jù)score排序;hash則無(wú)需排序

(1)底層實(shí)現(xiàn)方式:壓縮列表ziplist 或者 字典dict
當(dāng)Hash中數(shù)據(jù)項(xiàng)比較少的情況下,Hash底層才?壓縮列表ziplist進(jìn)?存儲(chǔ)數(shù)據(jù),隨著數(shù)據(jù)的增加,底層的ziplist就可能會(huì)轉(zhuǎn)成dict,具體配置如下:

hash-max-ziplist-entries 512

hash-max-ziplist-value 64

當(dāng)滿足上面兩個(gè)條件其中之?的時(shí)候,Redis就使?dict字典來(lái)實(shí)現(xiàn)hash。
Redis的hash之所以這樣設(shè)計(jì),是因?yàn)楫?dāng)ziplist變得很?的時(shí)候,它有如下幾個(gè)缺點(diǎn):

  • 每次插?或修改引發(fā)的realloc操作會(huì)有更?的概率造成內(nèi)存拷貝,從而降低性能。
  • ?旦發(fā)生內(nèi)存拷貝,內(nèi)存拷貝的成本也相應(yīng)增加,因?yàn)橐截惛?的?塊數(shù)據(jù)。
  • 當(dāng)ziplist數(shù)據(jù)項(xiàng)過(guò)多的時(shí)候,在它上?查找指定的數(shù)據(jù)項(xiàng)就會(huì)性能變得很低,因?yàn)閦iplist上的查找需要進(jìn)行遍歷。

總之,ziplist本來(lái)就設(shè)計(jì)為各個(gè)數(shù)據(jù)項(xiàng)挨在?起組成連續(xù)的內(nèi)存空間,這種結(jié)構(gòu)并不擅長(zhǎng)做修改操作。?旦數(shù)據(jù)發(fā)?改動(dòng),就會(huì)引發(fā)內(nèi)存realloc,可能導(dǎo)致內(nèi)存拷貝。

hash結(jié)構(gòu)如下:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

zset集合如下:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

因此,Hash底層采用的編碼與Zset也基本一致,只需要把排序有關(guān)的SkipList去掉即可:

Hash結(jié)構(gòu)默認(rèn)采用ZipList編碼,用以節(jié)省內(nèi)存。 ZipList中相鄰的兩個(gè)entry 分別保存field和value

當(dāng)數(shù)據(jù)量較大時(shí),Hash結(jié)構(gòu)會(huì)轉(zhuǎn)為HT編碼,也就是Dict,觸發(fā)條件有兩個(gè):

  • ZipList中的元素?cái)?shù)量超過(guò)了hash-max-ziplist-entries(默認(rèn)512)
  • ZipList中的任意entry大小超過(guò)了hash-max-ziplist-value(默認(rèn)64字節(jié))
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2 Redis網(wǎng)絡(luò)模型

2.1 用戶空間和內(nèi)核態(tài)空間

服務(wù)器大多都采用Linux系統(tǒng),這里我們以Linux為例來(lái)講解:

ubuntu和Centos 都是Linux的發(fā)行版,發(fā)行版可以看成對(duì)linux包了一層殼,任何Linux發(fā)行版,其系統(tǒng)內(nèi)核都是Linux。我們的應(yīng)用都需要通過(guò)Linux內(nèi)核與硬件交互

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

用戶的應(yīng)用,比如redis,mysql等其實(shí)是沒有辦法去執(zhí)行訪問(wèn)我們操作系統(tǒng)的硬件的,所以我們可以通過(guò)發(fā)行版的這個(gè)殼子去訪問(wèn)內(nèi)核,再通過(guò)內(nèi)核去訪問(wèn)計(jì)算機(jī)硬件

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

計(jì)算機(jī)硬件包括,如cpu,內(nèi)存,網(wǎng)卡等等,內(nèi)核(通過(guò)尋址空間)可以操作硬件的,但是內(nèi)核需要不同設(shè)備的驅(qū)動(dòng),有了這些驅(qū)動(dòng)之后,內(nèi)核就可以去對(duì)計(jì)算機(jī)硬件去進(jìn)行 內(nèi)存管理,文件系統(tǒng)的管理,進(jìn)程的管理等等

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

我們想要用戶的應(yīng)用來(lái)訪問(wèn),計(jì)算機(jī)就必須要通過(guò)對(duì)外暴露的一些接口,才能訪問(wèn)到,從而簡(jiǎn)介的實(shí)現(xiàn)對(duì)內(nèi)核的操控,但是內(nèi)核本身上來(lái)說(shuō)也是一個(gè)應(yīng)用,所以他本身也需要一些內(nèi)存,cpu等設(shè)備資源,用戶應(yīng)用本身也在消耗這些資源,如果不加任何限制,用戶去操作隨意的去操作我們的資源,就有可能導(dǎo)致一些沖突,甚至有可能導(dǎo)致我們的系統(tǒng)出現(xiàn)無(wú)法運(yùn)行的問(wèn)題,因此我們需要把用戶和內(nèi)核隔離開

  • 進(jìn)程的尋址空間劃分成兩部分:內(nèi)核空間、用戶空間
  • 用戶空間只能執(zhí)行受限的命令 (Ring3),而且不能直接調(diào)用系統(tǒng)資源,必須通過(guò)內(nèi)核提供的接口來(lái)訪問(wèn)
  • 內(nèi)核空間可以執(zhí)行特權(quán)命令 (Ring0),調(diào)用一切系統(tǒng)資源

什么是尋址空間呢?我們的應(yīng)用程序也好,還是內(nèi)核空間也好,都是沒有辦法直接去物理內(nèi)存的,而是通過(guò)分配一些虛擬內(nèi)存映射到物理內(nèi)存中,我們的內(nèi)核和應(yīng)用程序去訪問(wèn)虛擬內(nèi)存的時(shí)候,就需要一個(gè)虛擬地址,這個(gè)地址是一個(gè)無(wú)符號(hào)的整數(shù),比如一個(gè)32位的操作系統(tǒng),他的帶寬就是32,他的虛擬地址就是2的32次方,也就是說(shuō)他尋址的范圍就是0~2的32次方, 這片尋址空間對(duì)應(yīng)的就是2的32個(gè)字節(jié),就是4GB,這個(gè)4GB,會(huì)有3個(gè)GB分給用戶空間,會(huì)有1GB給內(nèi)核系統(tǒng)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

在linux中,他們權(quán)限分成兩個(gè)等級(jí),0和3,用戶空間只能執(zhí)行受限的命令(Ring3),而且不能直接調(diào)用系統(tǒng)資源,必須通過(guò)內(nèi)核提供的接口來(lái)訪問(wèn)內(nèi)核空間可以執(zhí)行特權(quán)命令(Ring0),調(diào)用一切系統(tǒng)資源,所以一般情況下,用戶的操作是運(yùn)行在用戶空間,而內(nèi)核運(yùn)行的數(shù)據(jù)是在內(nèi)核空間的,而有的情況下,一個(gè)應(yīng)用程序需要去調(diào)用一些特權(quán)資源,去調(diào)用一些內(nèi)核空間的操作,所以此時(shí)他倆需要在用戶態(tài)和內(nèi)核態(tài)之間進(jìn)行切換。

比如:

Linux系統(tǒng)為了提高IO效率,會(huì)在用戶空間和內(nèi)核空間都加入緩沖區(qū):

  • 寫數(shù)據(jù)時(shí),要把用戶緩沖數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),然后寫入設(shè)備

  • 讀數(shù)據(jù)時(shí),要從設(shè)備讀取數(shù)據(jù)到內(nèi)核緩沖區(qū),然后拷貝到用戶緩沖區(qū)

針對(duì)這個(gè)操作:我們的用戶在寫讀數(shù)據(jù)時(shí),會(huì)去向內(nèi)核態(tài)申請(qǐng),想要讀取內(nèi)核的數(shù)據(jù),而內(nèi)核數(shù)據(jù)要去等待驅(qū)動(dòng)程序從硬件上讀取數(shù)據(jù),當(dāng)從磁盤上加載到數(shù)據(jù)之后,內(nèi)核會(huì)將數(shù)據(jù)寫入到內(nèi)核的緩沖區(qū)中,然后再將數(shù)據(jù)拷貝到用戶態(tài)的buffer中,然后再返回給應(yīng)用程序,整體而言,速度慢,就是這個(gè)原因,為了加速,我們希望read也好,還是wait for data也最好都不要等待,或者時(shí)間盡量的短。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.2 阻塞IO

在《UNIX網(wǎng)絡(luò)編程》一書中,總結(jié)歸納了5種IO模型:

  • 阻塞IOBlocking IO
  • 非阻塞IONonblocking IO
  • IO多路復(fù)用IO Multiplexing
  • 信號(hào)驅(qū)動(dòng)IOSignal Driven IO
  • 異步IOAsynchronous IO

應(yīng)用程序想要去讀取數(shù)據(jù),他是無(wú)法直接去讀取磁盤數(shù)據(jù)的,他需要先到內(nèi)核里邊去等待內(nèi)核操作硬件拿到數(shù)據(jù),這個(gè)過(guò)程就是1,是需要等待的,等到內(nèi)核從磁盤上把數(shù)據(jù)加載出來(lái)之后,再把這個(gè)數(shù)據(jù)寫給用戶的緩存區(qū),這個(gè)過(guò)程是2,如果是阻塞IO,那么整個(gè)過(guò)程中,用戶從發(fā)起讀請(qǐng)求開始,一直到讀取到數(shù)據(jù),都是一個(gè)阻塞狀態(tài)。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

具體流程如下圖:

用戶去讀取數(shù)據(jù)時(shí),會(huì)去先發(fā)起recvform一個(gè)命令,去嘗試從內(nèi)核上加載數(shù)據(jù),如果內(nèi)核沒有數(shù)據(jù),那么用戶就會(huì)等待,此時(shí)內(nèi)核會(huì)去從硬件上讀取數(shù)據(jù),內(nèi)核讀取數(shù)據(jù)之后,會(huì)把數(shù)據(jù)拷貝到用戶態(tài),并且返回ok,整個(gè)過(guò)程,都是阻塞等待的,這就是阻塞IO

總結(jié)如下:

顧名思義,阻塞IO就是兩個(gè)階段都必須阻塞等待:

階段一:

  • 用戶進(jìn)程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
  • 此時(shí)數(shù)據(jù)尚未到達(dá),內(nèi)核需要等待數(shù)據(jù)
  • 此時(shí)用戶進(jìn)程也處于阻塞狀態(tài)

階段二:

  • 數(shù)據(jù)到達(dá)并拷貝到內(nèi)核緩沖區(qū),代表已就緒
  • 將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
  • 拷貝過(guò)程中,用戶進(jìn)程依然阻塞等待
  • 拷貝完成,用戶進(jìn)程解除阻塞,處理數(shù)據(jù)

可以看到,阻塞IO模型中,用戶進(jìn)程在兩個(gè)階段都是阻塞狀態(tài)。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.3 非阻塞IO

顧名思義,非阻塞IO的recvfrom操作會(huì)立即返回結(jié)果而不是阻塞用戶進(jìn)程。

階段一:

  • 用戶進(jìn)程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
  • 此時(shí)數(shù)據(jù)尚未到達(dá),內(nèi)核需要等待數(shù)據(jù)
  • 返回異常給用戶進(jìn)程
  • 用戶進(jìn)程拿到error后,再次嘗試讀取
  • 循環(huán)往復(fù),直到數(shù)據(jù)就緒

階段二:

  • 將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
  • 拷貝過(guò)程中,用戶進(jìn)程依然阻塞等待
  • 拷貝完成,用戶進(jìn)程解除阻塞,處理數(shù)據(jù)
  • 可以看到,非阻塞IO模型中,用戶進(jìn)程在第一個(gè)階段是非阻塞,第二個(gè)階段是阻塞狀態(tài)。雖然是非阻塞,但性能并沒有得到提高。而且忙等機(jī)制會(huì)導(dǎo)致CPU空轉(zhuǎn),CPU使用率暴增。
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.4 IO多路復(fù)用

無(wú)論是阻塞IO還是非阻塞IO,用戶應(yīng)用在一階段都需要調(diào)用recvfrom來(lái)獲取數(shù)據(jù),差別在于無(wú)數(shù)據(jù)時(shí)的處理方案:

  • 如果調(diào)用recvfrom時(shí),恰好沒有數(shù)據(jù),阻塞IO會(huì)使CPU阻塞,非阻塞IO使CPU空轉(zhuǎn),都不能充分發(fā)揮CPU的作用。
  • 如果調(diào)用recvfrom時(shí),恰好有數(shù)據(jù),則用戶進(jìn)程可以直接進(jìn)入第二階段,讀取并處理數(shù)據(jù)

所以怎么看起來(lái)以上兩種方式性能都不好

比如服務(wù)端處理客戶端Socket請(qǐng)求時(shí),在單線程情況下,只能依次處理每一個(gè)socket,如果正在處理的socket恰好未就緒(數(shù)據(jù)不可讀或不可寫),線程就會(huì)被阻塞,所有其它客戶端socket都必須等待,性能自然會(huì)很差。

就比如服務(wù)員給顧客點(diǎn)餐,分兩步
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

  • 顧客思考要吃什么(等待數(shù)據(jù)就緒)
  • 顧客想好了,開始點(diǎn)餐(讀取數(shù)據(jù))

要提高效率有幾種辦法?

  • 方案一:增加更多服務(wù)員(多線程)
  • 方案二:不排隊(duì),誰(shuí)想好了吃什么(數(shù)據(jù)就緒了),服務(wù)員就給誰(shuí)點(diǎn)餐(用戶應(yīng)用就去讀取數(shù)據(jù))

那么問(wèn)題來(lái)了:用戶進(jìn)程如何知道內(nèi)核中數(shù)據(jù)是否就緒呢?

所以接下來(lái)就需要詳細(xì)的來(lái)解決多路復(fù)用模型是如何知道到底怎么知道內(nèi)核數(shù)據(jù)是否就緒的問(wèn)題了

這個(gè)問(wèn)題的解決依賴于提出的

文件描述符(File Descriptor:簡(jiǎn)稱FD,是一個(gè)從0 開始的無(wú)符號(hào)整數(shù),用來(lái)關(guān)聯(lián)Linux中的一個(gè)文件。在Linux中,一切皆文件,例如常規(guī)文件、視頻、硬件設(shè)備等,當(dāng)然也包括網(wǎng)絡(luò)套接字(Socket)。

IO多路復(fù)用:是利用單個(gè)線程來(lái)同時(shí)監(jiān)聽多個(gè)FD,并在某個(gè)FD可讀、可寫時(shí)得到通知,從而避免無(wú)效的等待,充分利用CPU資源。

階段一:

  • 用戶進(jìn)程調(diào)用select,指定要監(jiān)聽的FD集合
  • 核監(jiān)聽FD對(duì)應(yīng)的多個(gè)socket
  • 任意一個(gè)或多個(gè)socket數(shù)據(jù)就緒則返回readable
  • 此過(guò)程中用戶進(jìn)程阻塞

階段二:

  • 用戶進(jìn)程找到就緒的socket
  • 依次調(diào)用recvfrom讀取數(shù)據(jù)
  • 內(nèi)核將數(shù)據(jù)拷貝到用戶空間
  • 用戶進(jìn)程處理數(shù)據(jù)

當(dāng)用戶去讀取數(shù)據(jù)的時(shí)候,不再去直接調(diào)用recvfrom了,而是調(diào)用select的函數(shù),select函數(shù)會(huì)將需要監(jiān)聽的數(shù)據(jù)交給內(nèi)核,由內(nèi)核去檢查這些數(shù)據(jù)是否就緒了,如果說(shuō)這個(gè)數(shù)據(jù)就緒了,就會(huì)通知應(yīng)用程序數(shù)據(jù)就緒,然后來(lái)讀取數(shù)據(jù),再?gòu)膬?nèi)核中把數(shù)據(jù)拷貝給用戶態(tài),完成數(shù)據(jù)處理,如果N多個(gè)FD一個(gè)都沒處理完,此時(shí)就進(jìn)行等待。

用IO復(fù)用模式,可以確保去讀數(shù)據(jù)的時(shí)候,數(shù)據(jù)是一定存在的,他的效率比原來(lái)的阻塞IO和非阻塞IO性能都要高
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

IO多路復(fù)用:是利用單個(gè)線程來(lái)同時(shí)監(jiān)聽多個(gè)FD,并在某個(gè)FD可讀、可寫時(shí)得到通知,從而避免無(wú)效的等待,充分利用CPU資源。不過(guò)監(jiān)聽FD的方式、通知的方式又有多種實(shí)現(xiàn),常見的有:

  • select
  • poll
  • epoll
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

其中select和pool相當(dāng)于是當(dāng)被監(jiān)聽的數(shù)據(jù)準(zhǔn)備好之后,他會(huì)把你監(jiān)聽的FD整個(gè)數(shù)據(jù)都發(fā)給你,你需要到整個(gè)FD中去找,哪些是處理好了的,需要通過(guò)遍歷的方式,所以性能也并不是那么好

而epoll,則相當(dāng)于內(nèi)核準(zhǔn)備好了之后,他會(huì)把準(zhǔn)備好的數(shù)據(jù),直接發(fā)給你,咱們就省去了遍歷的動(dòng)作。

2.4.1 select方式

select是Linux最早是由的I/O多路復(fù)用技術(shù):

簡(jiǎn)單說(shuō),就是我們把需要處理的數(shù)據(jù)封裝成FD,然后在用戶態(tài)時(shí)創(chuàng)建一個(gè)fd的集合(這個(gè)集合的大小是要監(jiān)聽的那個(gè)FD的最大值+1,但是大小整體是有限制的 ),這個(gè)集合的長(zhǎng)度大小是有限制的,同時(shí)在這個(gè)集合中,標(biāo)明出來(lái)我們要控制哪些數(shù)據(jù),

比如要監(jiān)聽的數(shù)據(jù),是1,2,5三個(gè)數(shù)據(jù),此時(shí)會(huì)執(zhí)行select函數(shù),然后將整個(gè)fd發(fā)給內(nèi)核態(tài),內(nèi)核態(tài)會(huì)去遍歷用戶態(tài)傳遞過(guò)來(lái)的數(shù)據(jù),如果發(fā)現(xiàn)這里邊都數(shù)據(jù)都沒有就緒,就休眠,直到有數(shù)據(jù)準(zhǔn)備好時(shí),就會(huì)被喚醒,喚醒之后,再次遍歷一遍,看看誰(shuí)準(zhǔn)備好了,然后再將處理掉沒有準(zhǔn)備好的數(shù)據(jù),最后再將這個(gè)FD集合寫回到用戶態(tài)中去,此時(shí)用戶態(tài)就知道了,奧,有人準(zhǔn)備好了,但是對(duì)于用戶態(tài)而言,并不知道誰(shuí)處理好了,所以用戶態(tài)也需要去進(jìn)行遍歷,然后找到對(duì)應(yīng)準(zhǔn)備好數(shù)據(jù)的節(jié)點(diǎn),再去發(fā)起讀請(qǐng)求,我們會(huì)發(fā)現(xiàn),這種模式下他雖然比阻塞IO和非阻塞IO好,但是依然有些麻煩的事情, 比如說(shuō)頻繁的傳遞fd集合,頻繁的去遍歷FD等問(wèn)題

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.4.2 poll模式

poll模式對(duì)select模式做了簡(jiǎn)單改進(jìn),但性能提升不明顯,部分關(guān)鍵代碼如下:

IO流程:

  • 創(chuàng)建pollfd數(shù)組,向其中添加關(guān)注的fd信息,數(shù)組大小自定義
  • 調(diào)用poll函數(shù),將pollfd數(shù)組拷貝到內(nèi)核空間,轉(zhuǎn)鏈表存儲(chǔ),無(wú)上限
  • 內(nèi)核遍歷fd,判斷是否就緒
  • 數(shù)據(jù)就緒或超時(shí)后,拷貝pollfd數(shù)組到用戶空間,返回就緒fd數(shù)量n
  • 用戶進(jìn)程判斷n是否大于0,大于0則遍歷pollfd數(shù)組,找到就緒的fd

與select對(duì)比:

  • select模式中的fd_set大小固定為1024,而pollfd在內(nèi)核中采用鏈表,理論上無(wú)上限
  • 監(jiān)聽FD越多,每次遍歷消耗時(shí)間也越久,性能反而會(huì)下降

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.4.3 epoll函數(shù)

epoll模式是對(duì)select和poll的改進(jìn),它提供了三個(gè)函數(shù):

第一個(gè)是:eventpoll的函數(shù),他內(nèi)部包含兩個(gè)東西

一個(gè)是:

1、紅黑樹 -> 記錄的事要監(jiān)聽的FD

另一個(gè)是:

2、鏈表 -> 一個(gè)鏈表,記錄的是就緒的FD

緊接著調(diào)用epoll_ctl操作,將要監(jiān)聽的數(shù)據(jù)添加到紅黑樹上去,并且給每個(gè)fd設(shè)置一個(gè)監(jiān)聽函數(shù),這個(gè)函數(shù)會(huì)在fd數(shù)據(jù)就緒時(shí)觸發(fā),就是準(zhǔn)備好了,現(xiàn)在就把fd把數(shù)據(jù)添加到list_head中去

3、調(diào)用epoll_wait函數(shù)

就去等待,在用戶態(tài)創(chuàng)建一個(gè)空的events數(shù)組,當(dāng)就緒之后,我們的回調(diào)函數(shù)會(huì)把數(shù)據(jù)添加到list_head中去,當(dāng)調(diào)用這個(gè)函數(shù)的時(shí)候,會(huì)去檢查list_head,當(dāng)然這個(gè)過(guò)程需要參考配置的等待時(shí)間,可以等一定時(shí)間,也可以一直等, 如果在此過(guò)程中,檢查到了list_head中有數(shù)據(jù)會(huì)將數(shù)據(jù)添加到鏈表中,此時(shí)將數(shù)據(jù)放入到events數(shù)組中,并且返回對(duì)應(yīng)的操作的數(shù)量,用戶態(tài)的此時(shí)收到響應(yīng)后,從events中拿到對(duì)應(yīng)準(zhǔn)備好的數(shù)據(jù)的節(jié)點(diǎn),再去調(diào)用方法去拿數(shù)據(jù)。

小總結(jié):

select模式存在的三個(gè)問(wèn)題:

  • 能監(jiān)聽的FD最大不超過(guò)1024
  • 每次select都需要把所有要監(jiān)聽的FD都拷貝到內(nèi)核空間
  • 每次都要遍歷所有FD來(lái)判斷就緒狀態(tài)

poll模式的問(wèn)題:

  • poll利用鏈表解決了select中監(jiān)聽FD上限的問(wèn)題,但依然要遍歷所有FD,如果監(jiān)聽較多,性能會(huì)下降

epoll模式中如何解決這些問(wèn)題的?

  • 基于epoll實(shí)例中的紅黑樹保存要監(jiān)聽的FD,理論上無(wú)上限,而且增刪改查效率都非常高
  • 每個(gè)FD只需要執(zhí)行一次epoll_ctl添加到紅黑樹,以后每次epol_wait無(wú)需傳遞任何參數(shù),無(wú)需重復(fù)拷貝FD到內(nèi)核空間
  • 利用ep_poll_callback機(jī)制來(lái)監(jiān)聽FD狀態(tài),無(wú)需遍歷所有FD,因此性能不會(huì)隨監(jiān)聽的FD數(shù)量增多而下降

2.4.4 事件通知機(jī)制

當(dāng)FD有數(shù)據(jù)可讀時(shí),我們調(diào)用epoll_wait(或者select、poll)可以得到通知。但是事件通知的模式有兩種:

  • LevelTriggered:簡(jiǎn)稱LT,也叫做水平觸發(fā)。只要某個(gè)FD中有數(shù)據(jù)可讀,每次調(diào)用epoll_wait都會(huì)得到通知。
  • EdgeTriggered:簡(jiǎn)稱ET,也叫做邊沿觸發(fā)。只有在某個(gè)FD有狀態(tài)變化時(shí),調(diào)用epoll_wait才會(huì)被通知。

舉個(gè)栗子:

  • 假設(shè)一個(gè)客戶端socket對(duì)應(yīng)的FD已經(jīng)注冊(cè)到了epoll實(shí)例中
  • 客戶端socket發(fā)送了2kb的數(shù)據(jù)
  • 服務(wù)端調(diào)用epoll_wait,得到通知說(shuō)FD就緒
  • 服務(wù)端從FD讀取了1kb數(shù)據(jù)回到步驟3(再次調(diào)用epoll_wait,形成循環(huán))

結(jié)論:

如果我們采用LT模式,因?yàn)镕D中仍有1kb數(shù)據(jù),則第⑤步依然會(huì)返回結(jié)果,并且得到通知

如果我們采用ET模式,因?yàn)榈冖鄄揭呀?jīng)消費(fèi)了FD可讀事件,第⑤步FD狀態(tài)沒有變化,因此epoll_wait不會(huì)返回,數(shù)據(jù)無(wú)法讀取,客戶端響應(yīng)超時(shí)。

2.4.5 基于epoll的服務(wù)器端流程

我們來(lái)梳理一下這張圖

服務(wù)器啟動(dòng)以后,服務(wù)端會(huì)去調(diào)用epoll_create,創(chuàng)建一個(gè)epoll實(shí)例,epoll實(shí)例中包含兩個(gè)數(shù)據(jù)

1、紅黑樹(為空):rb_root 用來(lái)去記錄需要被監(jiān)聽的FD

2、鏈表(為空):list_head,用來(lái)存放已經(jīng)就緒的FD

創(chuàng)建好了之后,會(huì)去調(diào)用epoll_ctl函數(shù),此函數(shù)會(huì)會(huì)將需要監(jiān)聽的數(shù)據(jù)添加到rb_root中去,并且對(duì)當(dāng)前這些存在于紅黑樹的節(jié)點(diǎn)設(shè)置回調(diào)函數(shù),當(dāng)這些被監(jiān)聽的數(shù)據(jù)一旦準(zhǔn)備完成,就會(huì)被調(diào)用,而調(diào)用的結(jié)果就是將紅黑樹的fd添加到list_head中去(但是此時(shí)并沒有完成)

3、當(dāng)?shù)诙酵瓿珊?,就?huì)調(diào)用epoll_wait函數(shù),這個(gè)函數(shù)會(huì)去校驗(yàn)是否有數(shù)據(jù)準(zhǔn)備完畢(因?yàn)閿?shù)據(jù)一旦準(zhǔn)備就緒,就會(huì)被回調(diào)函數(shù)添加到list_head中),在等待了一段時(shí)間后(可以進(jìn)行配置),如果等夠了超時(shí)時(shí)間,則返回沒有數(shù)據(jù),如果有,則進(jìn)一步判斷當(dāng)前是什么事件,如果是建立連接時(shí)間,則調(diào)用accept() 接受客戶端socket,拿到建立連接的socket,然后建立起來(lái)連接,如果是其他事件,則把數(shù)據(jù)進(jìn)行寫出
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.5 信號(hào)驅(qū)動(dòng)

信號(hào)驅(qū)動(dòng)IO是與內(nèi)核建立SIGIO的信號(hào)關(guān)聯(lián)并設(shè)置回調(diào),當(dāng)內(nèi)核有FD就緒時(shí),會(huì)發(fā)出SIGIO信號(hào)通知用戶,期間用戶應(yīng)用可以執(zhí)行其它業(yè)務(wù),無(wú)需阻塞等待。

階段一:

  • 用戶進(jìn)程調(diào)用sigaction,注冊(cè)信號(hào)處理函數(shù)
  • 內(nèi)核返回成功,開始監(jiān)聽FD
  • 用戶進(jìn)程不阻塞等待,可以執(zhí)行其它業(yè)務(wù)
  • 當(dāng)內(nèi)核數(shù)據(jù)就緒后,回調(diào)用戶進(jìn)程的SIGIO處理函數(shù)

階段二:

  • 收到SIGIO回調(diào)信號(hào)
  • 調(diào)用recvfrom,讀取
  • 內(nèi)核將數(shù)據(jù)拷貝到用戶空間
  • 用戶進(jìn)程處理數(shù)據(jù)
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

缺點(diǎn):

  • 當(dāng)有大量IO操作時(shí),信號(hào)較多,SIGIO處理函數(shù)不能及時(shí)處理可能導(dǎo)致信號(hào)隊(duì)列溢出
  • 而且內(nèi)核空間與用戶空間的頻繁信號(hào)交互性能也較低。

2.6 異步IO

異步IO的整個(gè)過(guò)程都是非阻塞的,用戶進(jìn)程調(diào)用完異步API后就可以去做其它事情,內(nèi)核等待數(shù)據(jù)就緒并拷貝到用戶空間后才會(huì)遞交信號(hào),通知用戶進(jìn)程。

這種方式,不僅僅是用戶態(tài)在試圖讀取數(shù)據(jù)后,不阻塞,而且當(dāng)內(nèi)核的數(shù)據(jù)準(zhǔn)備完成后,也不會(huì)阻塞
他會(huì)由內(nèi)核將所有數(shù)據(jù)處理完成后,由內(nèi)核將數(shù)據(jù)寫入到用戶態(tài)中,然后才算完成,所以性能極高,不會(huì)有任何阻塞,全部都由內(nèi)核完成,可以看到,異步IO模型中,用戶進(jìn)程在兩個(gè)階段都是非阻塞狀態(tài)。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
同步和異步對(duì)比

最后用一幅圖,來(lái)說(shuō)明他們之間的區(qū)別:

IO操作是同步還是異步,關(guān)鍵看數(shù)據(jù)在內(nèi)核空間與用戶空間的拷貝過(guò)程(數(shù)據(jù)讀寫的IO操作)
也就是階段二是同步還是異步:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

2.7 Redis網(wǎng)絡(luò)模型

2.7.1 Redis是單線程的嗎?為什么使用單線程

Redis到底是單線程還是多線程?

  • 如果僅僅聊Redis的核心業(yè)務(wù)部分(命令處理),答案是單線程
  • 如果是聊整個(gè)Redis,那么答案就是多線程

在Redis版本迭代過(guò)程中,在兩個(gè)重要的時(shí)間節(jié)點(diǎn)上引入了多線程的支持:

  • Redis v4.0:引入多線程異步處理一些耗時(shí)較舊的任務(wù),例如異步刪除命令unlink
  • Redis v6.0:在核心網(wǎng)絡(luò)模型中引入 多線程,進(jìn)一步提高對(duì)于多核CPU的利用率

因此,對(duì)于Redis的核心網(wǎng)絡(luò)模型,在Redis 6.0之前確實(shí)都是單線程。是利用epoll(Linux系統(tǒng))這樣的IO多路復(fù)用技術(shù)在事件循環(huán)中不斷處理客戶端情況。

為什么Redis要選擇單線程?

  • 拋開持久化不談,Redis是純 內(nèi)存操作,執(zhí)行速度非??欤男阅芷款i是網(wǎng)絡(luò)延遲而不是執(zhí)行速度,因此多線程并不會(huì)帶來(lái)巨大的性能提升。
  • 多線程會(huì)導(dǎo)致過(guò)多的上下文切換,帶來(lái)不必要的開銷
  • 引入多線程會(huì)面臨線程安全問(wèn)題,必然要引入線程鎖這樣的安全手段,實(shí)現(xiàn)復(fù)雜度增高,而且性能也會(huì)大打折扣

2.7.2 Redis的單線程模型-Redis單線程和多線程網(wǎng)絡(luò)模型變更

Redis通過(guò)IO多路復(fù)用來(lái)提高網(wǎng)絡(luò)性能,并且支持各種不同的多路復(fù)用實(shí)現(xiàn),并且將這些實(shí)現(xiàn)進(jìn)行封裝,提供了統(tǒng)一的高性能事件庫(kù)API庫(kù)AE:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

當(dāng)我們的客戶端想要去連接我們服務(wù)器,會(huì)去先到IO多路復(fù)用模型去進(jìn)行排隊(duì),會(huì)有一個(gè)連接應(yīng)答處理器,他會(huì)去接受讀請(qǐng)求,然后又把讀請(qǐng)求注冊(cè)到具體模型中去,此時(shí)這些建立起來(lái)的連接,如果是客戶端請(qǐng)求處理器去進(jìn)行執(zhí)行命令時(shí),他會(huì)去把數(shù)據(jù)讀取出來(lái),然后把數(shù)據(jù)放入到client中, clinet去解析當(dāng)前的命令轉(zhuǎn)化為redis認(rèn)識(shí)的命令,接下來(lái)就開始處理這些命令,從redis中的command中找到這些命令,然后就真正的去操作對(duì)應(yīng)的數(shù)據(jù)了,當(dāng)數(shù)據(jù)操作完成后,會(huì)去找到命令回復(fù)處理器,再由他將數(shù)據(jù)寫出。

3 Redis通信協(xié)議-RESP協(xié)議

Redis是一個(gè)CS架構(gòu)的軟件,通信一般分兩步(不包括pipeline和PubSub):

  • 客戶端(client)向服務(wù)端(server)發(fā)送一條命令

  • 服務(wù)端解析并執(zhí)行命令,返回響應(yīng)結(jié)果給客戶端

因此客戶端發(fā)送命令的格式、服務(wù)端響應(yīng)結(jié)果的格式必須有一個(gè)規(guī)范,這個(gè)規(guī)范就是通信協(xié)議。

而在Redis中采用的是RESP(Redis Serialization Protocol)協(xié)議:

Redis 1.2版本引入了RESP協(xié)議

Redis 2.0版本中成為與Redis服務(wù)端通信的標(biāo)準(zhǔn),稱為RESP2

Redis 6.0版本中,從RESP2升級(jí)到了RESP3協(xié)議,增加了更多數(shù)據(jù)類型并且支持6.0的新特性–客戶端緩存

但目前,默認(rèn)使用的依然是RESP2協(xié)議,也是我們要學(xué)習(xí)的協(xié)議版本(以下簡(jiǎn)稱RESP)。

在RESP中,通過(guò)首字節(jié)的字符來(lái)區(qū)分不同數(shù)據(jù)類型,常用的數(shù)據(jù)類型包括5種:

  • 單行字符串:首字節(jié)是 ‘+’ ,后面跟上單行字符串,以CRLF( “\r\n” )結(jié)尾。例如返回"OK": “+OK\r\n”

  • 錯(cuò)誤(Errors):首字節(jié)是 ‘-’ ,與單行字符串格式一樣,只是字符串是異常信息,例如:“-Error message\r\n”

  • 數(shù)值:首字節(jié)是 ‘:’ ,后面跟上數(shù)字格式的字符串,以CRLF結(jié)尾。例如:“:10\r\n”

  • 多行字符串:首字節(jié)是 ‘$’ ,表示二進(jìn)制安全的字符串,最大支持512MB:

    如果大小為0,則代表空字符串:“$0\r\n\r\n”

    如果大小為-1,則代表不存在:“$-1\r\n”

  • 數(shù)組:首字節(jié)是 ‘*’,后面跟上數(shù)組元素個(gè)數(shù),再跟上元素,元素?cái)?shù)據(jù)類型不限:
    中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

3.1 基于Socket自定義Redis的客戶端

Redis支持TCP通信,因此我們可以使用Socket來(lái)模擬客戶端,與Redis服務(wù)端建立連接:

public class Main {

    static Socket s;
    static PrintWriter writer;
    static BufferedReader reader;

    public static void main(String[] args) {
        try {
            // 1.建立連接
            String host = "192.168.150.101";
            int port = 6379;
            s = new Socket(host, port);
            // 2.獲取輸出流、輸入流
            writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
            reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));

            // 3.發(fā)出請(qǐng)求
            // 3.1.獲取授權(quán) auth 123321
            sendRequest("auth", "123321");
            Object obj = handleResponse();
            System.out.println("obj = " + obj);

            // 3.2.set name 虎哥
            sendRequest("set", "name", "虎哥");
            // 4.解析響應(yīng)
            obj = handleResponse();
            System.out.println("obj = " + obj);

            // 3.2.set name 虎哥
            sendRequest("get", "name");
            // 4.解析響應(yīng)
            obj = handleResponse();
            System.out.println("obj = " + obj);

            // 3.2.set name 虎哥
            sendRequest("mget", "name", "num", "msg");
            // 4.解析響應(yīng)
            obj = handleResponse();
            System.out.println("obj = " + obj);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.釋放連接
            try {
                if (reader != null) reader.close();
                if (writer != null) writer.close();
                if (s != null) s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static Object handleResponse() throws IOException {
        // 讀取首字節(jié)
        int prefix = reader.read();
        // 判斷數(shù)據(jù)類型標(biāo)示
        switch (prefix) {
            case '+': // 單行字符串,直接讀一行
                return reader.readLine();
            case '-': // 異常,也讀一行
                throw new RuntimeException(reader.readLine());
            case ':': // 數(shù)字
                return Long.parseLong(reader.readLine());
            case '$': // 多行字符串
                // 先讀長(zhǎng)度
                int len = Integer.parseInt(reader.readLine());
                if (len == -1) {
                    return null;
                }
                if (len == 0) {
                    return "";
                }
                // 再讀數(shù)據(jù),讀len個(gè)字節(jié)。我們假設(shè)沒有特殊字符,所以讀一行(簡(jiǎn)化)
                return reader.readLine();
            case '*':
                return readBulkString();
            default:
                throw new RuntimeException("錯(cuò)誤的數(shù)據(jù)格式!");
        }
    }

    private static Object readBulkString() throws IOException {
        // 獲取數(shù)組大小
        int len = Integer.parseInt(reader.readLine());
        if (len <= 0) {
            return null;
        }
        // 定義集合,接收多個(gè)元素
        List<Object> list = new ArrayList<>(len);
        // 遍歷,依次讀取每個(gè)元素
        for (int i = 0; i < len; i++) {
            list.add(handleResponse());
        }
        return list;
    }

    // set name 虎哥
    private static void sendRequest(String ... args) {
        writer.println("*" + args.length);
        for (String arg : args) {
            writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
            writer.println(arg);
        }
        writer.flush();
    }
}

3.2 Redis內(nèi)存回收

Redis之所以性能強(qiáng),最主要的原因就是基于內(nèi)存存儲(chǔ)。然而單節(jié)點(diǎn)的Redis其內(nèi)存大小不宜過(guò)大,會(huì)影響持久化或主從同步性能。

我們可以通過(guò)修改配置文件來(lái)設(shè)置Redis的最大內(nèi)存:
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

當(dāng)內(nèi)存使用達(dá)到上限時(shí),就無(wú)法存儲(chǔ)更多數(shù)據(jù)了。為了解決這個(gè)問(wèn)題,Redis提供了一些策略實(shí)現(xiàn)內(nèi)存回收:

3.2.1 內(nèi)存過(guò)期策略

在學(xué)習(xí)Redis緩存的時(shí)候我們說(shuō)過(guò),可以通過(guò)expire命令給Redis的key設(shè)置TTL(存活時(shí)間):
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

可以發(fā)現(xiàn),當(dāng)key的TTL到期以后,再次訪問(wèn)name返回的是nil,說(shuō)明這個(gè)key已經(jīng)不存在了,對(duì)應(yīng)的內(nèi)存也得到釋放。從而起到內(nèi)存回收的目的。

Redis本身是一個(gè)典型的key-value內(nèi)存存儲(chǔ)數(shù)據(jù)庫(kù),因此所有的key、value都保存在之前學(xué)習(xí)過(guò)的Dict結(jié)構(gòu)中。不過(guò)在其database結(jié)構(gòu)體中,有兩個(gè)Dict:一個(gè)用來(lái)記錄key-value;另一個(gè)用來(lái)記錄key-TTL。

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)
這里有兩個(gè)問(wèn)題需要我們思考:

  1. Redis是如何知道一個(gè)key是否過(guò)期呢?

    • 利用兩個(gè)Dict分別記錄key-value對(duì)及key-ttl對(duì)
  2. 是不是TTL到期就立即刪除了呢?

惰性刪除

惰性刪除:顧明思議并不是在TTL到期后就立刻刪除,而是在訪問(wèn)一個(gè)key的時(shí)候,檢查該key的存活時(shí)間,如果已經(jīng)過(guò)期才執(zhí)行刪除。
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

周期刪除

周期刪除:顧明思議是通過(guò)一個(gè)定時(shí)任務(wù),周期性的抽樣部分過(guò)期的key,然后執(zhí)行刪除。執(zhí)行周期有兩種:

  • Redis服務(wù)初始化函數(shù)initServer()中設(shè)置定時(shí)任務(wù),按照server.hz的頻率來(lái)執(zhí)行過(guò)期key清理,模式為SLOW
  • Redis的每個(gè)事件循環(huán)前會(huì)調(diào)用beforeSleep()函數(shù),執(zhí)行過(guò)期key清理,模式為FAST

SLOW模式規(guī)則:

  • 執(zhí)行頻率受server.hz影響,默認(rèn)為10,即每秒執(zhí)行10次,每個(gè)執(zhí)行周期100ms。
  • 執(zhí)行清理耗時(shí)不超過(guò)一次執(zhí)行周期的25%.默認(rèn)slow模式耗時(shí)不超過(guò)25ms
  • 逐個(gè)遍歷db,逐個(gè)遍歷db中的bucket,抽取20個(gè)key判斷是否過(guò)期
  • 如果沒達(dá)到時(shí)間上限(25ms)并且過(guò)期key比例大于10%,再進(jìn)行一次抽樣,否則結(jié)束
  • FAST模式規(guī)則(過(guò)期key比例小于10%不執(zhí)行 ):
  • 執(zhí)行頻率受beforeSleep()調(diào)用頻率影響,但兩次FAST模式間隔不低于2ms
  • 執(zhí)行清理耗時(shí)不超過(guò)1ms
  • 逐個(gè)遍歷db,逐個(gè)遍歷db中的bucket,抽取20個(gè)key判斷是否過(guò)期
    如果沒達(dá)到時(shí)間上限(1ms)并且過(guò)期key比例大于10%,再進(jìn)行一次抽樣,否則結(jié)束

小總結(jié):

RedisKey的TTL記錄方式:

  • 在RedisDB中通過(guò)一個(gè)Dict記錄每個(gè)Key的TTL時(shí)間

過(guò)期key的刪除策略:

  • 惰性清理:每次查找key時(shí)判斷是否過(guò)期,如果過(guò)期則刪除

  • 定期清理:定期抽樣部分key,判斷是否過(guò)期,如果過(guò)期則刪除。

定期清理的兩種模式:

  • SLOW模式執(zhí)行頻率默認(rèn)為10,每次不超過(guò)25ms

  • FAST模式執(zhí)行頻率不固定,但兩次間隔不低于2ms,每次耗時(shí)不超過(guò)1ms

3.2.2 內(nèi)存淘汰策略

內(nèi)存淘汰:就是當(dāng)Redis內(nèi)存使用達(dá)到設(shè)置的上限時(shí),主動(dòng)挑選部分key刪除以釋放更多內(nèi)存的流程。Redis會(huì)在處理客戶端命令的方法processCommand()中嘗試做內(nèi)存淘汰:

中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

淘汰策略

Redis支持8種不同策略來(lái)選擇要?jiǎng)h除的key:

  • noeviction: 不淘汰任何key,但是內(nèi)存滿時(shí)不允許寫入新數(shù)據(jù),默認(rèn)就是這種策略。

  • volatile-ttl: 對(duì)設(shè)置了TTL的key,比較key的剩余TTL值,TTL越小越先被淘汰

  • allkeys-random:對(duì)全體key ,隨機(jī)進(jìn)行淘汰。也就是直接從db->dict中隨機(jī)挑選

  • volatile-random:對(duì)設(shè)置了TTL的key ,隨機(jī)進(jìn)行淘汰。也就是從db->expires中隨機(jī)挑選。

  • allkeys-lru: 對(duì)全體key,基于LRU算法進(jìn)行淘汰

  • volatile-lru: 對(duì)設(shè)置了TTL的key,基于LRU算法進(jìn)行淘汰

  • allkeys-lfu: 對(duì)全體key,基于LFU算法進(jìn)行淘汰

  • volatile-lfu: 對(duì)設(shè)置了TTL的key,基于LFI算法進(jìn)行淘汰

    比較容易混淆的有兩個(gè):

    • LRU(Least Recently Used),最少最近使用。用當(dāng)前時(shí)間減去最后一次訪問(wèn)時(shí)間,這個(gè)值越大則淘汰優(yōu)先級(jí)越高。
    • LFU(Least Frequently Used),最少頻率使用。會(huì)統(tǒng)計(jì)每個(gè)key的訪問(wèn)頻率,值越小淘汰優(yōu)先級(jí)越高。

Redis的數(shù)據(jù)都會(huì)被封裝為RedisObject結(jié)構(gòu):
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)

LFU的訪問(wèn)次數(shù)之所以叫做邏輯訪問(wèn)次數(shù),是因?yàn)椴⒉皇敲看蝛ey被訪問(wèn)都計(jì)數(shù),而是通過(guò)運(yùn)算:

  • 生成0~1之間的隨機(jī)數(shù)R
  • 計(jì)算 (舊次數(shù) * lfu_log_factor + 1),記錄為P
  • 如果 R < P ,則計(jì)數(shù)器 + 1,且最大不超過(guò)255
  • 訪問(wèn)次數(shù)會(huì)隨時(shí)間衰減,距離上一次訪問(wèn)時(shí)間每隔 lfu_decay_time 分鐘,計(jì)數(shù)器 -1

最后用一副圖來(lái)描述當(dāng)前的這個(gè)流程吧
中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇),Redis,中間件,中間件,redis,數(shù)據(jù)庫(kù)文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-770981.html

到了這里,關(guān)于中間件系列 - Redis入門到實(shí)戰(zhàn)(原理篇)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來(lái)自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場(chǎng)。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請(qǐng)注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請(qǐng)點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 華為云應(yīng)用中間件DCS系列—Redis實(shí)現(xiàn)(社交APP)實(shí)時(shí)評(píng)論

    華為云應(yīng)用中間件DCS系列—Redis實(shí)現(xiàn)(社交APP)實(shí)時(shí)評(píng)論

    云服務(wù)、API、SDK,調(diào)試,查看,我都行 閱讀短文您可以學(xué)習(xí)到:應(yīng)用中間件系列之Redis實(shí)現(xiàn)(社交APP)實(shí)時(shí)評(píng)論 華為云開發(fā)者插件(Huawei Cloud Toolkit),作為華為云圍繞其產(chǎn)品能力向開發(fā)者桌面上的延伸,幫助開發(fā)者快速在本地連接華為云,打通華為云到開發(fā)者的最后一公里

    2024年02月07日
    瀏覽(21)
  • 華為云應(yīng)用中間件DCS系列—Redis實(shí)現(xiàn)(電商網(wǎng)站)秒殺搶購(gòu)示例

    華為云應(yīng)用中間件DCS系列—Redis實(shí)現(xiàn)(電商網(wǎng)站)秒殺搶購(gòu)示例

    云服務(wù)、API、SDK,調(diào)試,查看,我都行 閱讀短文您可以學(xué)習(xí)到:應(yīng)用中間件系列之Redis實(shí)現(xiàn)(電商網(wǎng)站)秒殺搶購(gòu)示例 華為云開發(fā)者插件(Huawei Cloud Toolkit),作為華為云圍繞其產(chǎn)品能力向開發(fā)者桌面上的延伸,幫助開發(fā)者快速在本地連接華為云,打通華為云到開發(fā)者的最后

    2024年02月07日
    瀏覽(22)
  • 中間件redis的使用

    Java中的中間件配置體現(xiàn)在springboot的yml配置文件中。Springboot框架支持微服務(wù)和中間件和restful api遠(yuǎn)程服務(wù)的調(diào)用。中間件是Java web系統(tǒng)的中間層的服務(wù)系統(tǒng)的調(diào)用接口。Springboot的自動(dòng)裝配和約定大于配置機(jī)制初始化springcontext的容器空間和注冊(cè)組件。使用容器管理服務(wù)注冊(cè)對(duì)象

    2024年02月05日
    瀏覽(23)
  • 中間件: Redis安裝與部署

    集群部署 啟動(dòng)6個(gè)redis節(jié)點(diǎn) 擴(kuò)縮容 https://blog.csdn.net/lzb348110175/article/details/122168638 擴(kuò)容: 添加兩個(gè)節(jié)點(diǎn), 第一個(gè)節(jié)點(diǎn)表示新節(jié)點(diǎn),第二個(gè)節(jié)點(diǎn)表示集群中已有的任意一個(gè)節(jié)點(diǎn) 分配slot, 后面的節(jié)點(diǎn)是已有的節(jié)點(diǎn) 添加slave, 第一個(gè)為slave, 第二個(gè)為slave對(duì)應(yīng)的master 縮容: 查看集

    2024年02月12日
    瀏覽(22)
  • Go重寫Redis中間件 - Go實(shí)現(xiàn)Redis集群

    Go重寫Redis中間件 - Go實(shí)現(xiàn)Redis集群

    這章的內(nèi)容是將我們之前實(shí)現(xiàn)的單機(jī)版的Redis擴(kuò)充成集群版,給Redis增加集群功能,在增加集群功能之前,我們先學(xué)習(xí)一下在分布式系統(tǒng)中引用非常廣泛的技術(shù)一致性哈希,一致性哈希在我們項(xiàng)目里就應(yīng)用在我們Redis集群的搭建這塊 詳解一致性哈希 Redis集群需求背景 單臺(tái)服務(wù)

    2024年02月13日
    瀏覽(30)
  • 緩存中間件Redis必知必會(huì)

    作者: 逍遙Sean 簡(jiǎn)介:一個(gè)主修Java的Web網(wǎng)站游戲服務(wù)器后端開發(fā)者 主頁(yè):https://blog.csdn.net/Ureliable 覺得博主文章不錯(cuò)的話,可以三連支持一下~ 如有需要我的支持,請(qǐng)私信或評(píng)論留言! 前言: 本文是對(duì)redis的基本用法操作的整理。 如果需要在linux環(huán)境中搭建一個(gè)redis服務(wù)參考

    2024年02月11日
    瀏覽(17)
  • Go重寫Redis中間件 - Go實(shí)現(xiàn)Redis持久化

    項(xiàng)目開發(fā)到這里,我們的下一步就是實(shí)現(xiàn)Redis的持久化落盤功能,Redis是一個(gè)內(nèi)存型的數(shù)據(jù)庫(kù),在之前我們實(shí)現(xiàn)的單機(jī)版Redis如果把進(jìn)程殺掉,我們通過(guò)GET、SET指令存儲(chǔ)的數(shù)據(jù)都將不復(fù)存在,數(shù)據(jù)只存在內(nèi)存的map里面,重啟之后什么都沒有了 我們現(xiàn)在的目標(biāo)就是把用戶發(fā)來(lái)的指

    2024年02月14日
    瀏覽(22)
  • Go重寫Redis中間件 - Go實(shí)現(xiàn)Redis協(xié)議解析器

    Redis網(wǎng)絡(luò)協(xié)議詳解 在解決完通信后,下一步就是搞清楚 Redis 的協(xié)議-RESP協(xié)議,其實(shí)就是一套類似JSON、Protocol Buffers的序列化協(xié)議,也就是我們的客戶端和服務(wù)端通信的協(xié)議 RESP定義了5種格式 簡(jiǎn)單字符串(Simple String) : 服務(wù)器用來(lái)返回簡(jiǎn)單的結(jié)果,以\\\"+\\\"開頭,\\\"rn\\\"結(jié)尾的字符串形

    2024年02月15日
    瀏覽(18)
  • 微服務(wù)中間件-分布式緩存Redis

    微服務(wù)中間件-分布式緩存Redis

    – 基于Redis集群解決單機(jī)Redis存在的問(wèn)題 單機(jī)的Redis存在四大問(wèn)題: 1.數(shù)據(jù)丟失問(wèn)題: Redis是內(nèi)存存儲(chǔ),服務(wù)重啟可能會(huì)丟失數(shù)據(jù) 2.并發(fā)能力問(wèn)題: 單節(jié)點(diǎn)Redis并發(fā)能力雖然不錯(cuò),但也無(wú)法滿足如618這樣的高并發(fā)場(chǎng)景 3.故障恢復(fù)問(wèn)題: 如果Redis宕機(jī),則服務(wù)不可用,需要一種自動(dòng)

    2024年02月12日
    瀏覽(21)
  • 【緩存中間件】Redis哈希槽的概念

    【緩存中間件】Redis哈希槽的概念

    分布式數(shù)據(jù)庫(kù)首先要解決把整個(gè)數(shù)據(jù)集按照分區(qū)規(guī)則映射到多個(gè)節(jié)點(diǎn)的問(wèn)題,即把數(shù)據(jù)集劃分到多個(gè)節(jié)點(diǎn)上,每個(gè)節(jié)點(diǎn)負(fù)責(zé)整體數(shù)據(jù)的一個(gè)子集。。 需要重點(diǎn)關(guān)注的是數(shù)據(jù)分區(qū)規(guī)則。常見的分區(qū)規(guī)則有哈希分區(qū)和順序分區(qū)兩種,哈希分區(qū)離散度好、數(shù)據(jù)分布業(yè)務(wù)無(wú)關(guān)、無(wú)法順

    2024年02月13日
    瀏覽(18)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請(qǐng)作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包