網(wǎng)站怎么做賺錢廣州推廣服務(wù)
文章目錄
- 01 Redis中的多線程
- 1)redis-server:
- 2)jemalloc_bg_thd
- 3)bio_xxx:
- 02 I/O多線程
- 03 Redis中的多進(jìn)程
- 04 結(jié)論
- ▼延伸閱讀
由面試題“Redis是否為單線程”引發(fā)的思考
作者:李樂
來源:IT閱讀排行榜
很多人都遇到過這么一道面試題:Redis是單線程還是多線程?這個問題既簡單又復(fù)雜。說他簡單是因為大多數(shù)人都知道Redis是單線程,說復(fù)雜是因為這個答案其實(shí)并不準(zhǔn)確。
難道Redis不是單線程?我們啟動一個Redis實(shí)例,驗證一下就知道了。Redis安裝部署方式如下所示:
// 下載
wget https://download.redis.io/redis-stable.tar.gz
tar -xzvf redis-stable.tar.gz
// 編譯安裝
cd redis-stable
make
// 驗證是否安裝成功
./src/redis-server -v
Redis server v=7.2.4
接下來啟動Redis實(shí)例,使用命令ps查看所有線程,如下所示:
// 啟動Redis實(shí)例
./src/redis-server ./redis.conf// 查看實(shí)例進(jìn)程ID
ps aux | grep redis
root 385806 0.0 0.0 245472 11200 pts/2 Sl+ 17:32 0:00 ./src/redis-server 127.0.0.1:6379// 查看所有線程
ps -L -p 385806PID LWP TTY TIME CMD
385806 385806 pts/2 00:00:00 redis-server
385806 385809 pts/2 00:00:00 bio_close_file
385806 385810 pts/2 00:00:00 bio_aof
385806 385811 pts/2 00:00:00 bio_lazy_free
385806 385812 pts/2 00:00:00 jemalloc_bg_thd
385806 385813 pts/2 00:00:00 jemalloc_bg_thd
竟然有6個線程!不是說Redis是單線程嗎?怎么會有這么多線程呢?
這6個線程的含義你可能不太了解,但是通過這個示例至少說明Redis并不是單線程。
01 Redis中的多線程
接下來我們逐個介紹上述6個線程的作用:
1)redis-server:
主線程,用于接收并處理客戶端請求。
2)jemalloc_bg_thd
jemalloc 是新一代的內(nèi)存分配器,Redis底層使用他管理內(nèi)存。
3)bio_xxx:
以bio前綴開始的都是異步線程,用于異步執(zhí)行一些耗時任務(wù)。其中,線程bio_close_file用于異步刪除文件,線程bio_aof用于異步將AOF文件刷到磁盤,線程bio_lazy_free用于異步刪除數(shù)據(jù)(懶刪除)。
需要說明的是,主線程是通過隊列將任務(wù)分發(fā)給異步線程的,并且這一操作是需要加鎖的。主線程與異步線程的關(guān)系如下圖所示:
主線程與異步線程 主線程與異步線程 主線程與異步線程
這里我們以懶刪除為例,講解為什么要使用異步線程。Redis是一款內(nèi)存數(shù)據(jù)庫,支持多種數(shù)據(jù)類型,包括字符串、列表、哈希表、集合等。思考一下,刪除(DEL)列表類型數(shù)據(jù)的流程是怎樣的呢?第一步從數(shù)據(jù)庫字典中刪除該鍵值對,第二步遍歷并刪除列表中的所有元素(釋放內(nèi)存)。想想如果列表中的元素數(shù)目非常多呢?這一步將非常耗時。這種刪除方式稱為同步刪除,流程如下圖所示:
同步刪除流程圖 同步刪除流程圖 同步刪除流程圖
針對上述問題,Redis提出了懶刪除(異步刪除),主線程在收到刪除命令(UNLINK)時,首先從數(shù)據(jù)庫字典中刪除該鍵值對,隨后再將刪除任務(wù)分發(fā)給異步線程bio_lazy_free,由異步線程執(zhí)行第二步耗時邏輯。這時候的流程如下圖所示:
懶刪除流程圖 懶刪除流程圖 懶刪除流程圖
02 I/O多線程
難道Redis是多線程?那為什么我們老說Redis是單線程呢?這是因為讀取客戶端命令請求,執(zhí)行命令以及向客戶端返回結(jié)果都是在主線程完成的。不然的話,多線程同時操作內(nèi)存數(shù)據(jù)庫,并發(fā)問題如何解決?如果每次操作之前都加鎖,那和單線程又有什么區(qū)別呢?
當(dāng)然這一流程在Redis6.0版本也發(fā)生了改變,Redis官方指出,Redis是基于內(nèi)存的鍵值對數(shù)據(jù)庫,執(zhí)行命令的過程是非??斓?#xff0c;讀取客戶端命令請求和向客戶端返回結(jié)果(即網(wǎng)絡(luò)I/O)通常會成為Redis的性能瓶頸。
因此,在Redis 6.0版本,作者加入了多線程I/O的能力,即可以開啟多個I/O線程,并行讀取客戶端命令請求,并行向客戶端返回結(jié)果。I/O多線程能力使得Redis性能提升至少一倍。
為了開啟多線程I/O能力,需要先修改配置文件redis.conf:
io-threads-do-reads yes
io-threads 4
這兩個配置含義如下:
-
io-threads-do-reads:是否開啟多線程I/O能力,默認(rèn)為"no";
-
io-threads:I/O線程數(shù)目,默認(rèn)為1,即只使用主線程執(zhí)行網(wǎng)絡(luò)I/O,線程數(shù)最大為128;該配置應(yīng)該根據(jù)CPU核數(shù)設(shè)置,作者建議,4核CPU設(shè)置2~3個I/O線程,8核CPU設(shè)置6個I/O線程。
開啟多線程I/O能力之后,重新啟動Redis實(shí)例,查看所有線程,結(jié)果如下:
ps -L -p 104648PID LWP TTY TIME CMD
104648 104648 pts/1 00:00:00 redis-server
104648 104654 pts/1 00:00:00 io_thd_1
104648 104655 pts/1 00:00:00 io_thd_2
104648 104656 pts/1 00:00:00 io_thd_3
……
由于我們設(shè)置了io-threads等于4,所以會創(chuàng)建4個線程用于執(zhí)行I/O操作(包括主線程),上述結(jié)果符合預(yù)期。
當(dāng)然,只有I/O階段才使用了多線程,處理命令請求還是單線程,畢竟多線程操作內(nèi)存數(shù)據(jù)存在并發(fā)問題。
最后,開啟了I/O多線程之后,命令的執(zhí)行流程如下圖所示:
I / O 多線程流程圖 I/O多線程流程圖 I/O多線程流程圖
03 Redis中的多進(jìn)程
Redis還有多進(jìn)程?是的。在某些場景下,Redis也會創(chuàng)建多個子進(jìn)程來執(zhí)行一些任務(wù)。以持久化為例,Redis支持兩種類型的持久化:
-
AOF(Append Only File):可以看作是命令的日志文件,Redis會將每一個寫命令都追加到AOF文件。
-
RDB(Redis Database):以快照的方式存儲Redis內(nèi)存中的數(shù)據(jù)。命令SAVE用于手動觸發(fā)RDB持久化。想想如果Redis中的數(shù)據(jù)量非常大,持久化操作必然耗時比較長,而Redis是單線程處理命令請求,那么當(dāng)命令SAVE的執(zhí)行時間過長時,必然會影響其他命令的執(zhí)行。
命令SAVE有可能會阻塞其他請求,為此,Redis又引入了命令BGSAVE,該命令會創(chuàng)建一個子進(jìn)程來執(zhí)行持久化操作,這樣就不會影響主進(jìn)程執(zhí)行其他請求了。
我們可以手動執(zhí)行命令BGSAVE驗證。首先,使用GDB跟蹤Redis進(jìn)程,添加斷點(diǎn),讓子進(jìn)程阻塞在持久化邏輯。如下所示:
// 查詢Redis進(jìn)程ID
ps aux | grep redis
root 448144 0.1 0.0 270060 11520 pts/1 tl+ 17:00 0:00 ./src/redis-server 127.0.0.1:6379// GDB跟蹤進(jìn)程
gdb -p 448144// 跟蹤創(chuàng)建的子進(jìn)程(默認(rèn)GDB只跟蹤主進(jìn)程,需手動設(shè)置)
(gdb) set follow-fork-mode child
// 函數(shù)rdbSaveDb用于持久化數(shù)據(jù)快照
(gdb) b rdbSaveDb
Breakpoint 1 at 0x541a10: file rdb.c, line 1300.
(gdb) c
設(shè)置好斷點(diǎn)之后,使用Redis客戶端發(fā)送命令BGSAVE,結(jié)果如下:
// 請求立即返回
127.0.0.1:6379> bgsave
Background saving started// GDB輸出以下信息
[New process 452541]
Breakpoint 1, rdbSaveDb (...) at rdb.c:1300
可以看到,GDB目前跟蹤的是子進(jìn)程,進(jìn)程ID是452541。也可以通過Linux命令 ps 查看所有進(jìn)程,結(jié)果如下:
ps aux | grep redis
root 448144 0.0 0.0 270060 11520 pts/1 Sl+ 17:00 0:00 ./src/redis-server 127.0.0.1:6379
root 452541 0.0 0.0 270064 11412 pts/1 t+ 17:19 0:00 redis-rdb-bgsave 127.0.0.1:6379
可以看到子進(jìn)程的名稱是redis-rdb-bgsave,也就是該進(jìn)程將所有數(shù)據(jù)的快照持久化在RDB文件。
最后再思考兩個問題。
- 問題1:為什么采用子進(jìn)程而不是子線程呢?
因為RDB是將數(shù)據(jù)快照持久化存儲,如果采用子線程,主線程與子線程將會共享內(nèi)存數(shù)據(jù),主線程在持久化的同時還會修改內(nèi)存數(shù)據(jù),這有可能導(dǎo)致數(shù)據(jù)不一致。而主進(jìn)程與子進(jìn)程的內(nèi)存數(shù)據(jù)是完全隔離的,不存在此問題。
- 問題2:假設(shè)Redis內(nèi)存中存儲了10GB的數(shù)據(jù),在創(chuàng)建子進(jìn)程執(zhí)行持久化操作之后,此時子進(jìn)程也需要10GB的內(nèi)存嗎?復(fù)制10GB的內(nèi)存數(shù)據(jù),也會比較耗時吧?另外如果系統(tǒng)只有15GB的內(nèi)存,還能執(zhí)行BGSAVE命令嗎?
這里有一個概念叫寫時復(fù)制(copy on write),在使用系統(tǒng)調(diào)用fork創(chuàng)建子進(jìn)程之后,主進(jìn)程與子進(jìn)程的內(nèi)存數(shù)據(jù)暫時還是共享的,但是當(dāng)主進(jìn)程需要修改內(nèi)存數(shù)據(jù)時,系統(tǒng)會自動將該內(nèi)存塊復(fù)制一份,以此實(shí)現(xiàn)內(nèi)存數(shù)據(jù)的隔離。
命令BGSAVE的執(zhí)行流程如下圖所示:
B G S A V E 執(zhí)行流程 BGSAVE執(zhí)行流程 BGSAVE執(zhí)行流程
04 結(jié)論
Redis的進(jìn)程模型/線程模型還是比較復(fù)雜的,這里也只是簡單介紹了部分場景下的多線程以及多進(jìn)程,其他場景下的多線程、多進(jìn)程還有待讀者自己研究。
作者介紹
李樂:好未來Golang開發(fā)專家、西安電子科技大學(xué)碩士,曾就職于滴滴,樂于鉆研技術(shù)與源碼,合著有《高效使用Redis:一書學(xué)透數(shù)據(jù)存儲與高可用集群》《Redis5設(shè)計與源碼分析》《Nginx底層設(shè)計與源碼分析》。
▼延伸閱讀
《高效使用 R e d i s :一書學(xué)透數(shù)據(jù)存儲與高可用集群》 《高效使用Redis:一書學(xué)透數(shù)據(jù)存儲與高可用集群》 《高效使用Redis:一書學(xué)透數(shù)據(jù)存儲與高可用集群》
推薦語:深入Redis數(shù)據(jù)結(jié)構(gòu)與底層實(shí)現(xiàn),攻克Redis數(shù)據(jù)存儲與集群管理難題。