Redis服務器和客戶端的通信

Redis客戶端使用RESP(Redis序列化協議)與Redis服務器進行通信,RESP在位於TCP之上,而網絡模型上客戶端和服務器是保持的雙工的連接。如圖1

1587974695972

而一個簡單的請求/響應的串行通信模型如下圖:

1587975373862

串行化通信

串行化通信比較簡單,上面那張圖就很表面的反應出來這種通信方式,同一個Connction需要在等上一個命令執行完成之後在執行下一個命令,我們在前面文章講Redis各種類型的時候做的測試,就是用這種方式。客戶端發送一個指令到Redis實例,Redis實例處理完成之後將結果返回給客戶端。

前面文章說Redis為什麼要用多線程中有說過,Redis處理請求的速度特別快,我們一個請求的瓶頸主要是在I/O上面,而對於串行化通信,每一個請求的發送都要等到上一個請求的響應介紹,因此在串行模式下,單連接的大部分時間都浪費在網絡等待上,沒有充分的利用服務器的處理能力

管道技術

Redis在很早的時候就支持管道技術了,簡單來說,就是可以完全無需等待服務端應答地發送多條指令給服務端,並最終一次性讀取所有應答。管道技術最顯著的優勢是提高了redis服務的性能,通過管道技術來進行大批量的操作的時候,可以節省很多在網絡延遲上的時間。

在.net core 的Redis客戶端StackExchange.Redis則是基於Task來實現管道技術,而StackExchangeRedis本身的異步也都是通過管道技術來實現。

事務

在菜鳥教程中是這麼介紹的

Redis 事務可以一次執行多個命令, 並且帶有以下三個重要的保證:

  • 批量操作在發送 EXEC 命令前被放入隊列緩存。
  • 收到 EXEC 命令後進入事務執行,事務中任意命令執行失敗,其餘的命令依然被執行。
  • 在事務執行過程,其他客戶端提交的命令請求不會插入到事務執行命令序列中。

一個事務從開始到執行會經歷以下三個階段:

  • 開始事務。
  • 命令入隊。
  • 執行事務
  • 放棄事務

原理很簡單,客戶端發送命令MULTI,服務器會將後續的命令都放入隊列緩存,直到收到EXEC命令才會依次執行命令。單個Redis的命令是原子性的,但是Redis並沒有在事務上增加任何的維持原子性的機制,當中間某條命令失敗並不會導致其他命令的回滾,這個跟我們在關係型數據庫的理解不一樣,更多的像一個打包的批處理腳本。

菜鳥中有這麼一句話

在事務執行過程,其他客戶端提交的命令請求不會插入到事務執行命令序列中。

粗略一看我還理解為事務開啟會阻塞其他客戶端的命令,嚇得我馬上做了一下測試

在客戶端1中開啟事務multi,並發送一個set 和 get 的命令,能看到都是QUEUED的狀態,表明是正確的入隊了

1588074772802

接着在客戶端2中獲取key1發現值是null,說明客戶端1的命令還沒有真正執行,接着設置key1的值為value2,接着取得key1的值,在客戶端1中開啟事務後,在客戶端2是可以順利執行命令的,菜鳥中的話的意思其實客戶端的命令不會進入開啟事務那個客戶端的命令隊列中。

1588081337699

我們接着在客戶端1提交命令,key1的值變為value1,客戶端2中設置的value2被更改為value1了。

我們將Redis事務與數據庫事務的四大特徵對比下

原子性 不支持 Redis單個指令是具有原子性的,但是事務沒有
一致性 不支持 在上面的例子就可以看見,在客戶端1的事務開啟的時候,我仍然能修改key1的值,在關係型數據庫中我們有悲觀鎖和樂觀鎖來解決這種並發問題,Redis也通過Watch可以實現樂觀鎖的效果,但是我還是沒有體會出來有什麼用處。在關係型數據中的事務,我們可能會先取出來值,在進行修改,最後提交事務,如果沒有鎖來保證,那麼我們最後的數據就沒有一致性了,但是對於Redis我還是沒想出來什麼場景下會需要用樂觀鎖來控制並發,知道的小夥伴麻煩告知一聲。
隔離性 支持 Redis本身是沒有隔離性這個說法的,之所以我覺得是支持隔離性,因為我覺得Redis的事務都是在最後才執行,而本身命令又是原子性的,所以隔離性對Redis是無意義的。
持久性 不支持 Redis有持久化方案,但是最高數據安全性的方式-AOF中的修改同步,仍然會在異常情況下導致數據丟失。

其實這個對比不太恰當,Redis的事務只是頂着事務這個名字,做的還是批量處理的事情,它的關注點不應該在正真的事務上

腳本

在說事務的時候有說事務更像是批處理的感覺,而腳本也是批處理,不同的是,我們可以根據上一個指令的結果作為我們下個指令的參數,這是處理邏輯問題的時候特別有用。

Redis腳本是通過Eval命令實現,當客戶都安使用Eval命令的時候,Redis實例會通過lua解釋器來執行腳本,我們這裡的腳本也是lua腳本,用Abp中清除緩存的的源碼作為示例

EVAL "local keys = redis.call('keys', ARGV[1]) 
                for i=1,#keys,5000 
                do 
                redis.call('del', unpack(keys, i, math.min(i+4999, #keys)))
                end" 
                0 'Test_*'

這個腳本第一步將以Test做為前綴的key全部取出來存入變量keys,接着從1開始,以keys的長度為最大值,步長為5000進行遍歷,每一步都是刪除5000個key。為什麼要用每次5000遍歷來執行呢?因為unpack函數在數量太多的時候會出現 ‘too many results to unpack’ 的錯誤,我們來實際操作下,往實例中添加10個用Test_為前綴的值,然後執行上面的腳本

1588151071491

1588151130571

可以看到我們以Test_做為前綴的Key都被刪除了

發佈/訂閱模式

前面有講到過,Redis實例和客戶都之間是雙工連接的,但是前面所說的不管是簡單的命令還是事務腳本都是客戶端主動發起請求,Redis實例被動回應的,而發佈/訂閱模式則是可以由Redis實例主動給客戶端發送消息,在下一節會詳細說這種模式。

Tags: