Redis 如何保證高效的查詢效率
為什么 Redis 比較快
Redis 中的查詢速度為什么那么快呢乔煞?
1、因?yàn)樗莾?nèi)存數(shù)據(jù)庫(kù)柒室;
2渡贾、歸功于它的數(shù)據(jù)結(jié)構(gòu);
3雄右、Redis 中是單線程空骚;
4纺讲、Redis 中使用了多路復(fù)用。
Redis 中的數(shù)據(jù)結(jié)構(gòu)
這里借用一張來(lái)自[Redis核心技術(shù)與實(shí)戰(zhàn)] Redis 中數(shù)據(jù)結(jié)構(gòu)和底層結(jié)構(gòu)的對(duì)應(yīng)圖片
1府怯、簡(jiǎn)單動(dòng)態(tài)字符串
Redis 中并沒(méi)有使用 C 中 char 來(lái)表示字符串刻诊,而是引入了 簡(jiǎn)單動(dòng)態(tài)字符串(Simple Dynamic Strings,SDS)來(lái)存儲(chǔ)字符串和整型數(shù)據(jù)牺丙。那么 SDS 對(duì)比傳統(tǒng)的字符串有什么優(yōu)點(diǎn)呢则涯?
先來(lái)看下 SDS 的結(jié)構(gòu)
struct sdshdr {
// 記錄 buf 數(shù)組中已使用字節(jié)的數(shù)量
// 等于 SDS 保存字符串的長(zhǎng)度,不包含'\0'
long len;
// 記錄buf數(shù)組中未使用字節(jié)的數(shù)量
long free;
// 字節(jié)數(shù)組冲簿,用于保存字符串
char buf[];
};
舉個(gè)栗子:
使用 SDS 存儲(chǔ)了一個(gè)字符串 hello,對(duì)應(yīng)的 len 就是5粟判,同時(shí)也申請(qǐng)了5個(gè)為未使用的空間,所以 free 就是5峦剔。
在 3.2 版本后档礁,sds 會(huì)根據(jù)字符串實(shí)際的長(zhǎng)度,選擇不同的數(shù)據(jù)結(jié)構(gòu)吝沫,以更好的提升內(nèi)存效率呻澜。當(dāng)前 sdshdr 結(jié)構(gòu)分為 5 種子類型,分別為 sdshdr5惨险、sdshdr8羹幸、sdshdr16、sdshdr32辫愉、sdshdr64
栅受。其中 sdshdr5 只有 flags 和 buf 字段,其他幾種類型的 len 和 alloc 采用從 uint8_t 到 uint64_t 的不同類型恭朗,以節(jié)省內(nèi)存空間屏镊。
SDS 對(duì)比 c 字符串的優(yōu)勢(shì)
SDS可以常數(shù)級(jí)別獲取字符串的長(zhǎng)度
因?yàn)榻Y(jié)構(gòu)里面已經(jīng)記錄了字符串的長(zhǎng)度,所以獲取字符串的長(zhǎng)度復(fù)雜度為O(1)痰腮,c 中字符串沒(méi)記錄長(zhǎng)度溜族,需要遍歷整個(gè)長(zhǎng)度再姑,復(fù)雜度為O(N)坝辫。
杜絕緩沖區(qū)溢出
如果在修改字符的時(shí)候创橄,沒(méi)有分配足夠的內(nèi)存大小,就很容易造成緩存溢出虫腋,內(nèi)存越界。
strcat 函數(shù)常見(jiàn)的錯(cuò)誤就是數(shù)組越界稀余,即兩個(gè)字符串連接后悦冀,長(zhǎng)度超過(guò)第一個(gè)字符串?dāng)?shù)組定義的長(zhǎng)度,導(dǎo)致越界睛琳。
SDS 中的空間分配策略可以杜絕這種情況盒蟆,當(dāng)對(duì) SDS 進(jìn)行修改時(shí)踏烙,API 會(huì)檢查 SDS 的空間是否滿足修改所需的要求,如果不滿足的話历等,API 會(huì)自動(dòng)將 SDS 的空間擴(kuò)展至執(zhí)行修改所需的大小讨惩,然后才執(zhí)行實(shí)際的修改操作『停空間的申請(qǐng)是自動(dòng)完成的荐捻,所以就避免了緩存溢出。
減少修改字符串時(shí)帶來(lái)的內(nèi)存分配次數(shù)
對(duì)于 C 字符串來(lái)說(shuō)寡夹,如果修改字符串的長(zhǎng)度处面,都需要重新執(zhí)行內(nèi)存分配操作;但是對(duì)于 Redis 數(shù)據(jù)庫(kù)來(lái)說(shuō)菩掏,如果頻繁執(zhí)行內(nèi)存分配/釋放操作魂角,必然會(huì)對(duì)性能產(chǎn)生一定影響。為了避免 C 字符串的缺陷智绸,SDS 采用了空間預(yù)分配和惰性空間釋放兩種優(yōu)化策略野揪。
空間預(yù)分配
空間預(yù)分配用于優(yōu)化 SDS 的字符串增長(zhǎng)操作,當(dāng) SDS 的 api 對(duì) SDS 進(jìn)行修改瞧栗,同時(shí)需要進(jìn)行空間擴(kuò)展的時(shí)候斯稳,除了會(huì)給 SDS 分配修改需要的空間,同時(shí)還會(huì)給 SDS 分配額外的未使用空間沼溜。
1平挑、如果對(duì) SDS 修改之后,SDS 的長(zhǎng)度小于1MB
,那么程序分配和 len 同樣大小的未使用空間系草,也就是這時(shí)候 SDS 中的 len 和 free 長(zhǎng)度相同通熄;
2、如果對(duì) SDS 修改之后找都,SDS 的長(zhǎng)度大于等于1MB
,那么程序分配1MB
的未使用空間唇辨。
在對(duì) SDS 空間進(jìn)行擴(kuò)展的時(shí)候,首先會(huì)判斷未使用空間的大小是否能滿足要求能耻,如果足夠赏枚,就不用在進(jìn)行內(nèi)存分配了,這樣能夠減少內(nèi)存的重新分配的次數(shù)晓猛。
惰性空間釋放
惰性空間釋放用于優(yōu)化 SDS 字符串的縮短操作饿幅,當(dāng) SDS 的 API 需要縮短 SDS 保護(hù)的字符串時(shí),程序并不會(huì)立即使用內(nèi)存重分配來(lái)回收縮短后多出來(lái)的內(nèi)存戒职,而是使用 free 屬性將這些字節(jié)的數(shù)量記錄起來(lái)栗恩,等待之后的重新使用。
二進(jìn)制安全
對(duì)于 C 字符串來(lái)說(shuō)洪燥,字符串中不能包含空字符磕秤,否則最先被程序讀入的空字符串被誤認(rèn)為是字符串結(jié)尾乳乌,這使得 C 字符串只能保存文本數(shù)據(jù),而不能保存圖片市咆、音視頻等二進(jìn)制文件汉操。對(duì)于 SDS 來(lái)說(shuō),所有 SDS 都會(huì)以處理二進(jìn)制的方式來(lái)處理 SDS 保存在 buf 數(shù)組中的內(nèi)容蒙兰,程序不會(huì)對(duì)里面的內(nèi)容做任何限制磷瘤。
兼容部分C字符串函數(shù)
SDS 末尾設(shè)置空字符的操作使得它可以和部分 C 字符串函數(shù)兼容。
2癞己、鏈表
鏈表是一種很常見(jiàn)的數(shù)據(jù)結(jié)構(gòu)膀斋,在很多高級(jí)語(yǔ)言中都能看到,Redis 中的 list 就用到了雙向鏈表痹雅,當(dāng)一個(gè)列表鍵包含了數(shù)量比較多的元素仰担,或者是列表中包含的元素都是比較長(zhǎng)的字符串的時(shí),Redis 就會(huì)使用鏈表作為 list 的底層實(shí)現(xiàn)绩社。
這里雙向鏈表不展開討論了摔蓝。
3、字典
redisDb 主要包括 2 個(gè)核心 dict 字典愉耙、3 個(gè)非核心 dict 字典贮尉、dbID 和其他輔助屬性。2 個(gè)核心 dict 包括一個(gè) dict 主字典和一個(gè) expires 過(guò)期字典朴沿。主 dict 字典用來(lái)存儲(chǔ)當(dāng)前 DB 中的所有數(shù)據(jù)猜谚,它將 key 和各種數(shù)據(jù)類型的 value 關(guān)聯(lián)起來(lái),該 dict 也稱 key space赌渣。過(guò)期字典用來(lái)存儲(chǔ)過(guò)期時(shí)間 key魏铅,存的是 key 與過(guò)期時(shí)間的映射。日常的數(shù)據(jù)存儲(chǔ)和訪問(wèn)基本都會(huì)訪問(wèn)到 redisDb 中的這兩個(gè) dict坚芜。
所以對(duì)于任何類型的數(shù)據(jù)查詢都需要經(jīng)過(guò)一次 dict 的查詢览芳,也就是 hash 的過(guò)程。通過(guò)這個(gè) dict 將 key 映射到對(duì)應(yīng)的數(shù)據(jù)類型的 value 中鸿竖。
3 個(gè)非核心 dict 包括一個(gè)字段名叫 blocking_keys 的阻塞 dict沧竟,一個(gè)字段名叫 ready_keys 的解除阻塞 dict,還有一個(gè)是字段名叫 watched_keys 的 watch 監(jiān)控 dict缚忧。
Redis 的字典使用哈希表作為底層實(shí)現(xiàn)悟泵,一個(gè)哈希表里面可以有多個(gè)哈希節(jié)點(diǎn),每個(gè)哈希表節(jié)點(diǎn)就保存了字典中的一個(gè)鍵值對(duì)闪水。
使用哈希表就難免遇到哈希碰撞魁袜,兩個(gè)key的哈希值和哈希桶計(jì)算對(duì)應(yīng)關(guān)系時(shí),正好落在了同一個(gè)哈希桶中敦第。
Redis 解決哈希沖突的方式峰弹,就是鏈?zhǔn)焦!f準(zhǔn)焦R埠苋菀桌斫馕吖褪侵竿粋€(gè)哈希桶中的多個(gè)元素用一個(gè)鏈表來(lái)保存鞠呈,它們之間依次用指針連接。
隨著寫入的消息越來(lái)越多右钾,哈希沖突的幾率也隨之升高蚁吝,這時(shí)候就需要對(duì)哈希表進(jìn)行擴(kuò)容,Redis 中的擴(kuò)容使用的是漸進(jìn)式rehash舀射。
其實(shí)窘茁,為了使 rehash 操作更高效,Redis 默認(rèn)使用了兩個(gè)全局哈希表:哈希表1和哈希表2脆烟。一開始山林,當(dāng)你剛插入數(shù)據(jù)時(shí),默認(rèn)使用哈希表1邢羔,此時(shí)的哈希表2并沒(méi)有被分配空間驼抹。隨著數(shù)據(jù)逐步增多,Redis 開始執(zhí)行 rehash拜鹤,這個(gè)過(guò)程分為三步:
1框冀、給哈希表2分配更大的空間,例如是當(dāng)前哈希表1大小的兩倍敏簿;
2明也、把哈希表1中的數(shù)據(jù)重新映射并拷貝到哈希表2中;
3惯裕、釋放哈希表1的空間温数。
當(dāng)然數(shù)據(jù)很大的話,一次遷移所有的數(shù)據(jù)轻猖,顯然是不合理的帆吻,會(huì)造成Redis線程阻塞,無(wú)法服務(wù)其他請(qǐng)求咙边。這里 Redis 使用的是漸進(jìn)式 rehash猜煮。
在 rehash 期間,每次執(zhí)行添加败许,刪除王带,查找或者更新操作時(shí),除了對(duì)命令本身的處理市殷,還會(huì)順帶將哈希表1中的數(shù)據(jù)拷貝到哈希表2中愕撰。從索引0開始,每執(zhí)行一次操作命令,拷貝一個(gè)索引位置的數(shù)據(jù)搞挣。
在進(jìn)行 rehash 期間带迟,刪除,查找或者更新操作都會(huì)在兩個(gè)哈希表中執(zhí)行囱桨,添加操作就直接添加到哈希表2中了仓犬。查找會(huì)把兩個(gè)哈希表都找一遍,直到找到或者兩個(gè)都找不到舍肠。
4搀继、跳表
其中 Redis 中的有序集合就是使用跳表來(lái)實(shí)現(xiàn)的
對(duì)于一個(gè)單鏈表來(lái)講,即便鏈表中存儲(chǔ)的數(shù)據(jù)是有序的翠语,如果我們要想在其中查找某個(gè)數(shù)據(jù)叽躯,也只能從頭到尾遍歷鏈表。這樣查找效率就會(huì)很低肌括,時(shí)間復(fù)雜度會(huì)很高点骑,是O(n)。
鏈表加多級(jí)索引的結(jié)構(gòu)们童,就是跳表,跳表查詢的時(shí)間復(fù)雜度是O(logn)
畔况。通過(guò)在每個(gè)節(jié)點(diǎn)中維持多個(gè)指向其他節(jié)點(diǎn)的指針,從而實(shí)現(xiàn)快速訪問(wèn)的節(jié)點(diǎn)的目的慧库。
5跷跪、整數(shù)數(shù)組
整數(shù)集合(intset)是集合鍵的底層實(shí)現(xiàn)之一,當(dāng)一個(gè)集合只包含整數(shù)值元素齐板,并且這個(gè)集合的元素?cái)?shù)量不多時(shí)吵瞻,Redis 就會(huì)使用整數(shù)集合作為集合鍵的底層實(shí)現(xiàn)。是 Redis 用戶保存整數(shù)值的集合抽象數(shù)據(jù)結(jié)構(gòu)甘磨,可以保存的類型是int16_t,int32_t,int64_t
的整數(shù)值橡羞,并且保證集合中不會(huì)出現(xiàn)重復(fù)的元素。
typedef struct intset{
// 編碼方法济舆,指定當(dāng)前存儲(chǔ)的是 16 位卿泽,32 位,還是 64 位的整數(shù)
int32 encoding;
// 集合中的元素?cái)?shù)量
int32 length;
// 保存元素的數(shù)組
int<T> contents;
}
這樣看滋觉,這個(gè)集合倒也沒(méi)有什么特殊的點(diǎn)签夭。這時(shí)候需要來(lái)看下整數(shù)集合的升級(jí)。
每當(dāng)一個(gè)整數(shù)被添加到整數(shù)集合時(shí)椎侠,首先需要判斷下 新元素的類型和集合中現(xiàn)有元素類型的長(zhǎng)短第租,如果新元素是一個(gè)32位的數(shù)字,現(xiàn)有集合類型是16位的我纪,那么就需要將整數(shù)集合進(jìn)行升級(jí)慎宾,然后才能將新的元素加入進(jìn)來(lái)丐吓。
這樣做的優(yōu)點(diǎn):
1、提升整數(shù)集合的靈活性趟据,可以隨意的添加整數(shù)券犁,而不用關(guān)心整數(shù)的類型;
2之宿、可以盡可能的節(jié)約內(nèi)存族操。
缺點(diǎn):
不支持降級(jí),一旦對(duì)數(shù)組進(jìn)行了升級(jí)比被,就會(huì)一直保持升級(jí)后的狀態(tài)。
6泼舱、壓縮列表
壓縮列表(ziplist)是列表鍵和哈希鍵的底層實(shí)現(xiàn)之一等缀。當(dāng)一個(gè)元素只包含少量的列表項(xiàng),并且每個(gè)列表項(xiàng)是小整數(shù)值或者是長(zhǎng)度比較短的字符串,redis 就會(huì)使用壓縮列表來(lái)作為列表鍵的底層實(shí)現(xiàn)娇昙。
壓縮列表(ziplist)的目的是為了節(jié)約內(nèi)存尺迂,通過(guò)一片連續(xù)的內(nèi)存空間來(lái)存儲(chǔ)數(shù)據(jù)。這樣看起來(lái)好像和數(shù)組很像冒掌,數(shù)組中每個(gè)節(jié)點(diǎn)的內(nèi)存大小是一樣的噪裕,對(duì)于壓縮列表,每個(gè)節(jié)點(diǎn)的內(nèi)存大小是不同的股毫,每個(gè)節(jié)點(diǎn)可以保存字節(jié)數(shù)組或一個(gè)整數(shù)值膳音。通過(guò)可變的節(jié)點(diǎn)內(nèi)存大小,來(lái)節(jié)約內(nèi)存的使用铃诬。
ziplist 的結(jié)構(gòu):
1祭陷、zlbytes : 是壓縮列表所占用的總內(nèi)存字節(jié)數(shù);
2、Zltail : 尾節(jié)點(diǎn)到起始位置的字節(jié)數(shù)趣席,(目的是為了直接定位到尾節(jié)點(diǎn)兵志,方便反向查詢);
3、Zllen : 總共包含的節(jié)點(diǎn)/內(nèi)存塊數(shù)宣肚,也就是壓縮列表的節(jié)點(diǎn)數(shù)量;
4想罕、Entry : 是 ziplist 保存的各個(gè)數(shù)據(jù)節(jié)點(diǎn),這些數(shù)據(jù)點(diǎn)長(zhǎng)度隨意;
5霉涨、Zlend : 是一個(gè)魔數(shù) 255按价,用來(lái)標(biāo)記壓縮列表的結(jié)束。
由于 ziplist 是連續(xù)緊湊存儲(chǔ)嵌纲,沒(méi)有冗余空間俘枫,所以插入新的元素需要 realloc 擴(kuò)展內(nèi)存,所以如果 ziplist 占用空間太大逮走,realloc 重新分配內(nèi)存和拷貝的開銷就會(huì)很大鸠蚪,所以 ziplist 不適合存儲(chǔ)過(guò)多元素,也不適合存儲(chǔ)過(guò)大的字符串。
因此只有在元素?cái)?shù)和 value 數(shù)都不大的時(shí)候茅信,ziplist 才作為 hash 和 zset 的內(nèi)部數(shù)據(jù)結(jié)構(gòu)盾舌。其中 hash 使用 ziplist 作為內(nèi)部數(shù)據(jù)結(jié)構(gòu)的限制時(shí),元素?cái)?shù)默認(rèn)不超過(guò) 512 個(gè)蘸鲸,value 值默認(rèn)不超過(guò) 64 字節(jié)妖谴。可以通過(guò)修改配置來(lái)調(diào)整 hash_max_ziplist_entries 酌摇、hash_max_ziplist_value
這兩個(gè)閥值的大小膝舅。
zset 有序集合,使用 ziplist 作為內(nèi)部數(shù)據(jù)結(jié)構(gòu)的限制元素?cái)?shù)默認(rèn)不超過(guò) 128 個(gè)窑多,value 值默認(rèn)不超過(guò) 64 字節(jié)仍稀。可以通過(guò)修改配置來(lái)調(diào)整 zset_max_ziplist_entries 和 zset_max_ziplist_value 這兩個(gè)閥值的大小埂息。
在壓縮列表中技潘,如果我們要查找定位第一個(gè)元素和最后一個(gè)元素,可以通過(guò)表頭三個(gè)字段的長(zhǎng)度直接定位千康,復(fù)雜度是O(1)享幽。而查找其他元素時(shí),就沒(méi)有這么高效了拾弃,只能逐個(gè)查找值桩,此時(shí)的復(fù)雜度就是O(N)了。
連鎖更新
壓縮列表 ziplist 的存儲(chǔ)節(jié)點(diǎn) Entry 數(shù)據(jù)節(jié)點(diǎn)的結(jié)構(gòu):
1砸彬、previous_entry_length : 記錄了前一個(gè)節(jié)點(diǎn)的長(zhǎng)度
如果前一節(jié)點(diǎn)的長(zhǎng)度小于 254 字節(jié)颠毙, 那么 previous_entry_length 屬性的長(zhǎng)度為 1 字節(jié): 前一節(jié)點(diǎn)的長(zhǎng)度就保存在這一個(gè)字節(jié)里面;
如果前一節(jié)點(diǎn)的長(zhǎng)度大于等于 254 字節(jié)砂碉, 那么 previous_entry_length 屬性的長(zhǎng)度為 5 字節(jié): 其中屬性的第一字節(jié)會(huì)被設(shè)置為 0xFE (十進(jìn)制值 254)蛀蜜, 而之后的四個(gè)字節(jié)則用于保存前一節(jié)點(diǎn)的長(zhǎng)度。
2增蹭、encoding : 記錄了節(jié)點(diǎn)的 content 屬性所保存數(shù)據(jù)的類型以及長(zhǎng)度
3滴某、content : 節(jié)點(diǎn)的 content 屬性負(fù)責(zé)保存節(jié)點(diǎn)的值, 節(jié)點(diǎn)值可以是一個(gè)字節(jié)數(shù)組或者整數(shù)滋迈, 值的類型和長(zhǎng)度由節(jié)點(diǎn)的 encoding 屬性決定霎奢。
舉個(gè)栗子:
如果壓縮列表中每個(gè)節(jié)點(diǎn)的長(zhǎng)度都是250,因?yàn)槭切∮?54饼灿,所以每個(gè)節(jié)點(diǎn)中的 previous_entry_length 長(zhǎng)度1字節(jié)就能夠保存了幕侠。
這時(shí)候,在頭部節(jié)點(diǎn)插入了一個(gè)新的元素 entryNew碍彭,然后長(zhǎng)度是大于254晤硕,那么后面的節(jié)點(diǎn)中 entry1 的 previous_entry_length 長(zhǎng)度為1字節(jié)悼潭,就不能保存了,需要擴(kuò)容成5字節(jié)舞箍,然后 entry1 節(jié)點(diǎn)進(jìn)行擴(kuò)容了舰褪,變成了254,所以后面的節(jié)點(diǎn)也就需要一次擴(kuò)容疏橄,這就引發(fā)一連串的擴(kuò)容占拍。也就是連鎖更新。
為什么單線程還能很快
Redis 是單線程捎迫,主要是指 Redis 的網(wǎng)絡(luò)IO和鍵值對(duì)讀寫是由一個(gè)線程來(lái)完成的晃酒,這也是 Redis 對(duì)外提供鍵值存儲(chǔ)服務(wù)的主要流程。
多線程必然會(huì)面臨對(duì)于共享資源的訪問(wèn)立砸,這時(shí)候通常的做法就是加鎖掖疮,雖然是多線程,這時(shí)候就會(huì)變成串行的訪問(wèn)颗祝。也就是多線程編程模式會(huì)面臨的共享資源的并發(fā)訪問(wèn)控制問(wèn)題。
同時(shí)多線程也會(huì)引入同步原語(yǔ)來(lái)保護(hù)共享資源的并發(fā)訪問(wèn)恼布,代碼的可維護(hù)性和易讀性將會(huì)下降螺戳。
基于多路復(fù)用的高性能I/O模型
首先,Redis 是跑在單線程中的折汞,所有的操作都是按照順序線性執(zhí)行的倔幼,但是由于讀寫操作等待用戶輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回爽待,這會(huì)導(dǎo)致某一文件的 I/O 阻塞導(dǎo)致整個(gè)進(jìn)程無(wú)法對(duì)其它客戶提供服務(wù)损同,而 I/O 多路復(fù)用 就是為了解決這個(gè)問(wèn)題而出現(xiàn)的。
Linux 中的IO多路復(fù)用機(jī)制是指一個(gè)線程處理多個(gè)IO流鸟款。多路是指網(wǎng)絡(luò)連接膏燃,復(fù)用指的是同一個(gè)線程。簡(jiǎn)單來(lái)說(shuō)何什,在 Redis 只運(yùn)行單線程的情況下组哩,該機(jī)制允許內(nèi)核中,同時(shí)存在多個(gè)監(jiān)聽(tīng)套接字和已連接套接字处渣。內(nèi)核會(huì)一直監(jiān)聽(tīng)這些套接字上的連接請(qǐng)求或數(shù)據(jù)請(qǐng)求伶贰。一旦有請(qǐng)求到達(dá),就會(huì)交給 Redis 線程處理罐栈,這就實(shí)現(xiàn)了一個(gè) Redis 線程處理多個(gè)IO流的效果黍衙。
文件事件是對(duì)連接 socket 操作的一個(gè)抽象。當(dāng)端口監(jiān)聽(tīng) socket 準(zhǔn)備 accept 新連接荠诬,或者連接 socket 準(zhǔn)備好讀取請(qǐng)求琅翻、寫入響應(yīng)位仁、關(guān)閉時(shí),就會(huì)產(chǎn)生一個(gè)文件事件望迎。IO 多路復(fù)用程序負(fù)責(zé)同時(shí)監(jiān)聽(tīng)多個(gè) socket障癌,當(dāng)這些 socket 產(chǎn)生文件事件時(shí),就會(huì)觸發(fā)事件通知辩尊,文件分派器就會(huì)感知并獲取到這些事件涛浙。
雖然多個(gè)文件事件可能會(huì)并發(fā)出現(xiàn),但 IO 多路復(fù)用程序總會(huì)將所有產(chǎn)生事件的 socket 放入一個(gè)隊(duì)列中摄欲,通過(guò)這個(gè)隊(duì)列轿亮,有序的把這些文件事件通知給文件分派器。
文件事件分派器接收 I/O 多路復(fù)用程序傳來(lái)的套接字胸墙,并根據(jù)套接字產(chǎn)生的事件類型我注,調(diào)用相應(yīng)的事件處理器。
服務(wù)器會(huì)為執(zhí)行不同任務(wù)的套接字關(guān)聯(lián)不同的事件處理器迟隅,這些處理器是一個(gè)個(gè)的函數(shù)但骨,他們定義了這個(gè)事件發(fā)生時(shí),服務(wù)器應(yīng)該執(zhí)行的動(dòng)作智袭。
Redis 封裝了 4 種多路復(fù)用程序奔缠,每種封裝實(shí)現(xiàn)都提供了相同的 API 實(shí)現(xiàn)。編譯時(shí)吼野,會(huì)按照性能和系統(tǒng)平臺(tái)校哎,選擇最佳的 IO 多路復(fù)用函數(shù)作為底層實(shí)現(xiàn),選擇順序是瞳步,首先嘗試選擇 Solaries 中的 evport闷哆,如果沒(méi)有,就嘗試選擇 Linux 中的 epoll单起,否則就選擇大多 UNIX 系統(tǒng)都支持的 kqueue抱怔,這 3 個(gè)多路復(fù)用函數(shù)都直接使用系統(tǒng)內(nèi)核內(nèi)部的結(jié)構(gòu),可以服務(wù)數(shù)十萬(wàn)的文件描述符馏臭。
如果當(dāng)前編譯環(huán)境沒(méi)有上述函數(shù)野蝇,就會(huì)選擇 select 作為底層實(shí)現(xiàn)方案。select 方案的性能較差括儒,事件發(fā)生時(shí)绕沈,會(huì)掃描全部監(jiān)聽(tīng)的描述符,事件復(fù)雜度是 O(n)帮寻,并且只能同時(shí)服務(wù)有限個(gè)文件描述符乍狐,32 位機(jī)默認(rèn)是 1024 個(gè),64 位機(jī)默認(rèn)是 2048 個(gè)固逗,所以一般情況下浅蚪,并不會(huì)選擇 select 作為線上運(yùn)行方案藕帜。
單線程處理IO請(qǐng)求性能瓶頸
1、后臺(tái) Redis 通過(guò)監(jiān)聽(tīng)處理事件隊(duì)列中的消息惜傲,來(lái)單線程的處理命令洽故,如果一個(gè)命令的執(zhí)行時(shí)間很久,就會(huì)影響整個(gè) server 的性能盗誊;
耗時(shí)的操作命令有下面幾種:
1时甚、操作 bigkey:bigkey 在寫入和刪除的時(shí)候,需要的時(shí)間都會(huì)很長(zhǎng)哈踱;
2荒适、使用復(fù)雜度過(guò)高的命令;
3开镣、大量 key 集中過(guò)期:Redis 的過(guò)期機(jī)制也是在主線程中執(zhí)行的刀诬,大量 key 集中過(guò)期會(huì)導(dǎo)致處理一個(gè)請(qǐng)求時(shí),耗時(shí)都在刪除過(guò)期 key邪财,耗時(shí)變長(zhǎng)陕壹;
4、淘汰策略:淘汰策略也是在主線程執(zhí)行的树埠,當(dāng)內(nèi)存超過(guò) Redis 內(nèi)存上限后帐要,每次寫入都需要淘汰一些 key,也會(huì)造成耗時(shí)變長(zhǎng)弥奸;
5、AO F刷盤開啟 always 機(jī)制:每次寫入都需要把這個(gè)操作刷到磁盤奋早,寫磁盤的速度遠(yuǎn)比寫內(nèi)存慢盛霎,會(huì)拖慢 Redis 的性能;
6耽装、主從全量同步生成 RDB:雖然采用 fork 子進(jìn)程生成數(shù)據(jù)快照愤炸,但 fork 這一瞬間也是會(huì)阻塞整個(gè)線程的,實(shí)例越大掉奄,阻塞時(shí)間越久规个;
上面的這幾種問(wèn)題,我們?cè)趯憳I(yè)務(wù)的時(shí)候需要去避免姓建,對(duì)于 bigkey诞仓,Redis 在4.0推出了 lazy-free 機(jī)制,把 bigkey 釋放內(nèi)存的耗時(shí)操作放在了異步線程中執(zhí)行速兔,降低對(duì)主線程的影響墅拭。
2、并發(fā)量非常大時(shí)涣狗,單線程讀寫客戶端 IO 數(shù)據(jù)存在性能瓶頸
使用 Redis 時(shí)谍婉,幾乎不存在 CPU 成為瓶頸的情況舒憾, Redis 主要受限于內(nèi)存和網(wǎng)絡(luò)。隨著硬件水平的提升穗熬,Redis 中的性能慢慢主要出現(xiàn)在網(wǎng)絡(luò) IO 的讀寫上镀迂。雖然采用 IO 多路復(fù)用機(jī)制,但是讀寫客戶端數(shù)據(jù)依舊是同步IO唤蔗,只能單線程依次讀取客戶端的數(shù)據(jù)探遵,無(wú)法利用到CPU多核。
為了提升網(wǎng)絡(luò) IO 的讀寫性能措译,Redis 在6.0推出了多線程别凤,同過(guò)多線程的 IO 來(lái)處理網(wǎng)絡(luò)請(qǐng)求。不過(guò)需要注意的是這里的多線程僅僅是針對(duì)客戶端的讀寫是并行的领虹,Redis 處理事件隊(duì)列中的命令规哪,還是單線程處理的。
總結(jié)
Redis 使用單線程塌衰,來(lái)避免共享資源的競(jìng)爭(zhēng)诉稍,使用多路復(fù)用實(shí)現(xiàn)高性能的I/O,它是內(nèi)存數(shù)據(jù)庫(kù)最疆,所有操作都在內(nèi)存上完成杯巨,使用哈希表,跳表等一系列高效的數(shù)據(jù)結(jié)構(gòu)努酸,這些特性保證了 Redis 的高性能服爷。
參考
【Redis核心技術(shù)與實(shí)戰(zhàn)】https://time.geekbang.org/column/intro/100056701
【Redis6.0為什么引入多線程?】https://juejin.cn/post/7004683161695158309
【Redis設(shè)計(jì)與實(shí)現(xiàn)】https://book.douban.com/subject/25900156/
【redis---sds(簡(jiǎn)單動(dòng)態(tài)字符串)詳解】https://blog.csdn.net/u010765526/article/details/89065607
【為什么 Redis 的查詢很快】https://boilingfrog.github.io/2022/01/07/為什么redis的查詢比較快/
【redis知識(shí)點(diǎn)】https://github.com/boilingfrog/Go-POINT/tree/master/redis