自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文弄懂Redis為什么這么快?

數(shù)據(jù)庫 其他數(shù)據(jù)庫 Redis
大家在工作里面也肯定和 Redis 打過交道,但是對于Redis 為什么快,除了對八股文的背誦,好像都還沒特別深入的了解。

前言

說起當(dāng)前主流NoSql數(shù)據(jù)庫非 Redis 莫屬。因為它讀寫速度極快,一般用于緩存熱點數(shù)據(jù)加快查詢速度,大家在工作里面也肯定和 Redis 打過交道,但是對于Redis 為什么快,除了對八股文的背誦,好像都還沒特別深入的了解。

今天我們一起深入的了解下redis吧:

高效的數(shù)據(jù)結(jié)構(gòu)

Redis 的底層數(shù)據(jù)結(jié)構(gòu)一共有6種,分別是,簡單動態(tài)字符串,雙向鏈表,壓縮列表,哈希表,跳表和整數(shù)數(shù)組,它們和數(shù)據(jù)類型的對應(yīng)關(guān)系如下圖所示:

本文暫時按下不表,后續(xù)會針對以上所有數(shù)據(jù)結(jié)構(gòu)進(jìn)行源碼級深入分析

單線程vs多線程

多線程VS單線程

在學(xué)習(xí)計算機操作系統(tǒng)時一定遇到過這個問題:多線程一定比單線程快嗎? 相信各位看官們一定不會像上面的傻哪吒一樣落入敖丙的圈套中。

多線程有時候確實比單線程快,但也有很多時候沒有單線程那么快。首先用一張3歲小孩都能看懂的圖解釋并發(fā)與并行的區(qū)別:

  • 并發(fā)(concurrency):指在同一時刻只能有一條指令執(zhí)行,但多個進(jìn)程指令被快速的輪換執(zhí)行,使得在宏觀上具有多個進(jìn)程同時執(zhí)行的效果,但在微觀上并不是同時執(zhí)行的,只是把時間分成若干段,使多個進(jìn)程快速交替的執(zhí)行。
  • 并行(parallel):指在同一時刻,有多條指令在多個處理器上同時執(zhí)行。所以無論從微觀還是從宏觀來看,二者都是一起執(zhí)行的。

不難發(fā)現(xiàn)并發(fā)在同一時刻只有一條指令執(zhí)行,只不過進(jìn)程(線程)在CPU中快速切換,速度極快,給人看起來就是“同時運行”的印象,實際上同一時刻只有一條指令進(jìn)行。但實際上如果我們在一個應(yīng)用程序中使用了多線程,線程之間的輪換以及上下文切換是需要花費很多時間的。

何同學(xué)

Talk is cheap,Show me the code

