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