前言
只有光頭才能變強
好的,今天我們要上黃金段位了,如果還沒經歷過青銅和白銀階段的,可以先去蹭蹭經驗再回來:
看過相關Redis基礎的同學可以知道Redis是單執行緒的,很多面試題也很可能會問到“為什麼Redis是單執行緒的還那麼快”。
這篇文章來講講單執行緒的內部的原理。
文字力求簡單講清每個知識點,希望大家看完能有所收穫
一、基礎鋪墊
在講解Redis之前,我們先來一些基礎的鋪墊,有更好的閱讀體驗。
1.1網路程式設計
我們在初學Java的時候肯定會學過網路程式設計這一章節的,當時學完寫的應用可能就是“網路聊天室”。
寫出來的效果可能就是在console噼裡啪啦的輸入資料,然後噼裡啪啦的返回資料,就完事了..(扎心了)
網路程式設計可簡單分為TCP和UPD兩種,一般我們更多關注的是TCP。TCP網路程式設計在Java中封裝成Socket和SocketServer,我們來回顧一下最簡單的TCP網路程式設計吧:
TCP客戶端
public class ClientDemo {
public static void main(String[] args) throws IOException {
//建立傳送端的Socket物件
Socket s = new Socket("192.168.1.106",8888);
//Socket物件可以獲取輸出流
OutputStream os = s.getOutputStream();
os.write("hello,tcp,我來了".getBytes());
s.close();
}
}
複製程式碼
TCP服務端:
public class ServerDemo {
public static void main(String[] args) throws IOException {
//建立接收端的Socket物件
ServerSocket ss = new ServerSocket(8888);
//監聽客戶端連線,返回一個對應的Socket物件
//偵聽並接受到此套接字的連線,此方法會阻塞
Socket s = ss.accept();
//獲取輸入流,讀取資料
InputStream is = s.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
String str = new String (bys,0,len);
String ip = s.getInetAddress().getHostAddress();
System.out.println(ip + " ---" +str);
//釋放資源
s.close();
//ss.close();
}
}
複製程式碼
上面的程式碼就可以實現:客戶端向伺服器傳送資料,服務端能夠接收客戶端傳送過來的資料。
1.2IO多路複用
之前我已經寫過Java NIO的文章了,Java的NIO也是基於IO多路複用模型的,建議先去看一下再回來,文章寫得挺詳細和通俗的了:JDK10都發布了,nio你瞭解多少?
這裡就簡單回顧一下吧:
- I/O多路複用的特點是通過一種機制一個程式能同時等待多個檔案描述符,而這些檔案描述符其中的任意一個進入讀就緒狀態、等等,
select()
函式就可以返回。 - select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。
說白了,使用IO多路複用機制的,一般自己會有一套事件機制,使用一個執行緒或者程式監聽這些事件,如果這些事件被觸發了,則呼叫對應的函式來處理。
二、Redis事件
Redis伺服器是一個事件驅動程式,主要處理以下兩類事件:
- 檔案事件:檔案事件其實就是對Socket操作的抽象,Redis伺服器與Redis客戶端的通訊會產生檔案事件,伺服器通過監聽並處理這些事件來完成一系列的網路操作
- 時間事件:時間事件其實就是對定時操作的抽象,前面我們已經講了RDB、AOF、定時刪除鍵這些操作都可以由服務端去定時或者週期去完成,底層就是通過觸發時間事件來實現的!
2.1檔案事件
Redis開發了自己的網路事件處理器,這個處理器被稱為檔案事件處理器。
檔案事件處理器由四部分組成:
檔案事件處理器使用I/O多路複用程式來同時監聽多個Socket。當被監聽的Socket準備好執行連線應答(accept)、讀取(read)等等操作時,與操作相對應的檔案事件就會產生,根據檔案事件來為Socket關聯對應的事件處理器,從而實現功能。
要值得注意的是:Redis中的I/O多路複用程式會將所有產生事件的Socket放到一個佇列裡邊,然後通過這個佇列以有序、同步、每次一個Socket的方式向檔案事件分派器傳送套接字。也就是說:當上一個Socket處理完畢後,I/O多路複用程式才會向檔案事件分派器傳送下一個Socket。
首先,IO多路複用程式首先會監聽著Socket的AE_READABLE
事件,該事件對應著連線應答處理器
- 可以理解簡單成
SocketServet.accpet()
此時,一個名字叫做3y的Socket要連線伺服器啦。伺服器會用連線應答處理器處理。建立出客戶端的Socket,並將客戶端的Socket與命令請求處理器進行關聯,使得客戶端可以向伺服器傳送命令請求。
- 相當於
Socket s = ss.accept();
,建立出客戶端的Socket,然後將該Socket關聯命令請求處理器 - 此時客戶端就可以向主伺服器傳送命令請求了
假設現在客戶端傳送一個命令請求set Java3y "關注、點贊、評論"
,客戶端Socket將產生AE_READABLE
事件,引發命令請求處理器執行。處理器讀取客戶端的命令內容,然後傳給對應的程式去執行。
客戶端傳送完命令請求後,服務端總得給客戶端迴應的。此時服務端會將客戶端的Scoket的AE_WRITABLE
事件與命令回覆處理器關聯。
最後客戶端嘗試讀取命令回覆時,客戶端Socket產生AE_WRITABLE事件,觸發命令回覆處理器執行。當把所有的回覆資料寫入到Socket之後,伺服器就會解除客戶端Socket的AE_WRITABLE事件與命令回覆處理器的關聯。
最後以《Redis設計與實現》的一張圖來概括:
2.2時間事件
持續執行的Redis伺服器會定期對自身的資源和狀態進行檢查和調整,這些定期的操作由serverCron函式負責執行,它的主要工作包括:
- 更新伺服器的統計資訊(時間、記憶體佔用、資料庫佔用)
- 清理資料庫的過期鍵值對
- AOF、RDB持久化
- 如果是主從伺服器,對從伺服器進行定期同步
- 如果是叢集模式,對進群進行定期同步和連線
- ...
Redis伺服器將時間事件放在一個連結串列中,當時間事件執行器執行時,會遍歷整個連結串列。時間事件包括:
- 週期性事件(Redis一般只執行serverCron時間事件,serverCron時間事件是週期性的)
- 定時事件
2.3時間事件和檔案事件
- 檔案事件和時間事件之間是合作關係,伺服器會輪流處理這兩種事件,並且處理事件的過程中不會發生搶佔。
- 時間事件的實際處理事件通常會比設定的到達時間晚一些
三、Redis多執行緒為什麼快?
- 1)純記憶體操作
- 2)核心是基於非阻塞的IO多路複用機制
- 3)單執行緒避免了多執行緒的頻繁上下文切換問題
四、客戶端與伺服器
在《Redis設計與實現》中各用了一章節來寫客戶端與伺服器,我看完覺得比較底層的東西,也很難記得住,所以我決定總結一下比較重要的知識。如果以後真的遇到了,再來補坑~
伺服器使用clints連結串列連線多個客戶端狀態,新新增的客戶端狀態會被放到連結串列的末尾
- 一個伺服器可以與多個客戶端建立網路連線,每個客戶端可以向伺服器傳送命令請求,而伺服器則接收並處理客戶端傳送的命令請求,並向客戶端返回命令回覆。
- Redis伺服器使用單執行緒單程式的方式處理命令請求。在資料庫中儲存客戶端執行命令所產生的資料,並通過資源管理來維持伺服器自身的運轉。
4.1客戶端
客戶端章節中主要講解了Redis客戶端的屬性(客戶端狀態、輸入/輸出緩衝區、命令引數、命令函式等等)
typedef struct redisClient{
//客戶端狀態的輸入緩衝區用於儲存客戶端傳送的命令請求,最大1GB,否則伺服器將關閉這個客戶端
sds querybuf;
//負責記錄argv陣列的長度。
int argc;
// 命令的引數
robj **argv;
// 客戶端要執行命令的實現函式
struct redisCommand *cmd, *lastcmd;
//記錄了客戶端的角色(role),以及客戶端所處的狀態。 (REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI)
int flags;
//記錄客戶端是否通過了身份驗證
int authenticated;
//時間相關的屬性
time_t ctime; /* Client creation time */
time_t lastinteraction; /* time of the last interaction, used for timeout */
time_t obuf_soft_limit_reached_time;
//固定大小的緩衝區用於儲存那些長度比較小的回覆
/* Response buffer */
int bufpos;
char buf[REDIS_REPLY_CHUNK_BYTES];
//可變大小的緩衝區用於儲存那些長度比較大的回覆
list *reply; //可變大小緩衝區由reply 連結串列和一個或多個字串物件組成
//...
}
複製程式碼
4.2服務端
伺服器章節中主要講解了Redis伺服器讀取客戶端傳送過來的命令是如何解析,以及初始化的過程。
伺服器從啟動到能夠處理客戶端的命令請求需要執行以下的步驟:
- 初始化伺服器狀態
- 載入伺服器配置
- 初始化伺服器的資料結構
- 還原資料庫狀態
- 執行事件迴圈
總的來說是這樣子的:
def main():
init_server();
while server_is_not_shutdown();
aeProcessEvents()
clean_server();
複製程式碼
從客戶端傳送命令道完成主要包括的步驟:
- 客戶端將命令請求傳送給伺服器
- 伺服器讀取命令請求,分析出命令引數
- 命令執行器根據引數查詢命令的實現函式,執行實現函式並得出命令回覆
- 伺服器將命令回覆返回給客戶端
五、最後
現在臨近雙十一買阿里雲伺服器就特別省錢!之前我買學生機也要9.8塊錢一個月,現在最低價只需要8.3一個月!
無論是Nginx/Elasticsearch/Redis這些技術都是在Linux下完美執行的,如果還是程式設計師新手,買一個學習Linux基礎命令,學習搭建環境也是不錯的選擇。
如果有要買伺服器的同學可通過我的連結直接享受最低價:m.aliyun.com/act/team111…
本來也想把“複製”(主從)在這邊一起寫的,但寫完可能就很長了,所以留到下一篇吧。
如果大家有更好的理解方式或者文章有錯誤的地方還請大家不吝在評論區留言,大家互相學習交流~~~
參考資料:
- 《Redis設計與實現》
- 《Redis實戰》
一個堅持原創的Java技術公眾號:Java3y,歡迎大家關注
3y所有的原創文章:
- 文章的目錄導航(腦圖+海量視訊資源):github.com/ZhongFuChen…