如下代碼演示了串行和并發(fā)執(zhí)行并累加操作的時間:

  1. public class ConcurrencyTest { 
  2.     private static final long count = 1000000000; 
  3.  
  4.     public static void main(String[] args) { 
  5.         try { 
  6.             concurrency(); 
  7.         } catch (InterruptedException e) { 
  8.             e.printStackTrace(); 
  9.         } 
  10.         serial(); 
  11.     } 
  12.  
  13.     private static void concurrency() throws InterruptedException { 
  14.         long start = System.currentTimeMillis(); 
  15.         Thread thread = new Thread(new Runnable() { 
  16.  
  17.             @Override 
  18.             public void run() { 
  19.                  int a = 0; 
  20.                  for (long i = 0; i < count; i++) 
  21.                  { 
  22.                      a += 5; 
  23.                  } 
  24.             } 
  25.         }); 
  26.         thread.start(); 
  27.         int b = 0; 
  28.         for (long i = 0; i < count; i++) { 
  29.             b--; 
  30.         } 
  31.         thread.join(); 
  32.         long time = System.currentTimeMillis() - start; 
  33.         System.out.println("concurrency : " + time + "ms,b=" + b); 
  34.     } 
  35.  
  36.     private static void serial() { 
  37.         long start = System.currentTimeMillis(); 
  38.         int a = 0; 
  39.         for (long i = 0; i < count; i++) 
  40.         { 
  41.             a += 5; 
  42.         } 
  43.         int b = 0; 
  44.         for (long i = 0; i < count; i++) { 
  45.             b--; 
  46.         } 
  47.         long time = System.currentTimeMillis() - start; 
  48.         System.out.println("serial : " + time + "ms,b=" + b); 
  49.     } 
  50.  

執(zhí)行時間如下表所示,不難發(fā)現(xiàn),當(dāng)并發(fā)執(zhí)行累加操作不超過百萬次時,速度會比串行執(zhí)行累加操作要慢。

由于線程有創(chuàng)建和上下文切換的開銷,導(dǎo)致并發(fā)執(zhí)行的速度會比串行慢的情況出現(xiàn)。

上下文切換

多個線程可以執(zhí)行在單核或多核CPU上,單核CPU也支持多線程執(zhí)行代碼,CPU通過給每個線程分配CPU時間片(機會)來實現(xiàn)這個機制。CPU為了執(zhí)行多個線程,就需要不停的切換執(zhí)行的線程,這樣才能保證所有的線程在一段時間內(nèi)都有被執(zhí)行的機會。

此時,CPU分配給每個線程的執(zhí)行時間段,稱作它的時間片。CPU時間片一般為幾十毫秒。CPU通過時間片分配算法來循環(huán)執(zhí)行任務(wù),當(dāng)前任務(wù)執(zhí)行一個時間片后切換到下一個任務(wù)。

但是,在切換前會保存上一個任務(wù)的狀態(tài),以便下次切換回這個任務(wù)時,可以再加載這個任務(wù)的狀態(tài)。所以任務(wù)從保存到再加載的過程就是一次上下文切換。

根據(jù)多線程的運行狀態(tài)來說明:多線程環(huán)境中,當(dāng)一個線程的狀態(tài)由Runnable轉(zhuǎn)換為非Runnable(Blocked、Waiting、Timed_Waiting)時,相應(yīng)線程的上下文信息(包括CPU的寄存器和程序計數(shù)器在某一時間點的內(nèi)容等)需要被保存,以便相應(yīng)線程稍后再次進(jìn)入Runnable狀態(tài)時能夠在之前的執(zhí)行進(jìn)度的基礎(chǔ)上繼續(xù)前進(jìn)。而一個線程從非Runnable狀態(tài)進(jìn)入Runnable狀態(tài)可能涉及恢復(fù)之前保存的上下文信息。這個對線程的上下文進(jìn)行保存和恢復(fù)的過程就被稱為上下文切換。

基于內(nèi)存

以MySQL為例,MySQL的數(shù)據(jù)和索引都是持久化保存在磁盤上的,因此當(dāng)我們使用SQL語句執(zhí)行一條查詢命令時,如果目標(biāo)數(shù)據(jù)庫的索引還沒被加載到內(nèi)存中,那么首先要先把索引加載到內(nèi)存,再通過若干尋址定位和磁盤I/O,把數(shù)據(jù)對應(yīng)的磁盤塊加載到內(nèi)存中,最后再讀取數(shù)據(jù)。

如果是機械硬盤,那么首先需要找到數(shù)據(jù)所在的位置,即需要讀取的磁盤地址??梢钥纯催@張示意圖:

磁盤結(jié)構(gòu)示意圖

讀取硬盤上的數(shù)據(jù),第一步就是找到所需的磁道,磁道就是以中間軸為圓心的圓環(huán),首先我們需要找到所需要對準(zhǔn)的磁道,并將磁頭移動到對應(yīng)的磁道上,這個過程叫做尋道。

然后,我們需要等到磁盤轉(zhuǎn)動,讓磁頭指向我們需要讀取的數(shù)據(jù)開始的位置,這里耗費的時間稱為旋轉(zhuǎn)延遲,平時我們說的硬盤轉(zhuǎn)速快慢,主要影響的就是耗費在這里的時間,而且這個轉(zhuǎn)動的方向是單向的,如果錯過了數(shù)據(jù)的開頭位置,就必須等到盤片旋轉(zhuǎn)到下一圈的時候才能開始讀。

最后,磁頭開始讀取記錄著磁盤上的數(shù)據(jù),這個原理其實與光盤的讀取原理類似,由于磁道上有一層磁性介質(zhì),當(dāng)磁頭掃過特定的位置,磁頭感應(yīng)不同位置的磁性狀態(tài)就可以將磁信號轉(zhuǎn)換為電信號。

可以看到,無論是磁頭的移動還是磁盤的轉(zhuǎn)動,本質(zhì)上其實都是機械運動,這也是為什么這種硬盤被稱為機械硬盤,而機械運動的效率就是磁盤讀寫的瓶頸。

扯得有點遠(yuǎn)了,我們說回redis,如果像Redis這樣把數(shù)據(jù)存在內(nèi)存中,讀寫都直接對數(shù)據(jù)庫進(jìn)行操作,天然地就比硬盤數(shù)據(jù)庫少了到磁盤讀取數(shù)據(jù)的這一步,而這一步恰恰是計算機處理I/O的瓶頸所在。

在內(nèi)存中讀取數(shù)據(jù),本質(zhì)上是電信號的傳遞,比機械運動傳遞信號要快得多。

硬盤數(shù)據(jù)庫讀取流程

 

 

內(nèi)存數(shù)據(jù)庫讀取流程

因此,可以負(fù)責(zé)任地說,Redis這么快當(dāng)然跟它基于內(nèi)存運行有著很大的關(guān)系。但是,這還遠(yuǎn)遠(yuǎn)不是全部的原因。

Redis FAQ

面對單線程的 Redis 你也許又會有疑問:敖丙,我的多核CPU發(fā)揮不了作用了呀!別急,Redis 針對這個問題專門進(jìn)行了解答。

 

CPU成為Redis性能瓶頸的情況并不常見,因為Redis通常會受到內(nèi)存或網(wǎng)絡(luò)的限制。例如,在 Linux 系統(tǒng)上使用流水線 Redis 每秒甚至可以提供 100 萬個請求,所以如果你的應(yīng)用程序主要使用O(N)或O(log(N))命令,它幾乎不會占用太多的CPU。

然而,為了最大化CPU利用率,你可以在同一個節(jié)點中啟動多個Redis實例,并將它們視為不同的Redis服務(wù)。在某些情況下,一個單獨的節(jié)點可能是不夠的,所以如果你想使用多個cpu,你可以開始考慮一些更早的分片方法。

你可以在Partitioning頁面中找到更多關(guān)于使用多個Redis實例的信息。

然而,在Redis 4.0中,我們開始讓Redis更加線程化。目前這僅限于在后臺刪除對象,以及阻塞通過Redis模塊實現(xiàn)的命令。對于未來的版本,我們的計劃是讓Redis變得越來越多線程。

注意:我們一直說的 Redis 單線程,只是在處理我們的網(wǎng)絡(luò)請求的時候只有一個線程來處理,一個正式的Redis Server運行的時候肯定是不止一個線程的!

例如Redis進(jìn)行持久化的時候會 fork了一個子進(jìn)程 執(zhí)行持久化操作

四種IO模型

當(dāng)一個網(wǎng)絡(luò)IO發(fā)生(假設(shè)是read)時,它會涉及兩個系統(tǒng)對象,一個是調(diào)用這個IO的進(jìn)程,另一個是系統(tǒng)內(nèi)核。

當(dāng)一個read操作發(fā)生時,它會經(jīng)歷兩個階段:

①等待數(shù)據(jù)準(zhǔn)備;

②將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中。

為了解決網(wǎng)絡(luò)IO中的問題,提出了4中網(wǎng)絡(luò)IO模型:

  • 阻塞IO模型
  • 非阻塞IO模型
  • 多路復(fù)用IO模型
  • 異步IO模型

阻塞和非阻塞的概念描述的是用戶線程調(diào)用內(nèi)核IO操作的方式:阻塞時指IO操作需要徹底完成后才返回到用戶空間;而非阻塞是指IO操作被調(diào)用后立即返回給用戶一個狀態(tài)值,不需要等到IO操作徹底完成。

阻塞IO模型

在Linux中,默認(rèn)情況下所有socket都是阻塞的,一個典型的讀操作如下圖所示:

當(dāng)應(yīng)用進(jìn)程調(diào)用了recvfrom這個系統(tǒng)調(diào)用后,系統(tǒng)內(nèi)核就開始了IO的第一個階段:準(zhǔn)備數(shù)據(jù)。

對于網(wǎng)絡(luò)IO來說,很多時候數(shù)據(jù)在一開始還沒到達(dá)時(比如還沒有收到一個完整的TCP包),系統(tǒng)內(nèi)核就要等待足夠的數(shù)據(jù)到來。而在用戶進(jìn)程這邊,整個進(jìn)程會被阻塞。

當(dāng)系統(tǒng)內(nèi)核一直等到數(shù)據(jù)準(zhǔn)備好了,它就會將數(shù)據(jù)從系統(tǒng)內(nèi)核中拷貝到用戶內(nèi)存中,然后系統(tǒng)內(nèi)核返回結(jié)果,用戶進(jìn)程才解除阻塞的狀態(tài),重新運行起來。所以,阻塞IO模型的特點就是在IO執(zhí)行的兩個階段(等待數(shù)據(jù)和拷貝數(shù)據(jù))都被阻塞了。

非阻塞IO模型

在Linux中,可以通過設(shè)置socket使IO變?yōu)榉亲枞麪顟B(tài)。當(dāng)對一個非阻塞的socket執(zhí)行read操作時,讀操作流程如下圖所示:

