?? 寫在前面參與規則!!!
?參與方式:關注博主、點贊、收藏、評論,任意評論(每人最多評論三次)
??本次送書1~4本【取決于閱讀量,閱讀量越多,送的越多】
很多人都遇到過這么一道面試題:Redis是單線程還是多線程?這個問題既簡單又復雜。說他簡單是因為大多數人都知道Redis是單線程,說復雜是因為這個答案其實并不準確。
難道Redis不是單線程?我們啟動一個Redis實例,驗證一下就知道了。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實例,使用命令ps查看所有線程,如下所示:
// 啟動Redis實例
./src/redis-server ./redis.conf// 查看實例進程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并不是單線程。
1 Redis中的多線程
接下來我們逐個介紹上述6個線程的作用:
1 redis-server:
主線程,用于接收并處理客戶端請求。
2 jemalloc_bg_thd
jemalloc 是新一代的內存分配器,Redis底層使用他管理內存。
3 bio_xxx:
以bio前綴開始的都是異步線程,用于異步執行一些耗時任務。其中,線程bio_close_file用于異步刪除文件,線程bio_aof用于異步將AOF文件刷到磁盤,線程bio_lazy_free用于異步刪除數據(懶刪除)。
需要說明的是,主線程是通過隊列將任務分發給異步線程的,并且這一操作是需要加鎖的。主線程與異步線程的關系如下圖所示:
這里我們以懶刪除為例,講解為什么要使用異步線程。Redis是一款內存數據庫,支持多種數據類型,包括字符串、列表、哈希表、集合等。思考一下,刪除(DEL)列表類型數據的流程是怎樣的呢?第一步從數據庫字典中刪除該鍵值對,第二步遍歷并刪除列表中的所有元素(釋放內存)。想想如果列表中的元素數目非常多呢?這一步將非常耗時。這種刪除方式稱為同步刪除,流程如下圖所示:
針對上述問題,Redis提出了懶刪除(異步刪除),主線程在收到刪除命令(UNLINK)時,首先從數據庫字典中刪除該鍵值對,隨后再將刪除任務分發給異步線程bio_lazy_free,由異步線程執行第二步耗時邏輯。這時候的流程如下圖所示:
2 I/O多線程
難道Redis是多線程?那為什么我們老說Redis是單線程呢?這是因為讀取客戶端命令請求,執行命令以及向客戶端返回結果都是在主線程完成的。不然的話,多線程同時操作內存數據庫,并發問題如何解決?如果每次操作之前都加鎖,那和單線程又有什么區別呢?
當然這一流程在Redis6.0版本也發生了改變,Redis官方指出,Redis是基于內存的鍵值對數據庫,執行命令的過程是非常快的,讀取客戶端命令請求和向客戶端返回結果(即網絡I/O)通常會成為Redis的性能瓶頸。
因此,在Redis 6.0版本,作者加入了多線程I/O的能力,即可以開啟多個I/O線程,并行讀取客戶端命令請求,并行向客戶端返回結果。I/O多線程能力使得Redis性能提升至少一倍。
為了開啟多線程I/O能力,需要先修改配置文件redis.conf:
io-threads-do-reads yes
io-threads 4
這兩個配置含義如下:
io-threads-do-reads:是否開啟多線程I/O能力,默認為"no";io-threads:I/O線程數目,默認為1,即只使用主線程執行網絡I/O,線程數最大為128;該配置應該根據CPU核數設置,作者建議,4核CPU設置2~3個I/O線程,8核CPU設置6個I/O線程。
開啟多線程I/O能力之后,重新啟動Redis實例,查看所有線程,結果如下:
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
……
由于我們設置了io-threads等于4,所以會創建4個線程用于執行I/O操作(包括主線程),上述結果符合預期。
當然,只有I/O階段才使用了多線程,處理命令請求還是單線程,畢竟多線程操作內存數據存在并發問題。
最后,開啟了I/O多線程之后,命令的執行流程如下圖所示:
3 Redis中的多進程
Redis還有多進程?是的。在某些場景下,Redis也會創建多個子進程來執行一些任務。以持久化為例,Redis支持兩種類型的持久化:
- AOF(Append Only File):可以看作是命令的日志文件,Redis會將每一個寫命令都追加到AOF文件。
- RDB(Redis
Database):以快照的方式存儲Redis內存中的數據。命令SAVE用于手動觸發RDB持久化。想想如果Redis中的數據量非常大,持久化操作必然耗時比較長,而Redis是單線程處理命令請求,那么當命令SAVE的執行時間過長時,必然會影響其他命令的執行。
命令SAVE有可能會阻塞其他請求,為此,Redis又引入了命令BGSAVE,該命令會創建一個子進程來執行持久化操作,這樣就不會影響主進程執行其他請求了。
我們可以手動執行命令BGSAVE驗證。首先,使用GDB跟蹤Redis進程,添加斷點,讓子進程阻塞在持久化邏輯。如下所示:
// 查詢Redis進程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跟蹤進程
gdb -p 448144
// 跟蹤創建的子進程(默認GDB只跟蹤主進程,需手動設置)
(gdb) set follow-fork-mode child
// 函數rdbSaveDb用于持久化數據快照
(gdb) b rdbSaveDb
Breakpoint 1 at 0x541a10: file rdb.c, line 1300.
(gdb) c
設置好斷點之后,使用Redis客戶端發送命令BGSAVE,結果如下:
// 請求立即返回
127.0.0.1:6379> bgsave
Background saving started
// GDB輸出以下信息
[New process 452541]
Breakpoint 1, rdbSaveDb (...) at rdb.c:1300
可以看到,GDB目前跟蹤的是子進程,進程ID是452541。也可以通過Linux命令 ps 查看所有進程,結果如下:
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
可以看到子進程的名稱是redis-rdb-bgsave,也就是該進程將所有數據的快照持久化在RDB文件。
最后再思考兩個問題。
- 問題1:為什么采用子進程而不是子線程呢?
因為RDB是將數據快照持久化存儲,如果采用子線程,主線程與子線程將會共享內存數據,主線程在持久化的同時還會修改內存數據,這有可能導致數據不一致。而主進程與子進程的內存數據是完全隔離的,不存在此問題。
- 問題2:假設Redis內存中存儲了10GB的數據,在創建子進程執行持久化操作之后,此時子進程也需要10GB的內存嗎?復制10GB的內存數據,也會比較耗時吧?另外如果系統只有15GB的內存,還能執行BGSAVE命令嗎?
這里有一個概念叫寫時復制(copy on write),在使用系統調用fork創建子進程之后,主進程與子進程的內存數據暫時還是共享的,但是當主進程需要修改內存數據時,系統會自動將該內存塊復制一份,以此實現內存數據的隔離。
命令BGSAVE的執行流程如下圖所示:
4 結論
Redis的進程模型/線程模型還是比較復雜的,這里也只是簡單介紹了部分場景下的多線程以及多進程,其他場景下的多線程、多進程還有待讀者自己研究。
5 延伸閱讀
作者介紹
李樂:好未來Golang開發專家、西安電子科技大學碩士,曾就職于滴滴,樂于鉆研技術與源碼,合著有《高效使用Redis:一書學透數據存儲與高可用集群》《Redis5設計與源碼分析》《Nginx底層設計與源碼分析》。
推薦語:
全書主要分為三部分介紹Redis。
- 第一部分介紹Redis6中使用的數據結構,包括動態字符串、跳躍表、壓縮列表、字典、整數集合和快速鏈表,詳細介紹其基本結構及常見操作。
- 第二部分為本書核心篇章,首先介紹了Redis6的啟動流程,命令解析流程,之后對Redis6中的命令實現進行了全面的介紹,包括鍵命令、字符串命令、哈希表命令、列表命令、集合及有序集合命令、地理位置相關的GEO命令、統計相關的HyperLogLog命令。
- 第三部分,主要介紹了Redis6的一些特性及使用,包括事務、持久化、主從復制以及集群等。