面試官:你確定 Redis 是單線程的進(jìn)程嗎?
?這次主要分享 Redis 線程模型篇的面試題。
- Redis 是單線程嗎?
- Redis 單線程模式是怎樣的?
- Redis 采用單線程為什么還這么快?
- Redis 6.0 之前為什么使用單線程?
- Redis 6.0 之后為什么引入了多線程?
Redis 是單線程嗎?
Redis 單線程指的是「接收客戶端請(qǐng)求->解析請(qǐng)求 ->進(jìn)行數(shù)據(jù)讀寫等操作->發(fā)生數(shù)據(jù)給客戶端」這個(gè)過程是由一個(gè)線程(主線程)來完成的,這也是我們常說 Redis 是單線程的原因。
但是,Redis 程序并不是單線程的,Redis 在啟動(dòng)的時(shí)候,是會(huì)啟動(dòng)后臺(tái)線程(BIO)的:
- Redis 在 2.6 版本,會(huì)啟動(dòng) 2 個(gè)后臺(tái)線程,分別處理關(guān)閉文件、AOF 刷盤這兩個(gè)任務(wù);
- Redis 在 4.0 版本之后,新增了一個(gè)新的后臺(tái)線程,用來異步釋放 Redis 內(nèi)存,也就是 lazyfree 線程。例如執(zhí)行 unlink key / flushdb async / flushall async 等命令,會(huì)把這些刪除操作交給后臺(tái)線程來執(zhí)行,好處是不會(huì)導(dǎo)致 Redis 主線程卡頓。因此,當(dāng)我們要?jiǎng)h除一個(gè)大 key 的時(shí)候,不要使用 del 命令刪除,因?yàn)?del 是在主線程處理的,這樣會(huì)導(dǎo)致 Redis 主線程卡頓,因此我們應(yīng)該使用 unlink 命令來異步刪除大key。
之所以 Redis 為「關(guān)閉文件、AOF 刷盤、釋放內(nèi)存」這些任務(wù)創(chuàng)建單獨(dú)的線程來處理,是因?yàn)檫@些任務(wù)的操作都是很耗時(shí)的,如果把這些任務(wù)都放在主線程來處理,那么 Redis 主線程就很容易發(fā)生阻塞,這樣就無法處理后續(xù)的請(qǐng)求了。
后臺(tái)線程相當(dāng)于一個(gè)消費(fèi)者,生產(chǎn)者把耗時(shí)任務(wù)丟到任務(wù)隊(duì)列中,消費(fèi)者(BIO)不停輪詢這個(gè)隊(duì)列,拿出任務(wù)就去執(zhí)行對(duì)應(yīng)的方法即可。
關(guān)閉文件、AOF 刷盤、釋放內(nèi)存這三個(gè)任務(wù)都有各自的任務(wù)隊(duì)列:
- BIO_CLOSE_FILE,關(guān)閉文件任務(wù)隊(duì)列:當(dāng)隊(duì)列有任務(wù)后,后臺(tái)線程會(huì)調(diào)用 close(fd) ,將文件關(guān)閉;
- BIO_AOF_FSYNC,AOF刷盤任務(wù)隊(duì)列:當(dāng) AOF 日志配置成 everysec 選項(xiàng)后,主線程會(huì)把 AOF 寫日志操作封裝成一個(gè)任務(wù),也放到隊(duì)列中。當(dāng)發(fā)現(xiàn)隊(duì)列有任務(wù)后,后臺(tái)線程會(huì)調(diào)用 fsync(fd),將 AOF 文件刷盤,
- BIO_LAZY_FREE,lazy free 任務(wù)隊(duì)列:當(dāng)隊(duì)列有任務(wù)后,后臺(tái)線程會(huì) free(obj) 釋放對(duì)象 / free(dict) 刪除數(shù)據(jù)庫所有對(duì)象 / free(skiplist) 釋放跳表對(duì)象;
Redis 單線程模式是怎樣的?
Redis 6.0 版本之前的單線模式如下圖:
圖片
圖中的藍(lán)色部分是一個(gè)事件循環(huán),是由主線程負(fù)責(zé)的,可以看到網(wǎng)絡(luò) I/O 和命令處理都是單線程。Redis 初始化的時(shí)候,會(huì)做下面這幾年事情:
- 首先,調(diào)用 epoll_create() 創(chuàng)建一個(gè) epoll 對(duì)象和調(diào)用 socket() 一個(gè)服務(wù)端 socket
- 然后,調(diào)用 bind() 綁定端口和調(diào)用 listen() 監(jiān)聽該 socket;
- 然后,將調(diào)用 epoll_crt() 將 listen socket 加入到 epoll,同時(shí)注冊(cè)「連接事件」處理函數(shù)。
初始化完后,主線程就進(jìn)入到一個(gè)事件循環(huán)函數(shù),主要會(huì)做以下事情:
首先,先調(diào)用處理發(fā)送隊(duì)列函數(shù),看是發(fā)送隊(duì)列里是否有任務(wù),如果有發(fā)送任務(wù),則通過 write 函數(shù)將客戶端發(fā)送緩存區(qū)里的數(shù)據(jù)發(fā)送出去,如果這一輪數(shù)據(jù)沒有發(fā)生完,就會(huì)注冊(cè)寫事件處理函數(shù),等待 epoll_wait 發(fā)現(xiàn)可寫后再處理 。
接著,調(diào)用 epoll_wait 函數(shù)等待事件的到來:
- 如果是連接事件到來,則會(huì)調(diào)用連接事件處理函數(shù),該函數(shù)會(huì)做這些事情:調(diào)用 accpet 獲取已連接的 socket -> 調(diào)用 epoll_ctr 將已連接的 socket 加入到 epoll -> 注冊(cè)「讀事件」處理函數(shù);
- 如果是讀事件到來,則會(huì)調(diào)用讀事件處理函數(shù),該函數(shù)會(huì)做這些事情:調(diào)用 read 獲取客戶端發(fā)送的數(shù)據(jù) -> 解析命令 -> 處理命令 -> 將客戶端對(duì)象添加到發(fā)送隊(duì)列 -> 將執(zhí)行結(jié)果寫到發(fā)送緩存區(qū)等待發(fā)送;
- 如果是寫事件到來,則會(huì)調(diào)用寫事件處理函數(shù),該函數(shù)會(huì)做這些事情:通過 write 函數(shù)將客戶端發(fā)送緩存區(qū)里的數(shù)據(jù)發(fā)送出去,如果這一輪數(shù)據(jù)沒有發(fā)生完,就會(huì)繼續(xù)注冊(cè)寫事件處理函數(shù),等待 epoll_wait 發(fā)現(xiàn)可寫后再處理 。
以上就是 Redis 單線模式的工作方式。?
Redis 采用單線程為什么還這么快?
官方使用基準(zhǔn)測(cè)試的結(jié)果是,單線程的 Redis 吞吐量可以達(dá)到 10W/每秒,如下圖所示:
之所以 Redis 采用單線程(網(wǎng)絡(luò) I/O 和執(zhí)行命令)那么快,有如下幾個(gè)原因:
- Redis 的大部分操作都在內(nèi)存中完成,并且采用了高效的數(shù)據(jù)結(jié)構(gòu),因此 Redis 瓶頸可能是機(jī)器的內(nèi)存或者網(wǎng)絡(luò)帶寬,而并非 CPU,既然 CPU 不是瓶頸,那么自然就采用單線程的解決方案了;
- Redis 采用單線程模型可以避免了多線程之間的競(jìng)爭(zhēng),省去了多線程切換帶來的時(shí)間和性能上的開銷,而且也不會(huì)導(dǎo)致死鎖問題。
- Redis 采用了I/O 多路復(fù)用機(jī)制處理大量的客戶端 Socket 請(qǐng)求,IO 多路復(fù)用機(jī)制是指一個(gè)線程處理多個(gè) IO 流,就是我們經(jīng)常聽到的 select/epoll 機(jī)制。簡(jiǎn)單來說,在 Redis 只運(yùn)行單線程的情況下,該機(jī)制允許內(nèi)核中,同時(shí)存在多個(gè)監(jiān)聽 Socket 和已連接 Socket。內(nèi)核會(huì)一直監(jiān)聽這些 Socket 上的連接請(qǐng)求或數(shù)據(jù)請(qǐng)求。一旦有請(qǐng)求到達(dá),就會(huì)交給 Redis 線程處理,這就實(shí)現(xiàn)了一個(gè) Redis 線程處理多個(gè) IO 流的效果。
Redis 6.0 之前為什么使用單線程?
我們都知道單線程的程序是無法利用服務(wù)器的多核 CPU 的,那么早期 Redis 版本的主要工作(網(wǎng)絡(luò) I/O 和執(zhí)行命令)為什么還要使用單線程呢?我們不妨先看一下Redis官方給出的FAQ。
核心意思是:CPU 并不是制約 Redis 性能表現(xiàn)的瓶頸所在,更多情況下是受到內(nèi)存大小和網(wǎng)絡(luò)I/O的限制,所以 Redis 核心網(wǎng)絡(luò)模型使用單線程并沒有什么問題,如果你想要使用服務(wù)的多核CPU,可以在一臺(tái)服務(wù)器上啟動(dòng)多個(gè)節(jié)點(diǎn)或者采用分片集群的方式。
除了上面的官方回答,選擇單線程的原因也有下面的考慮。
使用了單線程后,可維護(hù)性高,多線程模型雖然在某些方面表現(xiàn)優(yōu)異,但是它卻引入了程序執(zhí)行順序的不確定性,帶來了并發(fā)讀寫的一系列問題,增加了系統(tǒng)復(fù)雜度、同時(shí)可能存在線程切換、甚至加鎖解鎖、死鎖造成的性能損耗。
Redis 6.0 之后為什么引入了多線程?
雖然 Redis 的主要工作(網(wǎng)絡(luò) I/O 和執(zhí)行命令)一直是單線程模型,但是在 Redis 6.0 版本之后,也采用了多個(gè) I/O 線程來處理網(wǎng)絡(luò)請(qǐng)求,這是因?yàn)殡S著網(wǎng)絡(luò)硬件的性能提升,Redis 的性能瓶頸有時(shí)會(huì)出現(xiàn)在網(wǎng)絡(luò) I/O 的處理上。
所以為了提高網(wǎng)絡(luò)請(qǐng)求處理的并行度,Redis 6.0 對(duì)于網(wǎng)絡(luò)請(qǐng)求采用多線程來處理。但是對(duì)于讀寫命令,Redis 仍然使用單線程來處理,所以大家不要誤解 Redis 有多線程同時(shí)執(zhí)行命令。
Redis 官方表示,Redis 6.0 版本引入的多線程 I/O 特性對(duì)性能提升至少是一倍以上。
Redis 6.0 版本支持的 I/O 多線程特性,默認(rèn)是 I/O 多線程只處理寫操作(write client socket),并不會(huì)以多線程的方式處理讀操作(read client socket)。要想開啟多線程處理客戶端讀請(qǐng)求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置項(xiàng)設(shè)為 yes。
//讀請(qǐng)求也使用io多線程
io-threads-do-reads yes
同時(shí), Redis.conf 配置文件中提供了 IO 多線程個(gè)數(shù)的配置項(xiàng)。
// io-threads N,表示啟用 N-1 個(gè) I/O 多線程(主線程也算一個(gè) I/O 線程)
io-threads 4
關(guān)于線程數(shù)的設(shè)置,官方的建議是如果為 4 核的 CPU,建議線程數(shù)設(shè)置為 2 或 3,如果為 8 核 CPU 建議線程數(shù)設(shè)置為 6,線程數(shù)一定要小于機(jī)器核數(shù),線程數(shù)并不是越大越好。因此, Redis 6.0 版本之后,Redis 在啟動(dòng)的時(shí)候,默認(rèn)情況下會(huì)有 6 個(gè)線程:
- Redis-server :Redis的主線程,主要負(fù)責(zé)執(zhí)行命令;
- bio_close_file、bio_aof_fsync、bio_lazy_free:三個(gè)后臺(tái)線程,分別異步處理關(guān)閉文件任務(wù)、AOF刷盤任務(wù)、釋放內(nèi)存任務(wù);
- io_thd_1、io_thd_2、io_thd_3:三個(gè) I/O 線程,io-threads 默認(rèn)是 4 ,所以會(huì)啟動(dòng) 3(4-1)個(gè) I/O 多線程,用來分擔(dān) Redis 網(wǎng)絡(luò) I/O 的壓力。?