Redis進階特性之釋出/訂閱和Lua腳本執行原理分析
- 前言
- 釋出與訂閱
-
- 基于頻道的實作
-
- 實作原理分析
- 基于模式的實作
-
- 實作原理分析
- Lua腳本
-
- Lua腳本的調用
-
- Lua腳本中執行Redis指令
- Lua腳本摘要
- Lua腳本檔案
- 腳本異常
-
- 腳本逾時
- 腳本陷入死循環
- 為什麼可以執行script kill指令
- 總結
前言
Redis當中除了之前介紹的事務,持久化等進階特性之外,還提供了釋出與訂閱,Lua腳本,事件機制等進階特性,本文會繼續介紹Redis的另外兩大進階特性:
釋出與訂閱
和
Lua腳本
。
釋出與訂閱
理論上來說通過雙端連結清單就可以實作釋出與訂閱功能,但是這種通過連結清單來實作的釋出與訂閱功能有兩個局限性:
- 1、如果生産者生産消息的速度遠大于消費者消費消息的速度,那麼連結清單中未消費的消息會占用大量的記憶體。
- 2、基于連結清單實作的消息隊列,不支援一對多的消息分發。
為了解決這兩個局限性,Redis當中選擇了通過其他指令來實作釋出與訂閱模式,主要指令有:
subscribe
uncubscribe
publish
等。
在Redis中的釋出與訂閱也分為兩種類型,一種是
基于頻道
來實作,一種是
基于模式
來實作。
基于頻道的實作
基于頻道的實作方式主要通過以下三個指令:
- subscribe channel-1 channel-2:訂閱一個或者多個頻道
- unsubscribe channel-1:取消頻道的訂閱(基于指令操作,界面上無法退訂)
- publish channel-1 message:向頻道channel-1發送消息message
下圖就是用戶端1訂閱對應頻道之後,最後兩個紅框内就是用戶端2發送消息之後這邊同步收到的消息:
用戶端2釋出消息
同時,還有以下2個指令可以檢視訂閱的頻道資訊
- punsub channels [channel_name] :檢視目前伺服器被訂閱的頻道。不帶參數則傳回所有頻道,後面的參數可以使用通配符?或者*
- pubsub numsub channel-1 channel-2:檢視指定頻道的訂閱數
實作原理分析
用戶端與其訂閱的頻道資訊被儲存在
redisServer
對象中的
pubsub_channels
屬性中。
struct redisServer {
dict *pubsub_channels;//儲存了用戶端及其訂閱的頻道資訊
//省略其他資訊
};
pubsub_channels
屬性是一個字典,其key值儲存的就是頻道名,value是一個連結清單,連結清單中儲存的就是用戶端id。
-
訂閱
訂閱的時候首先會檢查字典内是否存在這個頻道:如果不存在,則需要為目前頻道建立一個字典,同時建立一個連結清單作為value,并将目前用戶端id放傳入連結表;如果存在,則直接将目前用戶端id放傳入連結表即可。
-
取消訂閱
取消訂閱的時候需要将用戶端id從對應的連結清單中移除,如果移除之後連結清單為空,則需要同時将該頻道從字典内删除。
-
發送消息
發送消息時首先會去
字典内尋找鍵,如果發現有可以比對上的鍵,則會找到對應的連結清單,進行周遊發送消息。pubsub_channels
基于模式的實作
基于模式的實作方式主要通過以下三個指令:
- psubscribe pattern-1 pattern-2:訂閱一個或者多個模式,模式可以通過通配符?和*來表示
- punsubscribe pattern-1 pattern-1:取消模式的訂閱(基于指令操作,界面上無法退訂)
- publish channel-1 message :向頻道channel-1發送消息message。注意,這裡和上面基于頻道指令是一樣的
用戶端1訂閱了模式
m*
,用戶端2向頻道
movie
發送消息,此時用戶端1可以收到消息:
同樣的,其提供了一個查詢指令:
- pubsub numpat:查詢目前伺服器被訂閱模式的數量
實作原理分析
用戶端與其訂閱的模式資訊被儲存在
redisServer
對象中的
pubsub_patterns
屬性中。
struct redisServer {
list pubsub_patterns;//儲存了用戶端及其訂閱的模式資訊
//省略其他資訊
};
pubsub_patterns
屬性是一個清單,其清單内結構(源碼serer.h内)定義如下:
typedef struct pubsubPattern {
client *client;//訂閱模式的用戶端
robj *pattern;//被訂閱的模式
} pubsubPattern;
-
訂閱
建立一個
資料結構加入到連結清單pubsubPattern
的結尾pubsub_patterns
-
取消訂閱
從連結清單中将目前取消訂閱的用戶端
從連結清單pubsubPattern
中移除pubsub_patterns
-
-發送消息
此時需要周遊整個連結清單來尋找能比對的模式。之是以基于模式場景使用連結清單是因為模式支援通配符,是以沒有辦法直接用字典實作。
PS:當基于頻道和基于模式兩種訂閱都存在時,Redis會先去尋找頻道字典,再去周遊模式連結清單進行消息發送。
Lua腳本
Redis從2.6版本開始支援Lua腳本,為了支援Lua腳本,Redis在伺服器中嵌入了Lua環境。
使用Lua腳本最大的好處是Redis會将整個腳本作為一個整體執行,不會被其他請求打斷,可以保持原子性且減少了網絡開銷。
Lua腳本的調用
Lua腳本的執行文法如下:
- eval:執行Lua腳本的指令
- lua-script:lua腳本内容
- numkeys:表示的是Lua腳本中需要用到多少個key,如果沒用到則寫0
- key [key …]:将key作為參數按順序傳遞到Lua腳本,numkeys是0則可省略
- arg:Lua腳本中用到的參數,如果沒有可省略
下面就是一個不帶任何key和參數的簡單腳本:
Lua腳本中執行Redis指令
在Lua腳本中執行Redis指令時需要使用以下文法:
- command:Redis中的指令,如set、get等。
- key:操作Redis中的key值,相當于我們調用方法時的形參。
- param:代表參數,相當于我們調用方法時的實參。
下面就是一個簡單的在Lua腳本中執行Redis指令的示例:
需要注意的是:
KEYS
和
ARGV
必須要大寫,參數的下标從1開始。上面的語句意思等價于在Redis中直接執行指令
set name lonely_wolf
。
Lua腳本摘要
有時候如果我們執行的一個Lua腳本很長的話,那麼直接這麼調用Lua腳本的話非常不友善,是以Redis當中提供了一個指令
script load
來為手動給每一個指令生成摘要,這裡之是以要說手動的原因是即使我們不使用這個指令,每次調用完Lua腳本的時候,Redis也會為每個Lua腳本生成一個摘要。
其他相關指令:
-
:判斷一個摘要是否存在。0表示不存在,1表示存在。script exists 摘要
-
:清除所有Lua腳本緩存。script flush
Lua腳本檔案
當我們的Lua腳本很長時,直接在指令視窗中寫腳本是不直覺的,也很難發現文法問題,是以Redis當中也支援我們直接把先把腳本寫入檔案中,然後直接調用檔案。
比如我們建立一個
test.lua
腳本:
redis.call('set',KEYS[1],ARGV[1])
return redis.call('get',KEYS[1])
執行的時候參數的數量可以省略,但是注意key和arg參數之間要以逗号隔開,且逗号兩邊的空格不能省略:
腳本異常
我們知道,Redis的指令是單線程執行的,而現在Lua腳本可以寫一些邏輯,那麼如果Lua
腳本執行逾時或者陷入了死循環,這個時候其他的指令就會被阻塞,導緻Redis無法正常使用。這個時候應該如何處理呢?
腳本逾時
為了解決逾時的問題,Redis提供了一個逾時時間的參數
lua-time-limit
來控制Lua腳本執行的逾時時間,預設是5秒。
腳本陷入死循環
假如腳本陷入了死循環,這時候逾時時間就不起作用了,我們來模拟一下:
首先執行一個死循環的lua腳本:
然後打開另一個用戶端,執行指令:
set key value
這時候會傳回
busy
,表示目前發執行這個指令
為了解決腳本死循環問題,Redis提供了一個
script kill
指令來中止腳本,我們執行一下這個指令之後發現執行lua腳本的用戶端就被停下來了
上面的死循環指令訓示一個普通的沒有執行任何Redis指令的指令,那麼假如我們的lua腳本執行了一些redis指令之後再陷入死循環又會怎麼樣呢?
執行一個死循環的lua腳本:
這時候再去另一個用戶端執行
script kill
指令,會提示無法中止lua腳本。
這時候我們就隻能執行
shutdown nosave
指令來強行中斷redis,并且加了nosave之後不會觸發持久化,進而保證了資料的一緻性:
為什麼可以執行script kill指令
Redis當中執行指令是單線程的,那麼為什麼lua腳本陷入死循環之後還可以執行
script kill
指令呢?
這是因為lua腳本引擎提供了鈎子(hook)函數,它允許在内部虛拟機執行指令時運作鈎子代碼,是以Redis正是利用了這一原理,在執行Lua腳本之前設定了一個鈎子,是以
script kill
指令正式通過鈎子(hook)函數來執行的。
總結
本文主要介紹Redis的另外兩大進階特性:
釋出與訂閱
和
Lua腳本
。介紹釋出與訂閱機制時主要介紹了其執行原理,至于Lua腳本,本文并沒有介紹Lua腳本的文法,但是介紹了Redis當中執行Lua腳本的一些特性和原理。