Redis全面解析一:redis是單線程結構爲何還可以支持高併發

前言

redis設計成單線程結構考慮:

從redis的性能上進行考慮,單線程避免了上下文頻繁切換問題,效率高;
從redis的內部結構設計原理進行考慮,redis是基於Reactor模式開發了自己的網絡事件處理器: 這個處理器被稱爲文件事件處理器

理解redis單線程

Redis客戶端對服務端的每次調用都經歷了發送命令,執行命令,返回結果三個過程。其中執行命令階段,由於Redis是單線程來處理命令的,所有到達服務端的命令都不會立刻執行,所有的命令都會進入一個隊列中,然後逐個執行,並且多個客戶端發送的命令的執行順序是不確定的,但是可以確定的是不會有兩條命令被同時執行,不會產生併發問題,這就是Redis的單線程基本模型。

如果想深入理解單線程模型,先得了解redis服務器和客戶端建立連接以及讀取數據機制。

redis服務器事件機制

Redis 採用事件驅動機制來處理大量的網絡IO。它並沒有使用 libevent 或者 libev 這樣的成熟開源方案,而是自己實現一個非常簡潔的事件驅動庫 ae_event。

Redis中的事件驅動庫只關注網絡IO,以及定時器。該事件庫處理下面兩類事件:

  • 文件事件(file  event):用於處理 Redis 服務器和客戶端之間的網絡IO。Redis服務器通過套接字與客戶端(或者其他Redis服務器)進行連接以及讀寫,而文件事件就是服務器對套接字操作的抽象。
  • 時間事件(time  eveat):Redis 服務器中的一些操作(比如serverCron函數)需要在給定的時間點執行,而時間事件就是處理這類定時操作的。

Redis服務器通過socket(套接字)與客戶端或其他Redis服務器進行連接,而文件事件就是服務器對socket操作的抽象。服務器與客戶端或其他服務器的通信會產生相應的文件事件,而服務器通過監聽並處理這些事件來完成一系列網絡通信操作。

(1)文件事件:

Redis基於Reactor模式開發了自己的網絡事件處理器,被稱爲文件事件處理器(file event handler):

  • 文件事件處理器使用I/O多路複用(multiplexing)程序來同時監聽多個套接字,並根據套接字目前執行的任務來爲套接字關聯不同的事件處理器。

  • 當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫入(write)、關閉(close)等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。

文件事件處理器的構成:文件事件處理器的四個組成部分,分別時套接字、I/O多路複用程序、文件事件分派器(dispatcher),以及事件處理器。

注意:其中I/O多路複用程序與文件事件派發器 還有一層socket層,通過隊列向文件事件分派器傳送socket

