Redis 核心篇:唯快不破的秘密
天下武功,無堅(jiān)不摧,唯快不破!
學(xué)習(xí)一個(gè)技術(shù),通常只接觸了零散的技術(shù)點(diǎn),沒有在腦海里建立一個(gè)完整的知識(shí)框架和架構(gòu)體系,沒有系統(tǒng)觀。這樣會(huì)很吃力,而且會(huì)出現(xiàn)一看好像自己會(huì),過后就忘記,一臉懵逼。
跟著「碼哥字節(jié)」一起吃透 Redis,深層次的掌握 Redis 核心原理以及實(shí)戰(zhàn)技巧。一起搭建一套完整的知識(shí)框架,學(xué)會(huì)全局觀去整理整個(gè)知識(shí)體系。
系統(tǒng)觀其實(shí)是至關(guān)重要的,從某種程度上說,在解決問題時(shí),擁有了系統(tǒng)觀,就意味著你能有依據(jù)、有章法地定位和解決問題。
Redis 全景圖
全景圖可以圍繞兩個(gè)緯度展開,分別是:
應(yīng)用緯度:緩存使用、集群運(yùn)用、數(shù)據(jù)結(jié)構(gòu)的巧妙使用
系統(tǒng)緯度:可以歸類為三高
- 高性能:線程模型、網(wǎng)絡(luò) IO 模型、數(shù)據(jù)結(jié)構(gòu)、持久化機(jī)制;
- 高可用:主從復(fù)制、哨兵集群、Cluster 分片集群;
- 高拓展:負(fù)載均衡
Redis 系列篇章圍繞如下思維導(dǎo)圖展開,這次從 《Redis 唯快不破的秘密》一起探索 Redis 的核心知識(shí)點(diǎn)。
吃透Redis
唯快不破的秘密
65 哥前段時(shí)間去面試 996 大廠,被問到「Redis 為什么快?」
65 哥:額,因?yàn)樗腔趦?nèi)存實(shí)現(xiàn)和單線程模型
”面試官:還有呢?
65 哥:沒了呀。
”很多人僅僅只是知道基于內(nèi)存實(shí)現(xiàn),其他核心的原因模凌兩可。今日跟著「碼哥字節(jié)」一起探索真正快的原因,做一個(gè)唯快不破的真男人!
Redis 為了高性能,從各方各面都進(jìn)行了優(yōu)化,下次小伙伴們面試的時(shí)候,面試官問 Redis 性能為什么如此高,可不能傻傻的只說單線程和內(nèi)存存儲(chǔ)了。
唯快不破的秘密
根據(jù)官方數(shù)據(jù),Redis 的 QPS 可以達(dá)到約 100000(每秒請(qǐng)求數(shù)),有興趣的可以參考官方的基準(zhǔn)程序測試《How fast is Redis?》,地址:https://redis.io/topics/benchmarks
基準(zhǔn)測試
橫軸是連接數(shù),縱軸是 QPS。此時(shí),這張圖反映了一個(gè)數(shù)量級(jí),希望大家在面試的時(shí)候可以正確的描述出來,不要問你的時(shí)候,你回答的數(shù)量級(jí)相差甚遠(yuǎn)!
完全基于內(nèi)存實(shí)現(xiàn)“
65 哥:這個(gè)我知道,Redis 是基于內(nèi)存的數(shù)據(jù)庫,跟磁盤數(shù)據(jù)庫相比,完全吊打磁盤的速度,就像段譽(yù)的凌波微步。對(duì)于磁盤數(shù)據(jù)庫來說,首先要將數(shù)據(jù)通過 IO 操作讀取到內(nèi)存里。
”沒錯(cuò),不論讀寫操作都是在內(nèi)存上完成的,我們分別對(duì)比下內(nèi)存操作與磁盤操作的差異。
磁盤調(diào)用棧圖
內(nèi)存操作
內(nèi)存直接由 CPU 控制,也就是 CPU 內(nèi)部集成的內(nèi)存控制器,所以說內(nèi)存是直接與 CPU 對(duì)接,享受與 CPU 通信的最優(yōu)帶寬。
Redis 將數(shù)據(jù)存儲(chǔ)在內(nèi)存中,讀寫操作不會(huì)因?yàn)榇疟P的 IO 速度限制,所以速度飛一般的感覺!
最后以一張圖量化系統(tǒng)的各種延時(shí)時(shí)間(部分?jǐn)?shù)據(jù)引用 Brendan Gregg)
高效的數(shù)據(jù)結(jié)構(gòu)
65 哥:學(xué)習(xí) MySQL 的時(shí)候我知道為了提高檢索速度使用了 B+ Tree 數(shù)據(jù)結(jié)構(gòu),所以 Redis 速度快應(yīng)該也跟數(shù)據(jù)結(jié)構(gòu)有關(guān)。
”回答正確,這里所說的數(shù)據(jù)結(jié)構(gòu)并不是 Redis 提供給我們使用的 5 種數(shù)據(jù)類型:String、List、Hash、Set、SortedSet。
在 Redis 中,常用的 5 種數(shù)據(jù)類型和應(yīng)用場景如下:
- String: 緩存、計(jì)數(shù)器、分布式鎖等。
- List: 鏈表、隊(duì)列、微博關(guān)注人時(shí)間軸列表等。
- Hash: 用戶信息、Hash 表等。
- Set: 去重、贊、踩、共同好友等。
- Zset: 訪問量排行榜、點(diǎn)擊量排行榜等。
上面的應(yīng)該叫做 Redis 支持的數(shù)據(jù)類型,也就是數(shù)據(jù)的保存形式?!复a哥字節(jié)」要說的是針對(duì)這 5 種數(shù)據(jù)類型,底層都運(yùn)用了哪些高效的數(shù)據(jù)結(jié)構(gòu)來支持。
65 哥:為啥搞這么多數(shù)據(jù)結(jié)構(gòu)呢?
”當(dāng)然是為了追求速度,不同數(shù)據(jù)類型使用不同的數(shù)據(jù)結(jié)構(gòu)速度才得以提升。每種數(shù)據(jù)類型都有一種或者多種數(shù)據(jù)結(jié)構(gòu)來支撐,底層數(shù)據(jù)結(jié)構(gòu)有 6 種。
Redis hash 字典
Redis 整體就是一個(gè) 哈希表來保存所有的鍵值對(duì),無論數(shù)據(jù)類型是 5 種的任意一種。哈希表,本質(zhì)就是一個(gè)數(shù)組,每個(gè)元素被叫做哈希桶,不管什么數(shù)據(jù)類型,每個(gè)桶里面的 entry 保存著實(shí)際具體值的指針。
Redis 全局哈希表
整個(gè)數(shù)據(jù)庫就是一個(gè)全局哈希表,而哈希表的時(shí)間復(fù)雜度是 O(1),只需要計(jì)算每個(gè)鍵的哈希值,便知道對(duì)應(yīng)的哈希桶位置,定位桶里面的 entry 找到對(duì)應(yīng)數(shù)據(jù),這個(gè)也是 Redis 快的原因之一。
那 Hash 沖突怎么辦?
當(dāng)寫入 Redis 的數(shù)據(jù)越來越多的時(shí)候,哈希沖突不可避免,會(huì)出現(xiàn)不同的 key 計(jì)算出一樣的哈希值。
Redis 通過鏈?zhǔn)焦=鉀Q沖突:也就是同一個(gè) 桶里面的元素使用鏈表保存。但是當(dāng)鏈表過長就會(huì)導(dǎo)致查找性能變差可能,所以 Redis 為了追求快,使用了兩個(gè)全局哈希表。用于 rehash 操作,增加現(xiàn)有的哈希桶數(shù)量,減少哈希沖突。
開始默認(rèn)使用 hash 表 1 保存鍵值對(duì)數(shù)據(jù),哈希表 2 此刻沒有分配空間。當(dāng)數(shù)據(jù)越來多觸發(fā) rehash 操作,則執(zhí)行以下操作:
- 給 hash 表 2 分配更大的空間;
- 將 hash 表 1 的數(shù)據(jù)重新映射拷貝到 hash 表 2 中;
- 釋放 hash 表 1 的空間。
值得注意的是,將 hash 表 1 的數(shù)據(jù)重新映射到 hash 表 2 的過程中并不是一次性的,這樣會(huì)造成 Redis 阻塞,無法提供服務(wù)。
而是采用了漸進(jìn)式 rehash,每次處理客戶端請(qǐng)求的時(shí)候,先從 hash 表 1 中第一個(gè)索引開始,將這個(gè)位置的 所有數(shù)據(jù)拷貝到 hash 表 2 中,就這樣將 rehash 分散到多次請(qǐng)求過程中,避免耗時(shí)阻塞。
SDS 簡單動(dòng)態(tài)字符
65 哥:Redis 是用 C 語言實(shí)現(xiàn)的,為啥還重新搞一個(gè) SDS 動(dòng)態(tài)字符串呢?
”字符串結(jié)構(gòu)使用最廣泛,通常我們用于緩存登陸后的用戶信息,key = userId,value = 用戶信息 JSON 序列化成字符串。
C 語言中字符串的獲取 「MageByte」的長度,要從頭開始遍歷,直到 「\0」為止,Redis 作為唯快不破的男人是不能忍受的。
C 語言字符串結(jié)構(gòu)與 SDS 字符串結(jié)構(gòu)對(duì)比圖如下所示:
C 語言字符串與 SDS
SDS 與 C 字符串區(qū)別
O(1) 時(shí)間復(fù)雜度獲取字符串長度
C 語言字符串布吉路長度信息,需要遍歷整個(gè)字符串時(shí)間復(fù)雜度為 O(n),C 字符串遍歷時(shí)遇到 '\0' 時(shí)結(jié)束。
SDS 中 len 保存這字符串的長度,O(1) 時(shí)間復(fù)雜度。
空間預(yù)分配
SDS 被修改后,程序不僅會(huì)為 SDS 分配所需要的必須空間,還會(huì)分配額外的未使用空間。
分配規(guī)則如下:如果對(duì) SDS 修改后,len 的長度小于 1M,那么程序?qū)⒎峙浜?len 相同長度的未使用空間。舉個(gè)例子,如果 len=10,重新分配后,buf 的實(shí)際長度會(huì)變?yōu)?10(已使用空間)+10(額外空間)+1(空字符)=21。如果對(duì) SDS 修改后 len 長度大于 1M,那么程序?qū)⒎峙?1M 的未使用空間。
惰性空間釋放
當(dāng)對(duì) SDS 進(jìn)行縮短操作時(shí),程序并不會(huì)回收多余的內(nèi)存空間,而是使用 free 字段將這些字節(jié)數(shù)量記錄下來不釋放,后面如果需要 append 操作,則直接使用 free 中未使用的空間,減少了內(nèi)存的分配。
二進(jìn)制安全
在 Redis 中不僅可以存儲(chǔ) String 類型的數(shù)據(jù),也可能存儲(chǔ)一些二進(jìn)制數(shù)據(jù)。
二進(jìn)制數(shù)據(jù)并不是規(guī)則的字符串格式,其中會(huì)包含一些特殊的字符如 '\0',在 C 中遇到 '\0' 則表示字符串的結(jié)束,但在 SDS 中,標(biāo)志字符串結(jié)束的是 len 屬性。
zipList 壓縮列表
壓縮列表是 List 、hash、 sorted Set 三種數(shù)據(jù)類型底層實(shí)現(xiàn)之一。
當(dāng)一個(gè)列表只有少量數(shù)據(jù)的時(shí)候,并且每個(gè)列表項(xiàng)要么就是小整數(shù)值,要么就是長度比較短的字符串,那么 Redis 就會(huì)使用壓縮列表來做列表鍵的底層實(shí)現(xiàn)。
ziplist 是由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型的數(shù)據(jù)結(jié)構(gòu),ziplist 中可以包含多個(gè) entry 節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)可以存放整數(shù)或者字符串。
ziplist 在表頭有三個(gè)字段 zlbytes、zltail 和 zllen,分別表示列表占用字節(jié)數(shù)、列表尾的偏移量和列表中的 entry 個(gè)數(shù);壓縮列表在表尾還有一個(gè) zlend,表示列表結(jié)束。
- struct ziplist<T> {
- int32 zlbytes; // 整個(gè)壓縮列表占用字節(jié)數(shù)
- int32 zltail_offset; // 最后一個(gè)元素距離壓縮列表起始位置的偏移量,用于快速定位到最后一個(gè)節(jié)點(diǎn)
- int16 zllength; // 元素個(gè)數(shù)
- T[] entries; // 元素內(nèi)容列表,挨個(gè)挨個(gè)緊湊存儲(chǔ)
- int8 zlend; // 標(biāo)志壓縮列表的結(jié)束,值恒為 0xFF
- }
ziplist
如果我們要查找定位第一個(gè)元素和最后一個(gè)元素,可以通過表頭三個(gè)字段的長度直接定位,復(fù)雜度是 O(1)。而查找其他元素時(shí),就沒有這么高效了,只能逐個(gè)查找,此時(shí)的復(fù)雜度就是 O(N)
雙端列表
Redis List 數(shù)據(jù)類型通常被用于隊(duì)列、微博關(guān)注人時(shí)間軸列表等場景。不管是先進(jìn)先出的隊(duì)列,還是先進(jìn)后出的棧,雙端列表都很好的支持這些特性。
Redis 的鏈表實(shí)現(xiàn)的特性可以總結(jié)如下:
- 雙端:鏈表節(jié)點(diǎn)帶有 prev 和 next 指針,獲取某個(gè)節(jié)點(diǎn)的前置節(jié)點(diǎn)和后置節(jié)點(diǎn)的復(fù)雜度都是 O(1)。
- 無環(huán):表頭節(jié)點(diǎn)的 prev 指針和表尾節(jié)點(diǎn)的 next 指針都指向 NULL,對(duì)鏈表的訪問以 NULL 為終點(diǎn)。
- 帶表頭指針和表尾指針:通過 list 結(jié)構(gòu)的 head 指針和 tail 指針,程序獲取鏈表的表頭節(jié)點(diǎn)和表尾節(jié)點(diǎn)的復(fù)雜度為 O(1)。
- 帶鏈表長度計(jì)數(shù)器:程序使用 list 結(jié)構(gòu)的 len 屬性來對(duì) list 持有的鏈表節(jié)點(diǎn)進(jìn)行計(jì)數(shù),程序獲取鏈表中節(jié)點(diǎn)數(shù)量的復(fù)雜度為 O(1)。
- 多態(tài):鏈表節(jié)點(diǎn)使用 void* 指針來保存節(jié)點(diǎn)值,并且可以通過 list 結(jié)構(gòu)的 dup、free、match 三個(gè)屬性為節(jié)點(diǎn)值設(shè)置類型特定函數(shù),所以鏈表可以用于保存各種不同類型的值。
后續(xù)版本對(duì)列表數(shù)據(jù)結(jié)構(gòu)進(jìn)行了改造,使用 quicklist 代替了 ziplist 和 linkedlist。
quicklist 是 ziplist 和 linkedlist 的混合體,它將 linkedlist 按段切分,每一段使用 ziplist 來緊湊存儲(chǔ),多個(gè) ziplist 之間使用雙向指針串接起來。
這也是為何 Redis 快的原因,不放過任何一個(gè)可以提升性能的細(xì)節(jié)。
skipList 跳躍表
sorted set 類型的排序功能便是通過「跳躍列表」數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)。
跳躍表(skiplist)是一種有序數(shù)據(jù)結(jié)構(gòu),它通過在每個(gè)節(jié)點(diǎn)中維持多個(gè)指向其他節(jié)點(diǎn)的指針,從而達(dá)到快速訪問節(jié)點(diǎn)的目的。
跳躍表支持平均 O(logN)、最壞 O(N)復(fù)雜度的節(jié)點(diǎn)查找,還可以通過順序性操作來批量處理節(jié)點(diǎn)。
跳表在鏈表的基礎(chǔ)上,增加了多層級(jí)索引,通過索引位置的幾個(gè)跳轉(zhuǎn),實(shí)現(xiàn)數(shù)據(jù)的快速定位,如下圖所示:
跳躍表
當(dāng)需要查找 40 這個(gè)元素需要經(jīng)歷 三次查找。
整數(shù)數(shù)組(intset)
當(dāng)一個(gè)集合只包含整數(shù)值元素,并且這個(gè)集合的元素?cái)?shù)量不多時(shí),Redis 就會(huì)使用整數(shù)集合作為集合鍵的底層實(shí)現(xiàn)。結(jié)構(gòu)如下:
- typedef struct intset{
- //編碼方式
- uint32_t encoding;
- //集合包含的元素?cái)?shù)量
- uint32_t length;
- //保存元素的數(shù)組
- int8_t contents[];
- }intset;
contents 數(shù)組是整數(shù)集合的底層實(shí)現(xiàn):整數(shù)集合的每個(gè)元素都是 contents 數(shù)組的一個(gè)數(shù)組項(xiàng)(item),各個(gè)項(xiàng)在數(shù)組中按值的大小從小到大有序地排列,并且數(shù)組中不包含任何重復(fù)項(xiàng)。length 屬性記錄了整數(shù)集合包含的元素?cái)?shù)量,也即是 contents 數(shù)組的長度。
合理的數(shù)據(jù)編碼
Redis 使用對(duì)象(redisObject)來表示數(shù)據(jù)庫中的鍵值,當(dāng)我們在 Redis 中創(chuàng)建一個(gè)鍵值對(duì)時(shí),至少創(chuàng)建兩個(gè)對(duì)象,一個(gè)對(duì)象是用做鍵值對(duì)的鍵對(duì)象,另一個(gè)是鍵值對(duì)的值對(duì)象。
例如我們執(zhí)行 SET MSG XXX 時(shí),鍵值對(duì)的鍵是一個(gè)包含了字符串“MSG“的對(duì)象,鍵值對(duì)的值對(duì)象是包含字符串"XXX"的對(duì)象。
redisObject
- typedef struct redisObject{
- //類型
- unsigned type:4;
- //編碼
- unsigned encoding:4;
- //指向底層數(shù)據(jù)結(jié)構(gòu)的指針
- void *ptr;
- //...
- }robj;
其中 type 字段記錄了對(duì)象的類型,包含字符串對(duì)象、列表對(duì)象、哈希對(duì)象、集合對(duì)象、有序集合對(duì)象。
對(duì)于每一種數(shù)據(jù)類型來說,底層的支持可能是多種數(shù)據(jù)結(jié)構(gòu),什么時(shí)候使用哪種數(shù)據(jù)結(jié)構(gòu),這就涉及到了編碼轉(zhuǎn)化的問題。
那我們就來看看,不同的數(shù)據(jù)類型是如何進(jìn)行編碼轉(zhuǎn)化的:
String:存儲(chǔ)數(shù)字的話,采用 int 類型的編碼,如果是非數(shù)字的話,采用 raw 編碼;
List:List 對(duì)象的編碼可以是 ziplist 或 linkedlist,字符串長度 < 64 字節(jié)且元素個(gè)數(shù) < 512 使用 ziplist 編碼,否則轉(zhuǎn)化為 linkedlist 編碼;
注意:這兩個(gè)條件是可以修改的,在 redis.conf 中:
list-max-ziplist-entries 512list-max-ziplist-value 64
Hash:Hash 對(duì)象的編碼可以是 ziplist 或 hashtable。
- 當(dāng) Hash 對(duì)象同時(shí)滿足以下兩個(gè)條件時(shí),Hash 對(duì)象采用 ziplist 編碼:
- Hash 對(duì)象保存的所有鍵值對(duì)的鍵和值的字符串長度均小于 64 字節(jié)。
Hash 對(duì)象保存的鍵值對(duì)數(shù)量小于 512 個(gè)。
否則就是 hashtable 編碼。
Set:Set 對(duì)象的編碼可以是 intset 或 hashtable,intset 編碼的對(duì)象使用整數(shù)集合作為底層實(shí)現(xiàn),把所有元素都保存在一個(gè)整數(shù)集合里面。
保存元素為整數(shù)且元素個(gè)數(shù)小于一定范圍使用 intset 編碼,任意條件不滿足,則使用 hashtable 編碼;
Zset:Zset 對(duì)象的編碼可以是 ziplist 或 zkiplist,當(dāng)采用 ziplist 編碼存儲(chǔ)時(shí),每個(gè)集合元素使用兩個(gè)緊挨在一起的壓縮列表來存儲(chǔ)。
Ziplist 壓縮列表第一個(gè)節(jié)點(diǎn)存儲(chǔ)元素的成員,第二個(gè)節(jié)點(diǎn)存儲(chǔ)元素的分值,并且按分值大小從小到大有序排列。
當(dāng) Zset 對(duì)象同時(shí)滿足一下兩個(gè)條件時(shí),采用 ziplist 編碼:
- Zset 保存的元素個(gè)數(shù)小于 128。
- Zset 元素的成員長度都小于 64 字節(jié)。
如果不滿足以上條件的任意一個(gè),ziplist 就會(huì)轉(zhuǎn)化為 zkiplist 編碼。注意:這兩個(gè)條件是可以修改的,在 redis.conf 中:
- zset-max-ziplist-entries 128
- zset-max-ziplist-value 64
單線程模型“
65 哥:為什么 Redis 是單線程的而不用多線程并行執(zhí)行充分利用 CPU 呢?
”我們要明確的是:Redis 的單線程指的是 Redis 的網(wǎng)絡(luò) IO 以及鍵值對(duì)指令讀寫是由一個(gè)線程來執(zhí)行的。 對(duì)于 Redis 的持久化、集群數(shù)據(jù)同步、異步刪除等都是其他線程執(zhí)行。
至于為啥用單線程,我們先了解多線程有什么缺點(diǎn)。
多線程的弊端
使用多線程,通??梢栽黾酉到y(tǒng)吞吐量,充分利用 CPU 資源。
但是,使用多線程后,沒有良好的系統(tǒng)設(shè)計(jì),可能會(huì)出現(xiàn)如下圖所示的場景,增加了線程數(shù)量,前期吞吐量會(huì)增加,再進(jìn)一步新增線程的時(shí)候,系統(tǒng)吞吐量幾乎不再新增,甚至?xí)陆?
線程數(shù)與吞吐量
在運(yùn)行每個(gè)任務(wù)之前,CPU 需要知道任務(wù)在何處加載并開始運(yùn)行。也就是說,系統(tǒng)需要幫助它預(yù)先設(shè)置 CPU 寄存器和程序計(jì)數(shù)器,這稱為 CPU 上下文。
這些保存的上下文存儲(chǔ)在系統(tǒng)內(nèi)核中,并在重新計(jì)劃任務(wù)時(shí)再次加載。這樣,任務(wù)的原始狀態(tài)將不會(huì)受到影響,并且該任務(wù)將看起來正在連續(xù)運(yùn)行。
切換上下文時(shí),我們需要完成一系列工作,這是非常消耗資源的操作。
另外,當(dāng)多線程并行修改共享數(shù)據(jù)的時(shí)候,為了保證數(shù)據(jù)正確,需要加鎖機(jī)制就會(huì)帶來額外的性能開銷,面臨的共享資源的并發(fā)訪問控制問題。
引入多線程開發(fā),就需要使用同步原語來保護(hù)共享資源的并發(fā)讀寫,增加代碼復(fù)雜度和調(diào)試難度。
單線程又什么好處?
- 不會(huì)因?yàn)榫€程創(chuàng)建導(dǎo)致的性能消耗;
- 避免上下文切換引起的 CPU 消耗,沒有多線程切換的開銷;
- 避免了線程之間的競爭問題,比如添加鎖、釋放鎖、死鎖等,不需要考慮各種鎖問題。
- 代碼更清晰,處理邏輯簡單。
單線程是否沒有充分利用 CPU 資源呢?
官方答案:因?yàn)?Redis 是基于內(nèi)存的操作,CPU 不是 Redis 的瓶頸,Redis 的瓶頸最有可能是機(jī)器內(nèi)存的大小或者網(wǎng)絡(luò)帶寬。既然單線程容易實(shí)現(xiàn),而且 CPU 不會(huì)成為瓶頸,那就順理成章地采用單線程的方案了。原文地址:https://redis.io/topics/faq。
I/O 多路復(fù)用模型
Redis 采用 I/O 多路復(fù)用技術(shù),并發(fā)處理連接。采用了 epoll + 自己實(shí)現(xiàn)的簡單的事件框架。epoll 中的讀、寫、關(guān)閉、連接都轉(zhuǎn)化成了事件,然后利用 epoll 的多路復(fù)用特性,絕不在 IO 上浪費(fèi)一點(diǎn)時(shí)間。
65 哥:那什么是 I/O 多路復(fù)用呢?
”在解釋 IO 多慮復(fù)用之前我們先了解下基本 IO 操作會(huì)經(jīng)歷什么。
基本 IO 模型
一個(gè)基本的網(wǎng)絡(luò) IO 模型,當(dāng)處理 get 請(qǐng)求,會(huì)經(jīng)歷以下過程:
- 和客戶端建立建立 accept;
- 從 socket 種讀取請(qǐng)求 recv;
- 解析客戶端發(fā)送的請(qǐng)求 parse;
- 執(zhí)行 get 指令;
- 響應(yīng)客戶端數(shù)據(jù),也就是 向 socket 寫回?cái)?shù)據(jù)。
其中,bind/listen、accept、recv、parse 和 send 屬于網(wǎng)絡(luò) IO 處理,而 get 屬于鍵值數(shù)據(jù)操作。既然 Redis 是單線程,那么,最基本的一種實(shí)現(xiàn)是在一個(gè)線程中依次執(zhí)行上面說的這些操作。
關(guān)鍵點(diǎn)就是 accept 和 recv 會(huì)出現(xiàn)阻塞,當(dāng) Redis 監(jiān)聽到一個(gè)客戶端有連接請(qǐng)求,但一直未能成功建立起連接時(shí),會(huì)阻塞在 accept() 函數(shù)這里,導(dǎo)致其他客戶端無法和 Redis 建立連接。
類似的,當(dāng) Redis 通過 recv() 從一個(gè)客戶端讀取數(shù)據(jù)時(shí),如果數(shù)據(jù)一直沒有到達(dá),Redis 也會(huì)一直阻塞在 recv()。
阻塞的原因由于使用傳統(tǒng)阻塞 IO ,也就是在執(zhí)行 read、accept 、recv 等網(wǎng)絡(luò)操作會(huì)一直阻塞等待。如下圖所示:
阻塞IO
IO 多路復(fù)用
多路指的是多個(gè) socket 連接,復(fù)用指的是復(fù)用一個(gè)線程。多路復(fù)用主要有三種技術(shù):select,poll,epoll。epoll 是最新的也是目前最好的多路復(fù)用技術(shù)。
它的基本原理是,內(nèi)核不是監(jiān)視應(yīng)用程序本身的連接,而是監(jiān)視應(yīng)用程序的文件描述符。
當(dāng)客戶端運(yùn)行時(shí),它將生成具有不同事件類型的套接字。在服務(wù)器端,I / O 多路復(fù)用程序(I / O 多路復(fù)用模塊)會(huì)將消息放入隊(duì)列(也就是 下圖的 I/O 多路復(fù)用程序的 socket 隊(duì)列),然后通過文件事件分派器將其轉(zhuǎn)發(fā)到不同的事件處理器。
簡單來說:Redis 單線程情況下,內(nèi)核會(huì)一直監(jiān)聽 socket 上的連接請(qǐng)求或者數(shù)據(jù)請(qǐng)求,一旦有請(qǐng)求到達(dá)就交給 Redis 線程處理,這就實(shí)現(xiàn)了一個(gè) Redis 線程處理多個(gè) IO 流的效果。
select/epoll 提供了基于事件的回調(diào)機(jī)制,即針對(duì)不同事件的發(fā)生,調(diào)用相應(yīng)的事件處理器。所以 Redis 一直在處理事件,提升 Redis 的響應(yīng)性能。
高性能 IO 多路復(fù)用
Redis 線程不會(huì)阻塞在某一個(gè)特定的監(jiān)聽或已連接套接字上,也就是說,不會(huì)阻塞在某一個(gè)特定的客戶端請(qǐng)求處理上。正因?yàn)榇耍琑edis 可以同時(shí)和多個(gè)客戶端連接并處理請(qǐng)求,從而提升并發(fā)性。
唯快不破的原理總結(jié)“
65 哥:學(xué)完之后我終于知道 Redis 為何快的本質(zhì)原因了,「碼哥」你別說話,我來總結(jié)!一會(huì)我再點(diǎn)贊和分享這篇文章,讓更多人知道 Redis 快的核心原理。
”純內(nèi)存操作,一般都是簡單的存取操作,線程占用的時(shí)間很多,時(shí)間的花費(fèi)主要集中在 IO 上,所以讀取速度快。
整個(gè) Redis 就是一個(gè)全局 哈希表,他的時(shí)間復(fù)雜度是 O(1),而且為了防止哈希沖突導(dǎo)致鏈表過長,Redis 會(huì)執(zhí)行 rehash 操作,擴(kuò)充 哈希桶數(shù)量,減少哈希沖突。并且防止一次性 重新映射數(shù)據(jù)過大導(dǎo)致線程阻塞,采用 漸進(jìn)式 rehash。巧妙的將一次性拷貝分?jǐn)偟蕉啻握?qǐng)求過程后總,避免阻塞。
Redis 使用的是非阻塞 IO:IO 多路復(fù)用,使用了單線程來輪詢描述符,將數(shù)據(jù)庫的開、關(guān)、讀、寫都轉(zhuǎn)換成了事件,Redis 采用自己實(shí)現(xiàn)的事件分離器,效率比較高。
采用單線程模型,保證了每個(gè)操作的原子性,也減少了線程的上下文切換和競爭。
Redis 全程使用 hash 結(jié)構(gòu),讀取速度快,還有一些特殊的數(shù)據(jù)結(jié)構(gòu),對(duì)數(shù)據(jù)存儲(chǔ)進(jìn)行了優(yōu)化,如壓縮表,對(duì)短數(shù)據(jù)進(jìn)行壓縮存儲(chǔ),再如,跳表,使用有序的數(shù)據(jù)結(jié)構(gòu)加快讀取的速度。
根據(jù)實(shí)際存儲(chǔ)的數(shù)據(jù)類型選擇不同編碼
本文轉(zhuǎn)載自微信公眾號(hào)「碼哥字節(jié)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼哥字節(jié)公眾號(hào)。