從圖中可以看出,當(dāng)用戶進(jìn)程發(fā)出 read 操作時,如果內(nèi)核中的數(shù)據(jù)還沒有準(zhǔn)備好,那么它不會阻塞用戶進(jìn)程,而是立刻返回一個錯誤。

從用戶進(jìn)程角度講,它發(fā)起一個read操作后,并不需要等待,而是馬上就得到了一個結(jié)果。當(dāng)用戶進(jìn)程判斷結(jié)果是一個錯誤時,它就知道數(shù)據(jù)還沒有準(zhǔn)備好,于是它可以再次發(fā)送read操作。

一旦內(nèi)核中的數(shù)據(jù)準(zhǔn)備好了,并且又再次收到了用戶進(jìn)程的系統(tǒng)調(diào)用,那么它馬上就將數(shù)據(jù)復(fù)制到了用戶內(nèi)存中,然后返回正確的返回值。

所以,在非阻塞式IO中,用戶進(jìn)程其實需要不斷地主動詢問kernel數(shù)據(jù)是否準(zhǔn)備好。非阻塞的接口相比阻塞型接口的顯著差異在于被調(diào)用之后立即返回。

多路復(fù)用IO模型

多路IO復(fù)用,有時也稱為事件驅(qū)動IO(Reactor設(shè)計模式)。它的基本原理就是有個函數(shù)會不斷地輪詢所負(fù)責(zé)的所有socket,當(dāng)某個socket有數(shù)據(jù)到達(dá)了,就通知用戶進(jìn)程,多路IO復(fù)用模型的流程如圖所示:

