Redis的高性能之謎
介紹
Redis通常用作緩存。當(dāng)一致性要求不高時(shí),它也可以用作存儲(chǔ)。此外,Redis還提供消息訂閱、事務(wù)、索引等功能。我們還可以使用集群功能構(gòu)建分布式存儲(chǔ)服務(wù),并實(shí)現(xiàn)非強(qiáng)一致性的分布式鎖服務(wù)。
在上述各種情況下,Redis都具有一個(gè)共同的優(yōu)勢(shì),即處理速度快(高性能)。
Redis有多快?
要了解Redis有多快,您需要有一個(gè)評(píng)估工具。
幸運(yùn)的是,Redis提供了這樣一個(gè)工具,并提供了一些常用硬件平臺(tái)的性能數(shù)據(jù)。
- Redis基準(zhǔn)測(cè)試可用于評(píng)估Redis的性能。命令行提供了在正常/管道模式下以及在不同壓力下評(píng)估特定命令性能的功能。
- Redis具有出色的性能。作為鍵值系統(tǒng),最大負(fù)載級(jí)別為10W / s,設(shè)置和獲取時(shí)間消耗級(jí)別分別為10ms和5ms。使用流水線可以提高Redis操作的性能。
redis-benchmark -t set,lpush -n 100000 -q
SET: 97087.38 每秒請(qǐng)求 //處理97000次設(shè)置請(qǐng)求每秒
LPUSH: 101112.23 每秒請(qǐng)求 //處理100000次lpush請(qǐng)求每秒
腳本執(zhí)行時(shí)間
redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
SCRIPT load redis.call('set','foo','bar'): 101317.12 每秒請(qǐng)求, p50=0.255毫秒
默認(rèn)情況下,Redis基準(zhǔn)測(cè)試使用100,000個(gè)請(qǐng)求、50個(gè)客戶端和3字節(jié)的負(fù)載進(jìn)行測(cè)試。
Redis為何如此之快?
Redis是單線程應(yīng)用程序,這意味著Redis使用單個(gè)線程來處理客戶端的請(qǐng)求。
Redis具有高性能的原因如下:
- 內(nèi)存存儲(chǔ):Redis使用內(nèi)存(內(nèi)存中)存儲(chǔ),沒有磁盤I/O開銷。
- 單線程實(shí)現(xiàn):Redis使用單個(gè)線程處理請(qǐng)求,避免了多線程之間的線程切換和鎖資源爭(zhēng)用的成本。
- 非阻塞I/O:Redis使用多路復(fù)用I/O技術(shù),在poll、epoll和kqueue中選擇最佳的I/O實(shí)現(xiàn)。
- 優(yōu)化的數(shù)據(jù)結(jié)構(gòu):Redis具有許多經(jīng)過優(yōu)化的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),可以直接應(yīng)用。應(yīng)用層可以直接使用本機(jī)數(shù)據(jù)結(jié)構(gòu)以提高性能。
單線程
Redis的核心網(wǎng)絡(luò)模型由單線程實(shí)現(xiàn),這在一開始時(shí)曾引起了許多人的困惑。Redis官方對(duì)此的回答是:
CPU很少成為Redis的瓶頸,因?yàn)橥ǔedis要么是內(nèi)存綁定的,要么是網(wǎng)絡(luò)綁定的。例如,使用管道在運(yùn)行在平均Linux系統(tǒng)上的Redis上,每秒可以傳輸甚至100萬個(gè)請(qǐng)求,因此,如果您的應(yīng)用程序主要使用O(N)或O(log(N))命令,它幾乎不會(huì)使用太多CPU。
單線程的好處是什么?
- 無需線程創(chuàng)建或線程銷毀引起的消耗
- 避免線程切換引起的CPU消耗
- 避免線程之間的競(jìng)爭(zhēng)問題,如添加鎖、釋放鎖、死鎖等
此外,單線程機(jī)制極大地降低了Redis內(nèi)部實(shí)現(xiàn)的復(fù)雜性。哈希的延遲重哈希、Lpush等“線程不安全”命令可以在無鎖的情況下執(zhí)行。
I/O模型
一般來說,I/O操作分為兩個(gè)步驟:
- 等待數(shù)據(jù)從網(wǎng)絡(luò)到達(dá),然后將其加載到內(nèi)核空間緩沖區(qū)
- 將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)復(fù)制到用戶空間緩沖區(qū)
根據(jù)這兩個(gè)步驟是否阻塞線程,可以將其分為阻塞/非阻塞、同步/異步。
阻塞I/O模型
I/O最常見的模型是阻塞I/O模型,我們迄今為止在文本中使用的所有示例都使用了阻塞I/O模型。默認(rèn)情況下,所有套接字都是阻塞的。
在此示例中,我們使用UDP而不是TCP,因?yàn)閷?duì)于UDP,數(shù)據(jù)“準(zhǔn)備”以供讀取的概念很簡(jiǎn)單:要么接收到整個(gè)數(shù)據(jù)報(bào),要么沒有。
而對(duì)于TCP,情況會(huì)更加復(fù)雜,因?yàn)檫€涉及到額外的變量,如套接字的低水位標(biāo)記等。
非阻塞I/O模型
當(dāng)我們將套接字設(shè)置為非阻塞時(shí),我們告訴內(nèi)核“當(dāng)我請(qǐng)求的I/O操作不能在不使進(jìn)程進(jìn)入休眠的情況下完成時(shí),請(qǐng)不要使進(jìn)程進(jìn)入休眠,而是返回一個(gè)錯(cuò)誤”。
前三次調(diào)用recvfrom時(shí),沒有數(shù)據(jù)返回,因此內(nèi)核立即返回EWOULDBLOCK錯(cuò)誤。第四次調(diào)用recvfrom時(shí),數(shù)據(jù)報(bào)準(zhǔn)備好了,它
被復(fù)制到我們的應(yīng)用程序緩沖區(qū)中,recvfrom成功返回。然后我們處理數(shù)據(jù)。
當(dāng)應(yīng)用程序循環(huán)調(diào)用非阻塞描述符上的recvfrom時(shí),這稱為輪詢。應(yīng)用程序不斷輪詢內(nèi)核,以查看某個(gè)操作是否準(zhǔn)備好。這通常會(huì)浪費(fèi)CPU時(shí)間,但通常在專用于一項(xiàng)功能的系統(tǒng)上遇到這種模型。
多路復(fù)用I/O模型
使用I/O多路復(fù)用時(shí),我們調(diào)用select或poll并在這兩個(gè)系統(tǒng)調(diào)用中的一個(gè)中阻塞,而不是在實(shí)際I/O系統(tǒng)調(diào)用中阻塞。
我們?cè)谡{(diào)用select中阻塞,等待數(shù)據(jù)報(bào)套接字可讀。當(dāng)select返回套接字可讀時(shí),我們?nèi)缓笳{(diào)用recvfrom將數(shù)據(jù)報(bào)復(fù)制到我們的應(yīng)用程序緩沖區(qū)中。
與阻塞I/O相比,使用select似乎沒有任何優(yōu)勢(shì),實(shí)際上,由于使用select需要兩個(gè)系統(tǒng)調(diào)用而不是一個(gè),因此實(shí)際上存在輕微的劣勢(shì)。
但使用select的優(yōu)勢(shì)在于,我們可以等待多個(gè)描述符準(zhǔn)備就緒。
現(xiàn)在讓我們看看Redis如何處理客戶端連接?
通常,Redis使用反應(yīng)器設(shè)計(jì)模式,封裝了多個(gè)實(shí)現(xiàn)(select、epoll、kqueue等)以多路復(fù)用IO來處理來自客戶端的請(qǐng)求。
反應(yīng)器設(shè)計(jì)模式通常用于實(shí)現(xiàn)事件驅(qū)動(dòng)。此外,Redis在不同平臺(tái)上封裝了不同的多路復(fù)用IO庫。
Redis將優(yōu)先選擇時(shí)間復(fù)雜度為O(1)的I/O多路復(fù)用函數(shù)作為底層實(shí)現(xiàn),包括Solaris 10中的evport、Linux中的epoll和Mac OS / FreeBSD中的kqueue。
這些函數(shù)都使用內(nèi)核的內(nèi)部結(jié)構(gòu),并可以為數(shù)十萬個(gè)文件描述符提供服務(wù)。
但是,如果當(dāng)前的編譯環(huán)境沒有上述函數(shù),將選擇select作為備選方案。因?yàn)樵谑褂脮r(shí)會(huì)掃描所有受監(jiān)視的描述符,所以其時(shí)間復(fù)雜度較差O(n),同時(shí)一次只能為1024個(gè)文件描述符提供服務(wù),因此通常不作為首選方案使用。