日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Redis 笔记(12)— 单线程架构(非阻塞 IO、多路复用)和多个异步线程

發(fā)布時間:2023/11/28 生活经验 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis 笔记(12)— 单线程架构(非阻塞 IO、多路复用)和多个异步线程 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

Redis 使用了單線程架構(gòu)、非阻塞 I/O 、多路復(fù)用模型來實現(xiàn)高性能的內(nèi)存數(shù)據(jù)庫服務(wù)。Redis 是單線程的。那么為什么說是單線程呢?

RedisReactor 模型內(nèi)開發(fā)了事件處理器,這個事件處理器分為多個 Socket(套接字)、IO 多路復(fù)用程序、事件分派器、事件處理器(連接應(yīng)答處理器、命令請求處理器、命令回復(fù)處理器),其中事件派發(fā)器的隊列是由單線程的事件處理器消費的,也是因為這個,Redis 才叫單線程模型。

當(dāng)多個事件并發(fā)出現(xiàn)時,I/O 多路復(fù)用程序?qū)⒈O(jiān)聽到的所有 Socket,關(guān)聯(lián)不同的事件處理器,這個關(guān)聯(lián)操作以有序(sequentially)、同步(synchronously)、每次一個套接字的方式向事件分派器傳送 Socket,當(dāng)上一個套接字產(chǎn)生的事件被處理完畢之后,I/O 多路復(fù)用程序才會繼續(xù)向事件分派器傳送下一個套接字。

1. 單線程模型

開啟三個 redis-cli 客戶端同時執(zhí)行命令。客戶端 1 設(shè)置一個字符串鍵值對:

127.0.0.1:6379> set hello world

客戶端 2 對 counter 做自增操作:

127.0.0.1:6379> incr counter

客戶端 3 對 counter 做自增操作:

127.0.0.1:6379> incr counter

Redis 客戶端與服務(wù)端的模型可以簡化成下圖,每次客戶端調(diào)用都經(jīng)歷了發(fā)送命令、執(zhí)行命令、返回結(jié)果三個過程。

其中第 2 步是重點要討論的,因為 Redis 是單線程來處理命令的,所以一條命令從客戶端達(dá)到服務(wù)端不會立刻被執(zhí)行,所有命令都會進(jìn)入一個隊列中,然后逐個被執(zhí)行。

所以上面 3 個客戶端命令的執(zhí)行順序是不確定的,如下圖所示。但是可以確定不會有兩條命令被同時執(zhí)行

所以兩條 incr 命令無論怎么執(zhí)行最終結(jié)果都是 2,不會產(chǎn)生并發(fā)問題,這就是 Redis 單線程的基本模型。

2. 非阻塞 IO

當(dāng)我們調(diào)用套接字的讀寫方法,默認(rèn)它們是阻塞的,比如 read 方法要傳遞進(jìn)去一個參數(shù) n,表示最多讀取這么多字節(jié)后再返回,如果一個字節(jié)都沒有,那么線程就會卡在那里,直到新的數(shù)據(jù)到來或者連接關(guān)閉了,read 方法才可以返回,線程才能繼續(xù)處理。而 write 方法一般來說不會阻塞,除非內(nèi)核為套接字分配的寫緩沖區(qū)已經(jīng)滿了,write 方法就會阻塞,直到緩存區(qū)中有空閑空間挪出來了。

非阻塞 IO 在套接字對象上提供了一個選項 Non_Blocking,當(dāng)這個選項打開時,讀寫方法不會阻塞,而是能讀多少讀多少,能寫多少寫多少。能讀多少取決于內(nèi)核為套接字分配的讀緩沖區(qū)內(nèi)部的數(shù)據(jù)字節(jié)數(shù),能寫多少取決于內(nèi)核為套接字分配的寫緩沖區(qū)的空閑空間字節(jié)數(shù)。讀方法和寫方法都會通過返回值來告知程序?qū)嶋H讀寫了多少字節(jié)。

有了非阻塞 IO 意味著線程在讀寫 IO 時可以不必再阻塞了,讀寫可以瞬間完成然后線程可以繼續(xù)干別的事了。

3. 多路復(fù)用(事件輪詢)

非阻塞 IO 有個問題,那就是線程要讀數(shù)據(jù),結(jié)果讀了一部分就返回了,線程如何知道何時才應(yīng)該繼續(xù)讀。也就是當(dāng)數(shù)據(jù)到來時,線程如何得到通知。寫也是一樣,如果緩沖區(qū)滿了,寫不完,剩下的數(shù)據(jù)何時才應(yīng)該繼續(xù)寫,線程也應(yīng)該得到通知。

事件輪詢 API 就是用來解決這個問題的,最簡單的事件輪詢 APIselect 函數(shù),它是操作系統(tǒng)提供給用戶程序的 API。輸入是讀寫描述符列表 read_fds & write_fds,輸出是與之對應(yīng)的可讀可寫事件。同時還提供了一個 timeout 參數(shù),如果沒有任何事件到來,那么就最多等待 timeout 時間,線程處于阻塞狀態(tài)。一旦期間有任何事件到來,就可以立即返回。時間過了之后還是沒有任何事件到來,也會立即返回。拿到事件后,線程就可以繼續(xù)挨個處理相應(yīng)的事件。處理完了繼續(xù)過來輪詢。于是線程就進(jìn)入了一個死循環(huán),我們把這個死循環(huán)稱為事件循環(huán),一個循環(huán)為一個周期。