當(dāng)用戶進(jìn)程調(diào)用了select,那么整個進(jìn)程會被阻塞,而同時,內(nèi)核會"監(jiān)視"所有select負(fù)責(zé)的socket,當(dāng)任何一個socket中的數(shù)據(jù)準(zhǔn)備好了,select就會返回。這個時候用戶進(jìn)程再調(diào)用read操作,將數(shù)據(jù)從內(nèi)核拷貝到用戶進(jìn)程。

這個模型和阻塞IO的模型其實并沒有太大的不同,事實上還更差一些。因為這里需要使用兩個系統(tǒng)調(diào)用(select和recvfrom),而阻塞IO只調(diào)用了一個系統(tǒng)調(diào)用(recvfrom)。但是,用select的優(yōu)勢在于它可以同時處理多個連接。所以,如果系統(tǒng)的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用多線程的阻塞IO的web server性能更好,可能延遲還更大;select/epoll的優(yōu)勢并不是對單個連接能處理得更快,而是在于能處理更多的連接。

如果select()發(fā)現(xiàn)某句柄捕捉到了"可讀事件",服務(wù)器程序應(yīng)及時做recv()操作,并根據(jù)接收到的數(shù)據(jù)準(zhǔn)備好待發(fā)送數(shù)據(jù),并將對應(yīng)的句柄值加入writefds,準(zhǔn)備下一次的"可寫事件"的select()檢測。同樣,如果select()發(fā)現(xiàn)某句柄捕捉到"可寫事件",則程序應(yīng)及時做send()操作,并準(zhǔn)備好下一次的"可讀事件"檢測準(zhǔn)備。

如下圖展示了基于事件驅(qū)動的工作模型,當(dāng)不同的事件產(chǎn)生時handler將感應(yīng)到并執(zhí)行相應(yīng)的事件,像一個多路開關(guān)似的。

IO多路復(fù)用是最常使用的IO模型,但是其異步程度還不夠“徹底”,因為它使用了會阻塞線程的select系統(tǒng)調(diào)用。因此IO多路復(fù)用只能稱為異步阻塞IO,而非真正的異步IO。

異步IO模型

“真正”的異步IO需要操作系統(tǒng)更強的支持。如下展示了異步 IO 模型的運行流程(Proactor設(shè)計模式):

用戶進(jìn)程發(fā)起read操作之后,立刻就可以開始去做其他的事;而另一方面,從內(nèi)核的角度,當(dāng)它收到一個異步的read請求操作之后,首先會立刻返回,所以不會對用戶進(jìn)程產(chǎn)生任何阻塞。

然后,內(nèi)核會等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存中,當(dāng)這一切都完成之后,內(nèi)核會給用戶進(jìn)程發(fā)送一個信號,返回read操作已完成的信息。

IO模型總結(jié)