(2)I/O多路複用:

 Redis 是跑在單線程中的,所有的操作都是按照順序線性執行的,但是由於讀寫操作等待用戶輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回,這會導致某一文件的 I/O 阻塞導致整個進程無法對其它客戶提供服務,(前面已經解釋過爲什麼使用單線程,但是單線程相對多線程這種致命的缺陷:單線程效率低,同時存在io阻塞問題,其他請求將不得不等待,怎麼解決呢I/O多路複用就是爲了解決這個問題而出現的

I/O多路複用:“多路”指的是多個網絡連接,“複用”指的是複用同一個線程。解決I/O傳輸層單線程阻塞問題。

①採用多路 I/O 複用技術可以讓單個線程高效的處理多個連接請求(儘量減少網絡 IO 的時間消耗),當多個連接沒有讀寫數據時,當前線程阻塞,當有一個或多個流有 I/O事件時,就從阻塞態中喚醒,於是程序就會輪詢一遍所有的流(epoll 是隻輪詢那些真正發出了事件的流),將流的處理過程依次放入隊列中,放入隊列後I/O多路複用程序此次任務完成,並不關心任務是否執行,可進行下一次事件放入隊列的操作。通過此種方式解決了I/O訪問層單線程阻塞問題。

② Redis在內存中操作數據的速度非常快,也就是說內存內的操作不會成爲影響Redis性能的瓶頸。所以將事件依次放入隊列中逐步執行的操作耗時可以忽略。

 I/O多路複用程序負責監聽多個套接字,並向文件事件派發器傳遞那些產生了事件的套接字。儘管多個文件事件可能會併發地出現,但I/O多路複用程序總是會將所有產生的套接字都放到同一個隊列裏邊,然後文件事件處理器會以有序、同步、單個套接字的方式處理該隊列中的套接字,也就是處理就緒的文件事件。

Redis 具有很高的吞吐量保證:

(1) 網絡IO都是通過Socket實現,Server在某一個端口持續監聽,客戶端通過Socket(IP+Port)與服務器建立連接(ServerSocket.accept),成功建立連接之後,就可以使用Socket中封裝的InputStream和OutputStream進行IO交互了。針對每個客戶端,Server都會創建一個新線程專門用於處理。

(2) 默認情況下,網絡IO是阻塞模式,即服務器線程在數據到來之前處於【阻塞】狀態,等到數據到達,會自動喚醒服務器線程,着手進行處理。I/O多路複用程序負責監聽多個套接字。

(3) 爲了提升服務器線程處理效率,有以下三種思路

      a.非阻塞[忙輪詢]:採用死循環方式輪詢每一個流,如果有IO事件就處理,這樣一個線程可以處理多個流,但效率不高,容易導致CPU空轉。

      b.Select代理(無差別輪詢):可以觀察多個流的IO事件,如果所有流都沒有IO事件,則將線程進入阻塞狀態,如果有一個或多個發生了IO事件,則喚醒線程去處理。但是會遍歷所有的流,找出流需要處理的流。如果流個數爲N,則時間複雜度爲O(N)

      c.Epoll代理:Select代理有一個缺點,線程在被喚醒後輪詢所有的Stream,會存在無效操作。Epoll哪個流發生了I/O事件會通知處理線程,對流的操作都是有意義的,複雜度降低到了O(1)。

Redis 使用的IO多路複用技術主要有:

selectepollevportkqueue等。每個IO多路複用函數庫在 Redis 源碼中都對應一個單獨的文件,比如ae_select.c,ae_epoll.c, ae_kqueue.c等。Redis 會根據不同的操作系統,按照不同的優先級選擇多路複用技術。事件響應框架一般都採用該架構,比如 netty 和 libevent。

clipboard.png

文件事件處理器過程

①客戶端與redis進行通信大致流程:

å¨è¿éæå¥å¾çæè¿°

  • 首先在redis啓動初始化的時候,redis會先將事件處理器中的連接應答處理器和AE_READABLE事件關聯起來;
  • 如果客戶端向redis發起連接,會產生AE_READABLE事件(步驟A),產生該事件後會被IO多路複用程序監聽到(步驟B),然後IO多路複用程序會把監聽到的socket信息放入到隊列中(步驟C),事件分配器每次從隊列中取出一個socket(步驟D),然後事件分派器把socket給對應的事件處理器(步驟E)。由於連接應答處理器和AE_READABLE事件在redis初始化的時候已經關聯起來,所以由連接應答處理器來處理跟客戶端建立連接,然後通過ServerSocket創建一個與客戶端一對一對應的socket,如叫socket01,同時將這個socket01的AE_READABLE事件和命令請求處理器關聯起來。

②客戶端向redis發生請求時(讀、寫操作)

å¨è¿éæå¥å¾çæè¿°

  • 首先就會在對應的socket如socket01上會產生AE_READABLE事件(步驟A),產生該事件後會被IO多路複用程序監聽到(步驟B),然後IO多路複用程序會把監聽到的socket信息放入到隊列中(步驟C),事件分配器每次從隊列中取出一個socket(步驟D),然後事件分派器把socket給對應的事件處理器(步驟E)。由於命令處理器和socket01的AE_READABLE事件關聯起來了,然後對應的命令請求處理器來處理。
  • 這個命令請求處理器會從事件分配器傳遞過來的socket01上讀取相關的數據,如何執行相應的讀寫處理。操作執行完之後,redis就會將準備好相應的響應數據(如你在redis客戶端輸入 set a 123回車時會看到響應ok),並將socket01的AE_WRITABLE事件和命令回覆處理器關聯起來。

③當客戶端會查詢redis是否完成相應的操作

å¨è¿éæå¥å¾çæè¿°

  • 當客戶端會查詢redis是否完成相應的操作,就會在socket01上產生一個AE_WRITABLE事件,會由對應的命令回覆處理器來處理,就是將準備好的相應數據寫入socket01(由於socket連接是雙向的),返回給客戶端,如讀操作,客戶端會顯示ok。
  • 如果命令回覆處理器執行完成後,就會刪除這個socket01的AE_WRITABLE事件和命令回覆處理器的關聯。

注:這樣客戶端就和redis進行了一次通信。由於連接應答處理器執行一次就夠了,如果客戶端在次進行操作就會由命令請求處理器來處理,反覆執行。

擴展

(1)Redis數據庫結構

redis底層實現實際上也是大量的對象、函數。只不過是用C而不是用Java。
redis的數據庫就是一個對象redisDb,redis服務器對象redisServer內部會持有一個redisDb數組,初始的時候數組大小爲16,即redis數據庫最開始有16個,客戶端存放的數據就在這16箇中的一個。
我們知道redis是以鍵值對存儲數據的,實際上redisDb內部保存了一字典dict,字典又保存了客戶端的多個鍵值對,這個字典又被稱爲鍵空間。

讀到這裏你應該明白了,所有客戶端存儲在redis的數據都在redisServer對象的redisDb對象數組的某一個元素裏面的dict下面
redisDb還有一個expires屬性,這個屬性也是一個字典,用與保存對象的過期時間,怎麼進行數據增刪改查?瞭解hashmap的你一定知道,直接操作dict鍵空間就可以了。

(2)Redis的垃圾回收機制

Redis回收過期對象的策略:定期刪除+惰性刪除

問題1、什麼是定期刪除?

答:定期刪除就是每隔一定時間就進行一次刪除,但與其他定期刪除不同,redis定期刪除並不會刪除所有數據庫中的所有過期對象。redis會檢查某一些數據庫(redisDb數組中的一些)中的某一些鍵,如果過期就刪除,redis還會保存已經檢查到了第幾個數據庫了,下次直接在該數據庫開始檢查。redis默認情況下每隔100ms執行一次定期刪除,默認掃描16個數據庫,每隔庫檢查20個鍵。

問題2、什麼是惰性刪除?

答:當客戶端調用讀寫數據庫的命令的時候,redis會判斷這些命令涉及到的鍵是否過期,如果過期就刪除。


常見疑問

通過上述的原理以及結構表述,我們再來重新回顧下問題

1.redis爲什麼不採用多線程處理而使用單線程處理機制?

  • 多線程處理可能涉及到鎖
  • 多線程處理會涉及到線程切換而消耗CPU
  • redis基於內存,瓶頸在於物理內存,不在於執行效率

2.redis 單線程模型爲什麼效率這麼高

  • 純內存操作
  • 核心是基於非阻塞的 IO 多路複用機制解決單線程阻塞問題
  • 單線程反而避免了多線程的頻繁上下文切換問題
  • redis底層使用了一些特殊的數據結構如跳躍表等,通過這些數據結構的優化可以讓對象更快的存入內存

3.Redis不存在線程安全問題

Redis採用了線程封閉的方式,把任務封閉在一個線程,自然避免了線程安全問題,不過對於需要依賴多個redis操作(即:多個Redis操作命令)的複合操作來說,依然需要鎖,而且有可能是分佈式鎖。

注:redis單線程機制無法發揮多核CPU性能,不過可以通過在單機開多個Redis實例來完善

 

文章參考:

https://baijiahao.baidu.com/s?id=1644978229039981414&wfr=spider&for=pc

https://segmentfault.com/a/1190000020014518

https://blog.csdn.net/qq_38601777/article/details/91325622

https://www.cnblogs.com/myseries/p/11733861.html

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章