每個客戶端套接字 socket 都有對應(yīng)的讀寫文件描述符。

read_events, write_events = select(read_fds, write_fds, timeout)
for event in read_events:handle_read(event.fd)
for event in write_events:handle_write(event.fd)
handle_others()  # 處理其它事情,如定時任務(wù)等

因為我們通過 select 系統(tǒng)調(diào)用同時處理多個通道描述符的讀寫事件,因此我們將這類系統(tǒng)調(diào)用稱為多路復(fù)用 API

現(xiàn)代操作系統(tǒng)的多路復(fù)用 API 已經(jīng)不再使用 select 系統(tǒng)調(diào)用,而改用 epoll(linux)kqueue(freebsd & macosx),因為 select 系統(tǒng)調(diào)用的性能在描述符特別多時性能會非常差。它們使用起來可能在形式上略有差異,但是本質(zhì)上都是差不多的,都可以使用上面的偽代碼邏輯進(jìn)行理解。

服務(wù)器套接字 serversocket 對象的讀操作是指調(diào)用 accept 接受客戶端新連接。何時有新連接到來,也是通過 select 系統(tǒng)調(diào)用的讀事件來得到通知的。

4. 為什么單線程還能如此快

為什么 Redis 使用單線程模型會達(dá)到每秒萬級別的處理能力呢?可以將其歸結(jié)為三點:

  1. 純內(nèi)存訪問,Redis 將所有數(shù)據(jù)放在內(nèi)存中,內(nèi)存的響應(yīng)時長大約為 100 納秒,這是 Redis 達(dá)到每秒萬級別訪問的重要基礎(chǔ)。
  2. 非阻塞 I/ORedis 使用 epoll 作為 I/O 多路復(fù)用技術(shù)的實現(xiàn),再加上 Redis 自身的事件處理模型將 epoll 中的連接、讀寫、關(guān)閉都轉(zhuǎn)換為件,不在網(wǎng)絡(luò) I/O 上浪費過多的時間。
  3. 單線程避免了線程切換和競態(tài)產(chǎn)生的消耗。

單線程能帶來幾個好處:

  1. 單線程可以簡化數(shù)據(jù)結(jié)構(gòu)和算法的實現(xiàn)。如果對高級編程語言熟悉的讀者應(yīng)該了解并發(fā)數(shù)據(jù)結(jié)構(gòu)實現(xiàn)不但困難而且開發(fā)測試比較麻煩。
  2. 單線程避免了線程切換和競態(tài)產(chǎn)生的消耗,對于服務(wù)端開發(fā)來說,鎖和線程切換通常是性能殺手。

但是單線程會有一個問題:對于每個命令的執(zhí)行時間是有要求的。如果某個命令執(zhí)行過長,會造成其他命令的阻塞,對于 Redis 這種高性能的服務(wù)來說是致命的,所以 Redis 是面向快速執(zhí)行場景的數(shù)據(jù)庫。

5. 多個異步線程

5.1 懶惰刪除

在 4.0 版本引入了 unlink 指令,它能對刪除操作進(jìn)行懶處理,丟給后臺線程來異步回收內(nèi)存。

> unlink key
OK

可以將整個 Redis 內(nèi)存里面所有有效的數(shù)據(jù)想象成一棵大樹。當(dāng) unlink 指令發(fā)出時,它只是把大樹中的一個樹枝別斷了,然后扔到旁邊的火堆里焚燒 (異步線程池)。樹枝離開大樹的一瞬間,它就再也無法被主線程中的其它指令訪問到了,因為主線程只會沿著這顆大樹來訪問。

Redis 提供了 flushdbflushall 指令,用來清空數(shù)據(jù)庫,這也是極其緩慢的操作。Redis 4.0 同樣給這兩個指令也帶來了異步化,在指令后面增加 async 參數(shù)就可以將整棵大樹連根拔起,扔給后臺線程慢慢焚燒。

> flushall async
OK

5.2 異步隊列

主線程將對象的引用從「大樹」中摘除后,會將這個 key 的內(nèi)存回收操作包裝成一個任務(wù),塞進(jìn)異步任務(wù)隊列,后臺線程會從這個異步隊列中取任務(wù)。任務(wù)隊列被主線程和異步線程同時操作,所以必須是一個線程安全的隊列。

不是所有的 unlink 操作都會延后處理,如果對應(yīng) key 所占用的內(nèi)存很小,延后處理就沒有必要了,這時候 Redis 會將對應(yīng)的 key 內(nèi)存立即回收,跟 del 指令一樣。

5.3 其它異步刪除點

Redis 回收內(nèi)存除了 del 指令和 flush 之外,還會存在于在 key 的過期、LRU 淘汰、rename 指令以及從庫全量同步時接受完 rdb 文件后會立即進(jìn)行的 flush 操作。

Redis4.0 為這些刪除點也帶來了異步刪除機(jī)制,打開這些點需要額外的配置選項。

  • slave-lazy-flush 從庫接受完 rdb 文件后的 flush 操作
  • lazyfree-lazy-eviction 內(nèi)存達(dá)到 maxmemory 時進(jìn)行淘汰
  • lazyfree-lazy-expire key 過期刪除
  • lazyfree-lazy-server-del rename 指令刪除 destKey

參考:

  • 《Redis 開發(fā)運維》
  • https://juejin.cn/book/6844733724618129422/section/6844733724710420487

總結(jié)

以上是生活随笔為你收集整理的Redis 笔记(12)— 单线程架构(非阻塞 IO、多路复用)和多个异步线程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。