調(diào)用阻塞IO會一直阻塞住對應(yīng)的進(jìn)程直到操作完成,而非阻塞IO在內(nèi)核還在準(zhǔn)備數(shù)據(jù)的情況下會立刻返回。

兩者的區(qū)別就在于同步IO進(jìn)行IO操作時會阻塞進(jìn)程。按照這個定義,之前所述的阻塞IO、非阻塞IO及多路IO復(fù)用都屬于同步IO。實際上,真實的IO操作,就是例子中的recvfrom這個系統(tǒng)調(diào)用。

非阻塞IO在執(zhí)行recvfrom這個系統(tǒng)調(diào)用的時候,如果內(nèi)核的數(shù)據(jù)沒有準(zhǔn)備好,這時候不會阻塞進(jìn)程。但是當(dāng)內(nèi)核中數(shù)據(jù)準(zhǔn)備好時,recvfrom會將數(shù)據(jù)從內(nèi)核拷貝到用戶內(nèi)存中,這個時候進(jìn)程則被阻塞。

而異步IO則不一樣,當(dāng)進(jìn)程發(fā)起IO操作之后,就直接返回,直到內(nèi)核發(fā)送一個信號,告訴進(jìn)程IO已完成,則在這整個過程中,進(jìn)程完全沒有被阻塞。

各個IO模型的比較如下圖所示:

Redis中的應(yīng)用

Redis服務(wù)器是一個事件驅(qū)動程序,服務(wù)器需要處理以下兩類事件:

  • 文件事件:Redis服務(wù)端通過套接字與客戶端(或其他Redis服務(wù)器)進(jìn)行連接,而文件事件就是服務(wù)器對套接字操作的抽象。服務(wù)器與客戶端(或者其他服務(wù)器)的通信會產(chǎn)生相應(yīng)的文件事件,而服務(wù)器則通過監(jiān)聽并處理這些事件來完成一系列網(wǎng)絡(luò)通信操作。
  • 時間事件:Redis服務(wù)器中的一些操作(如serverCron)函數(shù)需要在給定的時間點執(zhí)行,而時間事件就是服務(wù)器對這類定時操作的抽象。

I/O多路復(fù)用程序

Redis的 I/O 多路復(fù)用程序的所有功能都是通過包裝常見的select、epoll、evport、kqueue這些多路復(fù)用函數(shù)庫來實現(xiàn)的。

因為Redis 為每個 I/O 多路復(fù)用函數(shù)庫都實現(xiàn)了相同的API,所以I/O多路復(fù)用程序的底層實現(xiàn)是可以互換的。

Redis 在 I/O 多路復(fù)用程序的實現(xiàn)源碼中用 #include 宏定義了相應(yīng)的規(guī)則,程序會在編譯時自動選擇系統(tǒng)中性能最高的 I/O 多路復(fù)用函數(shù)庫來作為 Redis 的 I/O 多路復(fù)用程序的底層實現(xiàn)(ae.c文件):

  1. /* Include the best multiplexing layer supported by this system. 
  2.  * The following should be ordered by performances, descending. */ 
  3. #ifdef HAVE_EVPORT 
  4. #include "ae_evport.c" 
  5. #else 
  6.     #ifdef HAVE_EPOLL 
  7.     #include "ae_epoll.c" 
  8.     #else 
  9.         #ifdef HAVE_KQUEUE 
  10.         #include "ae_kqueue.c" 
  11.         #else 
  12.         #include "ae_select.c" 
  13.         #endif 
  14.     #endif 
  15. #endif 

文件事件處理器

Redis基于 Reactor 模式開發(fā)了自己的網(wǎng)絡(luò)事件處理器:這個處理器被稱為文件事件處理器:

  • 文件事件處理器使用 I/O 多路復(fù)用程序來同時監(jiān)聽多個套接字,并根據(jù)套接字目前執(zhí)行的任務(wù)來為套接字關(guān)聯(lián)不同的事件處理器。
  • 當(dāng)被監(jiān)聽的套接字準(zhǔn)備好執(zhí)行連接應(yīng)答(accept)、讀取(read)、寫入(write)、關(guān)閉(close)等操作時,與操作相對應(yīng)的文件事件就會產(chǎn)生,這時文件事件處理器就會調(diào)用套接字之前關(guān)聯(lián)好的事件處理器來處理這些事件。

下圖展示了文件事件處理器的四個組成部分:套接字、I/O多路復(fù)用程序、文件事件分派器(dispatcher)、事件處理器。

