Redis6.0爲什麼引入多線程?單線程不香嗎?

本文主要分兩部分。首先咱們先聊一下Redis6.0以前爲何採用單線程模型。而後再詳細解釋Redis6.0的多線程。css

Redis6.0以前爲何採用單線程模型java

嚴格地說,從Redis 4.0以後並非單線程。除了主線程外,還有一些後臺線程處理一些較爲緩慢的操做,例如無用鏈接的釋放、大 key 的刪除等等。redis

單線程模型,爲什麼性能那麼高?編程

Redis做者從設計之初,進行了多方面的考慮。最終選擇使用單線程模型來處理命令。之因此選擇單線程模型,主要有以下幾個重要緣由:緩存

  1. Redis操做基於內存,絕大多數操做的性能瓶頸不在CPU
  2. 單線程模型,避免了線程間切換帶來的性能開銷
  3. 使用單線程模型也能併發的處理客戶端的請求(多路複用I/O)
  4. 使用單線程模型,可維護性更高,開發,調試和維護的成本更低

上述第三個緣由是Redis最終採用單線程模型的決定性因素,其餘的兩個緣由都是使用單線程模型額外帶來的好處,在這裏咱們會按順序介紹上述的幾個緣由。安全

性能瓶頸不在CPU性能優化

下圖是Redis官網對單線程模型的說明。大概意思是:Redis的瓶頸並不在CPU,它的主要瓶頸在於內存和網絡。在Linux環境中,Redis每秒甚至能夠提交100萬次請求。網絡

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

爲何說Redis的瓶頸不在CPU?多線程

首先,Redis絕大部分操做是基於內存的,並且是純kv(key-value)操做,因此命令執行速度很是快。咱們能夠大概理解成,redis中的數據存儲在一張大HashMap中,HashMap的優點就是查找和寫入的時間複雜度都是O(1)。Redis內部採用這種結構存儲數據,就奠基了Redis高性能的基礎。根據Redis官網描述,在理想狀況下Redis每秒能夠提交一百萬次請求,每次請求提交所需的時間在納秒的時間量級。既然每次的Redis操做都這麼快,單線程就能夠徹底搞定了,那還何須要用多線程呢!併發

線程上下文切換問題

另外,多線程場景下會發生線程上下文切換。線程是由CPU調度的,CPU的一個核在一個時間片內只能同時執行一個線程,在CPU由線程A切換到線程B的過程當中會發生一系列的操做,主要過程包括保存線程A的執行現場,而後載入線程B的執行現場,這個過程就是「線程上下文切換」。其中涉及線程相關指令的保存和恢復。

頻繁的線程上下文切換可能會致使性能急劇降低,這會致使咱們不只沒有提高處理請求的速度,反而下降了性能,這也是 Redis 對於多線程技術持謹慎態度的緣由之一。

在Linux系統中可使用vmstat命令來查看上下文切換的次數,下面是vmstat查看上下文切換次數的示例:

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

vmstat 1 表示每秒統計一次, 其中cs列就是指上下文切換的數目. 通常狀況下, 空閒系統的上下文切換每秒在1500如下。

並行處理客戶端的請求(I/O多路複用)

如上所述:Redis的瓶頸並不在CPU,它的主要瓶頸在於內存和網絡。所謂內存瓶頸很好理解,Redis作爲緩存使用時不少場景須要緩存大量數據,因此須要大量內存空間,這能夠經過集羣分片去解決,例如Redis自身的無中心集羣分片方案以及Codis這種基於代理的集羣分片方案。

對於網絡瓶頸,Redis在網絡I/O模型上採用了多路複用技術,來減小網絡瓶頸帶來的影響。不少場景中使用單線程模型並不意味着程序不能併發的處理任務。Redis 雖然使用單線程模型處理用戶的請求,可是它卻使用 I/O 多路複用技術「並行」處理來自客戶端的多個鏈接,同時等待多個鏈接發送的請求。使用 I/O多路複用技術能極大地減小系統的開銷,系統再也不須要爲每一個鏈接建立專門的監聽線程,避免了因爲大量的線程建立帶來的巨大性能開銷。

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

下面咱們詳細解釋一下多路複用I/O模型。爲了能更充分理解,咱們先了解幾個基本概念。

Socket(套接字):Socket能夠理解成,在兩個應用程序進行網絡通訊時,分別在兩個應用程序中的通訊端點。通訊時,一個應用程序將數據寫入Socket,而後經過網卡把數據發送到另一個應用程序的Socket中。咱們日常所說的HTTP和TCP協議的遠程通訊,底層都是基於Socket實現的。5種網絡IO模型也都要基於Socket實現網絡通訊。

阻塞與非阻塞:所謂阻塞,就是發出一個請求不能馬上返回響應,要等全部的邏輯全處理完才能返回響應。非阻塞反之,發出一個請求馬上返回應答,不用等處理完全部邏輯。

內核空間與用戶空間:在Linux中,應用程序穩定性遠遠比不上操做系統程序,爲了保證操做系統的穩定性,Linux區分了內核空間和用戶空間。能夠這樣理解,內核空間運行操做系統程序和驅動程序,用戶空間運行應用程序。Linux以這種方式隔離了操做系統程序和應用程序,避免了應用程序影響到操做系統自身的穩定性。這也是Linux系統超級穩定的主要緣由。全部的系統資源操做都在內核空間進行,好比讀寫磁盤文件,內存分配和回收,網絡接口調用等。因此在一次網絡IO讀取過程當中,數據並非直接從網卡讀取到用戶空間中的應用程序緩衝區,而是先從網卡拷貝到內核空間緩衝區,而後再從內核拷貝到用戶空間中的應用程序緩衝區。對於網絡IO寫入過程,過程則相反,先將數據從用戶空間中的應用程序緩衝區拷貝到內核緩衝區,再從內核緩衝區把數據經過網卡發送出去。

