天天看點

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

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發送消息之後這邊同步收到的消息:

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

用戶端2釋出消息

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

同時,還有以下2個指令可以檢視訂閱的頻道資訊

  • punsub channels [channel_name] :檢視目前伺服器被訂閱的頻道。不帶參數則傳回所有頻道,後面的參數可以使用通配符?或者*
  • pubsub numsub channel-1 channel-2:檢視指定頻道的訂閱數
    【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

實作原理分析

用戶端與其訂閱的頻道資訊被儲存在

redisServer

對象中的

pubsub_channels

屬性中。

struct redisServer {
	dict *pubsub_channels;//儲存了用戶端及其訂閱的頻道資訊
	//省略其他資訊
};
           

pubsub_channels

屬性是一個字典,其key值儲存的就是頻道名,value是一個連結清單,連結清單中儲存的就是用戶端id。

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結
  • 訂閱

    訂閱的時候首先會檢查字典内是否存在這個頻道:如果不存在,則需要為目前頻道建立一個字典,同時建立一個連結清單作為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可以收到消息:

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結
【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

同樣的,其提供了一個查詢指令:

  • pubsub numpat:查詢目前伺服器被訂閱模式的數量

實作原理分析

用戶端與其訂閱的模式資訊被儲存在

redisServer

對象中的

pubsub_patterns

屬性中。

struct redisServer {
	list pubsub_patterns;//儲存了用戶端及其訂閱的模式資訊
	//省略其他資訊
};
           

pubsub_patterns

屬性是一個清單,其清單内結構(源碼serer.h内)定義如下:

typedef struct pubsubPattern {
    client *client;//訂閱模式的用戶端
    robj *pattern;//被訂閱的模式
} pubsubPattern;
           
【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結
  • 訂閱

    建立一個

    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和參數的簡單腳本:

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

Lua腳本中執行Redis指令

在Lua腳本中執行Redis指令時需要使用以下文法:

  • command:Redis中的指令,如set、get等。
  • key:操作Redis中的key值,相當于我們調用方法時的形參。
  • param:代表參數,相當于我們調用方法時的實參。

下面就是一個簡單的在Lua腳本中執行Redis指令的示例:

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

需要注意的是:

KEYS

ARGV

必須要大寫,參數的下标從1開始。上面的語句意思等價于在Redis中直接執行指令

set name lonely_wolf

Lua腳本摘要

有時候如果我們執行的一個Lua腳本很長的話,那麼直接這麼調用Lua腳本的話非常不友善,是以Redis當中提供了一個指令

script load

來為手動給每一個指令生成摘要,這裡之是以要說手動的原因是即使我們不使用這個指令,每次調用完Lua腳本的時候,Redis也會為每個Lua腳本生成一個摘要。

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

其他相關指令:

  • script exists 摘要

    :判斷一個摘要是否存在。0表示不存在,1表示存在。
    【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結
  • script flush

    :清除所有Lua腳本緩存。
    【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

Lua腳本檔案

當我們的Lua腳本很長時,直接在指令視窗中寫腳本是不直覺的,也很難發現文法問題,是以Redis當中也支援我們直接把先把腳本寫入檔案中,然後直接調用檔案。

比如我們建立一個

test.lua

腳本:

redis.call('set',KEYS[1],ARGV[1])
return redis.call('get',KEYS[1])
           

執行的時候參數的數量可以省略,但是注意key和arg參數之間要以逗号隔開,且逗号兩邊的空格不能省略:

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

腳本異常

我們知道,Redis的指令是單線程執行的,而現在Lua腳本可以寫一些邏輯,那麼如果Lua

腳本執行逾時或者陷入了死循環,這個時候其他的指令就會被阻塞,導緻Redis無法正常使用。這個時候應該如何處理呢?

腳本逾時

為了解決逾時的問題,Redis提供了一個逾時時間的參數

lua-time-limit

來控制Lua腳本執行的逾時時間,預設是5秒。

腳本陷入死循環

假如腳本陷入了死循環,這時候逾時時間就不起作用了,我們來模拟一下:

首先執行一個死循環的lua腳本:

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

然後打開另一個用戶端,執行指令:

set key value
           

這時候會傳回

busy

,表示目前發執行這個指令

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

為了解決腳本死循環問題,Redis提供了一個

script kill

指令來中止腳本,我們執行一下這個指令之後發現執行lua腳本的用戶端就被停下來了

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

上面的死循環指令訓示一個普通的沒有執行任何Redis指令的指令,那麼假如我們的lua腳本執行了一些redis指令之後再陷入死循環又會怎麼樣呢?

執行一個死循環的lua腳本:

這時候再去另一個用戶端執行

script kill

指令,會提示無法中止lua腳本。

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

這時候我們就隻能執行

shutdown nosave

指令來強行中斷redis,并且加了nosave之後不會觸發持久化,進而保證了資料的一緻性:

【Redis系列7】Redis進階特性之釋出/訂閱和Lua腳本執行原理分析前言釋出與訂閱Lua腳本總結

為什麼可以執行script kill指令

Redis當中執行指令是單線程的,那麼為什麼lua腳本陷入死循環之後還可以執行

script kill

指令呢?

這是因為lua腳本引擎提供了鈎子(hook)函數,它允許在内部虛拟機執行指令時運作鈎子代碼,是以Redis正是利用了這一原理,在執行Lua腳本之前設定了一個鈎子,是以

script kill

指令正式通過鈎子(hook)函數來執行的。

總結

本文主要介紹Redis的另外兩大進階特性:

釋出與訂閱

Lua腳本

。介紹釋出與訂閱機制時主要介紹了其執行原理,至于Lua腳本,本文并沒有介紹Lua腳本的文法,但是介紹了Redis當中執行Lua腳本的一些特性和原理。

繼續閱讀