文件事件是對套接字操作的抽象,每當(dāng)一個套接字準(zhǔn)備好執(zhí)行連接應(yīng)答、寫入、讀取、關(guān)閉等操作時,就會產(chǎn)生一個文件事件。因為一個服務(wù)器通常會連接多個套接字,所以多個文件事件有可能會并發(fā)地出現(xiàn)。I/O 多路復(fù)用程序負(fù)責(zé)監(jiān)聽多個套接字,并向文件事件分派器傳送那些產(chǎn)生了事件的套接字。

哪吒問的問題很棒,聯(lián)想一下,生活中一群人去食堂打飯,阿姨說的最多的一句話就是:排隊啦!排隊啦!一個都不會少!

沒錯,一切來源生活!Redis 的 I/O多路復(fù)用程序總是會將所有產(chǎn)生事件的套接字都放到一個隊列里面,然后通過這個隊列,以有序、同步、每次一個套接字的方式向文件事件分派器傳送套接字。當(dāng)上一個套接字產(chǎn)生的事件被處理完畢之后,I/O 多路復(fù)用程序才會繼續(xù)向文件事件分派器傳送下一個套接字。

Redis為文件事件處理器編寫了多個處理器,這些事件處理器分別用于實現(xiàn)不同的網(wǎng)絡(luò)通信需求:

  • 為了對連接服務(wù)器的各個客戶端進(jìn)行應(yīng)答,服務(wù)器要為監(jiān)聽套接字關(guān)聯(lián)連接應(yīng)答處理器;
  • 為了接受客戶端傳來的命令請求,服務(wù)器要為客戶端套接字關(guān)聯(lián)命令請求處理器 ;
  • 為了向客戶端返回命令的執(zhí)行結(jié)果,服務(wù)器要為客戶端套接字關(guān)聯(lián)命令回復(fù)處理器 ;
  • 當(dāng)主服務(wù)器和從服務(wù)器進(jìn)行復(fù)制操作時,主從服務(wù)器都需要關(guān)聯(lián)特別為復(fù)制功能編寫的復(fù)制處理器。

連接應(yīng)答處理器

networking.c/acceptTcpHandler函數(shù)是Redis的連接應(yīng)答處理器,這個處理器用于對連接服務(wù)器監(jiān)聽套接字的客戶端進(jìn)行應(yīng)答,具體實現(xiàn)為sys/socket.h/acccept函數(shù)的包裝。

當(dāng)Redis服務(wù)器進(jìn)行初始化的時候,程序會將這個連接應(yīng)答處理器和服務(wù)器監(jiān)聽套接字的AE_READABLE時間關(guān)聯(lián)起來,當(dāng)有客戶端用sys/socket.h/connect函數(shù)連接服務(wù)器監(jiān)聽套接字的時候,套接字就會產(chǎn)生AE_READABLE事件,引發(fā)連接應(yīng)答處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字應(yīng)答操作。

命令請求處理器

networking.c/readQueryFromClient函數(shù)是Redis的命令請求處理器,這個處理器負(fù)責(zé)從套接字中讀入客戶端發(fā)送的命令請求內(nèi)容,具體實現(xiàn)為unistd.h/read函數(shù)的包裝。

當(dāng)一個客戶端通過連接應(yīng)答處理器成功連接到服務(wù)器之后,服務(wù)器會將客戶端套接字的AE_READABLE事件和命令請求處理器關(guān)聯(lián)起來,當(dāng)客戶端向服務(wù)器發(fā)送命令請求的時候,套接字就會產(chǎn)生AE_READABLE事件,引發(fā)命令請求處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字讀入操作。

在客戶端連接服務(wù)器的整個過程中,服務(wù)器都會一直為客戶端套接字AE_READABLE事件關(guān)聯(lián)命令請求處理器。

命令回復(fù)處理器

networking.c/sendReplyToClient函數(shù)是Redis的命令回復(fù)處理器,這個處理器負(fù)責(zé)從服務(wù)器執(zhí)行命令后得到的命令回復(fù)通過套接字返回給客戶端,具體實現(xiàn)為unistd.h/write函數(shù)的包裝。

當(dāng)服務(wù)器有命令回復(fù)需要傳送給客戶端的時候,服務(wù)器會將客戶端套接字的AE_WRITABLE事件和命令回復(fù)處理器關(guān)聯(lián)起來,當(dāng)客戶端準(zhǔn)備好接收服務(wù)器傳回的命令回復(fù)時,就會產(chǎn)生AE_WRITABLE事件,引發(fā)命令回復(fù)處理器執(zhí)行,并執(zhí)行相應(yīng)的套接字寫入操作。