多路複用I/O模型,創建在多路事件分離函數select,poll,epoll之上。以Redis採用的epoll爲例,在發起read請求前,先更新epoll的socket監控列表,而後等待epoll函數返回(此過程是阻塞的,因此說多路複用IO本質上也是阻塞IO模型)。當某個socket有數據到達時,epoll函數返回。此時用戶線程才正式發起read請求,讀取並處理數據。這種模式用一個專門的監視線程去檢查多個socket,若是某個socket有數據到達就交給工做線程處理。因爲等待Socket數據到達過程很是耗時,因此這種方式解決了阻塞IO模型一個Socket鏈接就須要一個線程的問題,也不存在非阻塞IO模型忙輪詢帶來的CPU性能損耗的問題。多路複用IO模型的實際應用場景不少,你們耳熟能詳的Redis,Java NIO,以及Dubbo採用的通訊框架Netty都採用了這種模型。

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

下圖是基於epoll函數Socket編程的詳細流程。

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

可維護性

咱們知道,多線程能夠充分利用多核CPU,在高併發場景下,可以減小因I/O等待帶來的CPU損耗,帶來很好的性能表現。不過多線程倒是一把雙刃劍,帶來好處的同時,還會帶來代碼維護困難,線上問題難於定位和調試,死鎖等問題。多線程模型中代碼的執行過程再也不是串行的,多個線程同時訪問的共享變量若是處理不當也會帶來詭異的問題。

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

咱們經過一個例子,看一下多線程場景下發生的詭異現象。看下面的代碼:

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

flag爲true時,cal() 方法返回值是多少?不少人會說:這還用問嗎!確定返回2

結果可能會讓你大吃一驚!上面的這段代碼,因爲語句1和語句2沒有數據依賴性,可能會發生指令重排序,有可能編譯器會把flag=true放到num=1的前面。此時set和cal方法分別在不一樣線程中執行,沒有前後關係。cal方法,只要flag爲true,就會進入if的代碼塊執行相加的操做。可能的順序是:

  • 語句1先於語句2執行,這時的執行順序多是:語句1->語句2->語句3->語句4。執行語句4前,num = 1,因此cal的返回值是2
  • 語句2先於語句1執行,這時的執行順序多是:語句2->語句3->語句4->語句1。執行語句4前,num = 0,因此cal的返回值是0

咱們能夠看到,在多線程環境下若是發生了指令重排序,會對結果形成嚴重影響。

固然能夠在第三行處,給flag加上關鍵字volatile來避免指令重排。即在flag處加上了內存柵欄,來阻隔flag(柵欄)先後的代碼的重排序。固然多線程還會帶來可見性問題,死鎖問題以及共享資源安全等問題。

boolean volatile flag = false;

Redis6.0爲什麼引入多線程?

Redis6.0引入的多線程部分,實際上只是用來處理網絡數據的讀寫和協議解析,執行命令仍然是單一工做線程。

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

從上圖咱們能夠看到Redis在處理網絡數據時,調用epoll的過程是阻塞的,也就是說這個過程會阻塞線程,若是併發量很高,達到幾萬的QPS,此處可能會成爲瓶頸。通常咱們遇到此類網絡IO瓶頸的問題,能夠增長線程數來解決。開啓多線程除了能夠減小因爲網絡I/O等待形成的影響,還能夠充分利用CPU的多核優點。Redis6.0也不例外,在此處增長了多線程來處理網絡數據,以此來提升Redis的吞吐量。固然相關的命令處理仍是單線程運行,不存在多線程下併發訪問帶來的種種問題。

性能對比

壓測配置:

Redis Server: 阿里雲 Ubuntu 18.04,8 CPU 2.5 GHZ, 8G 內存,主機型號 ecs.ic5.2xlarge
Redis Benchmark Client: 阿里雲 Ubuntu 18.04,8 2.5 GHZ CPU, 8G 內存,主機型號 ecs.ic5.2xlarge

多線程版本Redis 6.0,單線程版本是 Redis 5.0.5。多線程版本須要新增如下配置:

io-threads 4 # 開啓 4 個 IO 線程
io-threads-do-reads yes # 請求解析也是用 IO 線程

壓測命令: redis-benchmark -h 192.168.0.49 -a foobared -t set,get -n 1000000 -r 100000000 --threads 4 -d ${datasize} -c 256

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

Redis6.0爲什麼引入多線程?單線程不香嗎?

 

從上面能夠看到 GET/SET 命令在多線程版本中性能相比單線程幾乎翻了一倍。另外,這些數據只是爲了簡單驗證多線程 I/O 是否真正帶來性能優化,並無針對具體的場景進行壓測,數據僅供參考。本次性能測試基於 unstble 分支,不排除後續發佈的正式版本的性能會更好。

最後

可見單線程有單線程的好處,多線程有多線程的優點,只有充分理解其中的本質原理,才能靈活運用於生產實踐當中。

相關文章
相關標籤/搜索