本篇博客已被收錄GitHub:https://zhouwenxing.github.io/
在平常開發中,爲了保證數據的一致性,咱們通常都選擇關係型數據庫來存儲數據,如 MySQL
,Oracle
等,由於關係型數據庫有着事務的特性。然而在併發量比較大的業務場景,關係型數據庫卻又每每會成爲系統瓶頸,沒法徹底知足咱們的需求,因此就須要使用到緩存,而非關係型數據庫,即 NoSQL
數據庫每每又會成爲最佳選擇。git
NoSQL
數據庫最多見的解釋是 non-relational
,也有人解釋爲 Not Only SQL
。非關係型數據庫不保證事務,也就是不具有事務 ACID
特性,這也是非關係型數據庫和關係型數據庫最大的區別,而咱們即將介紹的 Redis
就屬於 NoSQL
數據庫的一種。github
Redis
全稱是:REmote DIctionary Service
,即遠程字典服務。Redis
是一個開源的(遵照 BSD
協議)、支持網絡、可基於內存亦可持久化的日誌型、Key-Value
數據庫。
Redis
具備如下特性:redis
API
。tar -zxvf redis-5.0.5.tar.gz
進行解壓。Redis
主目錄,執行命令 make && make install PREFIX=/xxx/xxx/redis-5.0.5
進行安裝,若是不指定目錄,則默認是安裝在 /usr/local
目錄下。Redis
主目錄下多了一個 bin
目錄,bin
目錄內包含了一些可執行腳本。Redis
主目錄下,找到 redis.conf
配置文件,將其中的配置 daemonize no
修改成 daemonize yes
,表示在後臺啓動服務。/xxx/xxx/redis-5.0.5/bin/redis-server /xxx/xxx/redis-5.0.5/redis.conf
啓動 Redis
服務。你們可能都知道 Redis
很快,但是 Redis
到底能有多快呢,好比 Redis
的吞吐量能達到多少?我想這就不是每個人都能說的上來一個具體的數字了。數據庫
Redis
官方提供了一個測試腳本,能夠供咱們測試 Redis
的 吞吐量。編程
redis-benchmark -q -n 100000
能夠測試經常使用命令的吞吐量。redis-benchmark -t set,lpush -n 100000 -q
測試 Redis
處理 set
和 lpush
命令的吞吐量。redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
測試 Redis
處理 Lua
腳本等吞吐量。下圖就是我這邊執行第一條命令的自測結果,能夠看到大部分命令的吞吐量均可以達到 4
萬以上,也就是說每秒鐘能夠處理 4
萬次以上請求:緩存
可是若是你覺得這就是 Redis
的真實吞吐量,那就錯了。實際上,Redis
官方的測試結果是能夠達到 10
萬的吞吐量,下圖就是官方提供的一個基準測試結果(縱座標就是吞吐量,橫座標是鏈接數):安全
這個問題比較經典,由於在不少人的認知裏,Redis
就是單線程的。然而 Redis
從 4.0
版本開始就有了多線程的概念,雖然處理命令請求的核心模塊確實是保證了單線程執行,然而在其餘許多地方已經有了多線程,好比:在後臺刪除對象,經過 Redis
模塊實現阻塞命令,生成 dump
文件,以及 6.0
版本中網絡 I/O
實現了多線程等,並且在將來 Redis
應該會有愈來愈多的模塊實現多線程。服務器
所謂的單線程,只是說 Redis
的處理客戶端的請求(即執行命令)時,是單線程去執行的,並非說整個 Redis
都是單線程。網絡
Redis
爲何會選擇使用單線程呢?這是由於 CPU
成爲 Redis
瓶頸的狀況並不常見,成爲 Redis
瓶頸的一般是內存或網絡帶寬。例如,在一個普通的 Linux
系統上使用 pipelining
命令,Redis
能夠每秒完成 100
萬個請求,因此若是咱們的應用程序主要使用 O(N)
或 O(log(N))
複雜度的命令,它幾乎不會使用太多的 CPU
。多線程
那麼既然 CPU
不會成爲瓶頸,理所固然的就不必去使用多線程來執行命令,咱們須要明確的一個問題就是多線程必定比單線程快嗎?答案是不必定。由於多線程也是有代價的,最直接的兩個代價就是線程的建立和銷燬線程(固然能夠經過線程池來必定程度的減小頻繁的建立線程和銷燬線程)以及線程的上下文切換。
在咱們的平常系統中,主要能夠區分爲兩種:CPU
密集型 和 IO
密集型。
CPU
的利用率很高,那麼使用多線程反而會增長上下文切換而帶來額外的開銷,因此使用多線程效率可能會不升反降。舉個例子:假如你如今在幹活,你一直不停的在作一件事,須要 1
分鐘能夠作完,可是你中途老是被人打斷,須要花 1
秒鐘時間步行到旁邊去作另外一件事,假如這件事也須要 1
分鐘,那麼你由於反覆切換作兩件事,每切換一次就要花 1
秒鐘,最後作完這 2
件事的時間確定大於 2
分鐘(取決於中途切換的次數),可是若是中途不被打斷,你作完一件事再去作另外一件事,那麼你最多隻須要切換 1
次,也就是 2
分 1
秒就能作完。IO
操做也能夠分爲磁盤 IO
和網絡 IO
等操做。大部分 IO
操做的特色是比較耗時且 CPU
利用率不高,因此 Redis 6.0
版本網絡 IO
會改進爲多線程。至於磁盤 IO
,由於 Redis
中的數據都存儲在內存(也能夠持久化),因此並不會過多的涉及到磁盤操做。舉個例子:假如你如今給樹苗澆水,你每澆完一次水以後就須要等別人給你加水以後你才能繼續澆,那麼假如這個等待過程須要 5
秒鐘,也就是說你澆完一次水就能夠休息 5
秒鐘,而你切換去作另外一件事來回只須要 2
秒,那麼你徹底能夠先去作另外一件事,作完以後再回來,這樣就能夠充分利用你空閒的 5
秒鐘時間,從而提高了效率。使用多線程還會帶來一個問題就是數據的安全性,因此多線程編程都會涉及到鎖競爭,由此也會帶來額外的開銷。
I/O
指的是網絡 I/O
, 多路指的是多個 TCP
鏈接(如 Socket
),複用指的是複用一個或多個線程。I/O
多路複用的核心原理就是再也不由應用程序本身來監聽鏈接,而是由服務器內核替應用程序監聽。
在 Redis
中,其多路複用有多種實現,如:select
,epoll
,evport
,kqueue
等。
咱們用去餐廳吃飯爲的例子來解釋一下 I/O
多路複用機制(點餐人至關於客戶端,餐廳的廚房至關於服務器,廚師就是線程)。
IO
:張三去餐廳吃飯,點了一道菜,這時候他啥事也不幹了,就是一直等,等到廚師炒好菜,他就把菜端走開始吃飯了。也就是在菜被炒好以前,張三被阻塞了,這就是 BIO
(阻塞 IO
),效率會很是低下。IO
:張三去餐廳吃飯,點了一道菜,這時候張三他不會一直等,找了個位置坐下,刷刷抖音,打打電話,作點其餘事,而後每隔一段時間就去廚房問一下本身的菜好了沒有。這種就屬於非阻塞 IO
,這種方式雖然能夠提升性能,可是若是有大量 IO
都來按期輪詢,也會給服務器形成很是大的負擔。select
模型,不過 select
模型最多隻能監聽 1024
個 socket
(poll
模型解決了這個限制問題)。epoll
模型。須要注意的是在 IO
多路複用機制下,客戶端能夠阻塞也能夠選擇不阻塞(大部分場景下是阻塞 IO
),這個要具體狀況具體分析,可是在多路複用機制下,服務端就能夠經過多線程(上面示例中能夠多幾個廚師同時炒菜)來提高併發效率。
Redis
服務器是一個事件驅動程序,服務器須要處理兩類事件:文件事件和時間事件。
Redis
服務器和客戶端(或其餘服務器)進行通訊會產生相應的文件事件,而後服務器經過監聽並處理這些事件來完成一系列的通訊操做。Redis
內部的一些在給定時間以內須要進行的操做。Redis
的文件事件處理器以單線程的方式運行,其內部使用了 I/O
多路複用程序來同時監聽多個套接字(Socket
)鏈接,提高了性能的同時又保持了內部單線程設計的簡單性。下圖就是文件事件處理器的示意圖:
I/O
多路複用程序雖然會同時監聽多個 Socket
鏈接,可是其會將監聽的 Socket
都放到一個隊列裏面,而後經過這個隊列有序的,同步的將每一個 Socket
對應的事件傳送給文件事件分派器,再由文件事件分派器分派給對應的事件處理器進行處理,只有當一個 Socket
所對應的事件被處理完畢以後,I/O
多路複用程序纔會繼續向文件事件分派器傳送下一個 Socket
所對應的事件,這也能夠驗證上面的結論,處理客戶端的命令請求是單線程的方式逐個處理,可是事件處理器內並非只有一個線程。
Redis
爲何這麼快的緣由前面已經基本提到了,如今咱們再進行總結一下:
Redis
是一款純內存結構,避免了磁盤 I/O
等耗時操做。Redis
命令處理的核心模塊爲單線程,減小了鎖競爭,以及頻繁建立線程和銷燬線程的代價,減小了線程上下文切換的消耗。I/O
多路複用機制,大大提高了併發效率。