當(dāng)命令回復(fù)發(fā)送完畢之后,服務(wù)器就會解除命令回復(fù)處理器與客戶端套接字的AE_WRITABLE事件之間的關(guān)聯(lián)。 

小總結(jié)

一句話描述 IO 多路復(fù)用在 Redis 中的應(yīng)用:Redis 將所有產(chǎn)生事件的套接字都放到一個隊列里面,以有序、同步、每次一個套接字的方式向文件事件分派器傳送套接字,文件事件分派器根據(jù)套接字對應(yīng)的事件選擇響應(yīng)的處理器進(jìn)行處理,從而實現(xiàn)了高效的網(wǎng)絡(luò)請求。

Redis的自定義協(xié)議

Redis客戶端使用RESP(Redis的序列化協(xié)議)協(xié)議與Redis的服務(wù)器端進(jìn)行通信。它實現(xiàn)簡單,解析快速并且人類可讀。

RESP 支持以下數(shù)據(jù)類型:簡單字符串、錯誤、整數(shù)、批量字符串和數(shù)組。

RESP 在 Redis 中用作請求-響應(yīng)協(xié)議的方式如下:

  • 客戶端將命令作為批量字符串的 RESP 數(shù)組發(fā)送到 Redis 服務(wù)器。
  • 服務(wù)器根據(jù)命令實現(xiàn)以其中一種 RESP 類型進(jìn)行回復(fù)。

在 RESP 中,某些數(shù)據(jù)的類型取決于第一個字節(jié):

  • 對于簡單字符串,回復(fù)的第一個字節(jié)是“+”
  • 對于錯誤,回復(fù)的第一個字節(jié)是“-”
  • 對于整數(shù),回復(fù)的第一個字節(jié)是“:”
  • 對于批量字符串,回復(fù)的第一個字節(jié)是“$”
  • 對于數(shù)組,回復(fù)的第一個字節(jié)是“*”

此外,RESP 能夠使用稍后指定的批量字符串或數(shù)組的特殊變體來表示 Null 值。在 RESP 中,協(xié)議的不同部分總是以“\r\n”(CRLF)終止。

下面只簡單介紹字符串的編碼方式和錯誤的編碼方式,詳情可以查看 Redis 官網(wǎng)對 RESP 進(jìn)行了詳細(xì)的說明。

簡單字符串

用如下方法編碼:一個“+”號后面跟字符串,最后是“\r\n”,字符串里不能包含"\r\n"。簡單字符串用來傳輸比較短的二進(jìn)制安全的字符串。例如很多redis命令執(zhí)行成功會返回“OK”,用RESP編碼就是5個字節(jié):

  1. "+OK\r\n" 

想要發(fā)送二進(jìn)制安全的字符串,需要用RESP的塊字符串。當(dāng)redis返回了一個簡單字符串的時候,客戶端庫需要給調(diào)用者返回“+”號(不含)之后CRLF之前(不含)的字符串。

RESP錯誤

RESP 有一種專門為錯誤設(shè)計的類型。實際上錯誤類型很像RESP簡單字符串類型,但是第一個字符是“-”。簡單字符串類型和錯誤類型的區(qū)別是客戶端把錯誤類型當(dāng)成一個異常,錯誤類型包含的字符串是異常信息。格式是:

  1. "-Error message\r\n" 

有錯誤發(fā)生的時候才會返回錯誤類型,例如你執(zhí)行了一個對于某類型錯誤的操作,或者命令不存在等。當(dāng)返回一個錯誤類型的時候客戶端庫應(yīng)該發(fā)起一個異常。下面是一個錯誤類型的例子

  1. -ERR unknown command 'foobar' -WRONGTYPE Operation against a key holding the wrong kind of value 

“-”號之后空格或者換行符之前的字符串代表返回的錯誤類型,這只是慣例,并不是RESP要求的格式。例如ERR是一般錯誤,WRONGTYPE是更具體的錯誤表示客戶端的試圖在錯誤的類型上執(zhí)行某個操作。這個稱為錯誤前綴,能讓客戶端更方便的識別錯誤類型。

客戶端可能為不同的錯誤返回不同的異常,也可能只提供一個一般的方法來捕捉錯誤并提供錯誤名。但是不能依賴客戶端提供的這些特性,因為有的客戶端僅僅返回一般錯誤,比如false。

高性能 Redis 協(xié)議分析器

盡管 Redis 的協(xié)議非常利于人類閱讀, 定義也很簡單, 但這個協(xié)議的實現(xiàn)性能仍然可以和二進(jìn)制協(xié)議一樣快。

因為 Redis 協(xié)議將數(shù)據(jù)的長度放在數(shù)據(jù)正文之前, 所以程序無須像 JSON 那樣, 為了尋找某個特殊字符而掃描整個 payload , 也無須對發(fā)送至服務(wù)器的 payload 進(jìn)行轉(zhuǎn)義(quote)。

程序可以在對協(xié)議文本中的各個字符進(jìn)行處理的同時, 查找 CR 字符, 并計算出批量回復(fù)或多條批量回復(fù)的長度, 就像這樣:

  1. #include <stdio.h> 
  2.  
  3. int main(void) { 
  4.     unsigned char *p = "$123\r\n"
  5.     int len = 0; 
  6.  
  7.     p++; 
  8.     while(*p != '\r') { 
  9.         len = (len*10)+(*p - '0'); 
  10.         p++; 
  11.     } 
  12.  
  13.     /* Now p points at '\r'and the len is in bulk_len. */ 
  14.     printf("%d\n", len); 
  15.     return 0; 

得到了批量回復(fù)或多條批量回復(fù)的長度之后, 程序只需調(diào)用一次 read 函數(shù), 就可以將回復(fù)的正文數(shù)據(jù)全部讀入到內(nèi)存中, 而無須對這些數(shù)據(jù)做任何的處理。在回復(fù)最末尾的 CR 和 LF 不作處理,丟棄它們。

Redis 協(xié)議的實現(xiàn)性能可以和二進(jìn)制協(xié)議的實現(xiàn)性能相媲美, 并且由于 Redis 協(xié)議的簡單性, 大部分高級語言都可以輕易地實現(xiàn)這個協(xié)議, 這使得客戶端軟件的 bug 數(shù)量大大減少。

冷知識:redis到底有多快?

在成功安裝了Redis之后,Redis自帶一個可以用來進(jìn)行性能測試的命令 redis-benchmark,通過運行這個命令,我們可以模擬N個客戶端同時發(fā)送請求的場景,并監(jiān)測Redis處理這些請求所需的時間。

根據(jù)官方的文檔,Redis經(jīng)過在60000多個連接中進(jìn)行了基準(zhǔn)測試,并且仍然能夠在這些條件下維持50000 q/s的效率,同樣的請求量如果打到MySQL上,那肯定扛不住,直接就崩掉了。也是因為這個原因,Redis經(jīng)常作為緩存存在,能夠起到對數(shù)據(jù)庫的保護(hù)作用。

官方給的Redis效率測試統(tǒng)計圖[1](橫坐標(biāo)是連接數(shù)量,縱坐標(biāo)是QPS)

可以看出來啊,Redis號稱十萬吞吐量確實也沒吹牛,以后大家面試的時候也可以假裝不經(jīng)意間提一嘴這個數(shù)量級,發(fā)現(xiàn)很多人對“十萬級“、”百萬級“這種量級經(jīng)常亂用,能夠比較精準(zhǔn)的說出來也是一個加分項呢。

我是敖丙,你知道的越多,你不知道的越多,我們下期見!

 

責(zé)任編輯:姜華 來源: 三太子敖丙
相關(guān)推薦

2018-04-25 10:13:30

Redis內(nèi)存模型

2019-02-18 08:10:53

2024-05-09 10:11:30

2023-08-29 07:46:08

Redis數(shù)據(jù)ReHash

2023-03-21 08:02:36

Redis6.0IO多線程

2020-02-27 21:03:30

調(diào)度器架構(gòu)效率

2024-02-26 21:15:20

Kafka緩存參數(shù)

2020-02-27 15:44:41

Nginx服務(wù)器反向代理

2023-11-28 09:31:55

MySQL算法

2022-08-09 09:10:43

Kubernetes容器

2020-10-21 09:17:52

Redis面試內(nèi)存

2024-07-24 08:38:07

2020-03-30 15:05:46

Kafka消息數(shù)據(jù)

2020-04-20 10:47:57

Redis數(shù)據(jù)開發(fā)

2022-09-01 08:01:56

Pythongunicorn

2023-12-12 07:31:51

Executors工具開發(fā)者

2023-09-18 08:02:45

CSS布局屬性

2023-10-26 16:27:50

前端 WebCSS開發(fā)

2024-11-26 08:52:34

SQL優(yōu)化Kafka

2021-06-27 22:48:28

Redis數(shù)據(jù)庫內(nèi)存
點贊
收藏

51CTO技術(shù)棧公眾號