天天看點

Redis進階應用:Redis+Lua腳本實作複合操作

引言

Redis是高性能的key-value資料庫,在很大程度克服了memcached這類key/value存儲的不足,在部分場景下,是對關系資料庫的良好補充。得益于超高性能和豐富的資料結構,Redis已成為目前架構設計中的首選key-value存儲系統。

雖然Redis官網上提供了200多個指令,但做程式設計時還是避免不了為了實作一小步業務邏輯而多次調用Redis的情況。

以compare and set場景為例。如果使用Redis原生指令,需要從Redis中擷取這個key,然後提取其中的值進行比對:如果相等就不做處理;如果不相等或者key不存在則将key設定成目标值。僅僅一個單點的compare and set操作就需要與Redis通訊兩次。

此外,這種分散操作無法利用Redis的原子特性,占用多次網絡IO。

今天我們就來探讨一下如何優雅地應對上述場景。

一、Redis與Lua

在介紹Lua之前,我們需要先對這個語言有個初步了解。Lua 是一個小巧的腳本語言,幾乎可以運作在所有作業系統和平台上。我們一般不會用Lua處理特别複雜的事務,是以隻需了解一些lua的基本文法即可。

Redis問世之後,其開發者也意識到了開篇提到的問題,是以Redis從2.6版本開始支援Lua腳本。新版本的Redis還支援Lua Script debug,感興趣的小夥伴可以去官網的Documentation中找到對應介紹和QuickStart。

有了Lua腳本之後,使用Redis程式時便能夠在以下方面實作顯著提升:

  • 減少網絡開銷:本來N次網絡請求的操作,可以用一個請求完成。原先N次請求的邏輯放在Redis伺服器上完成,減少了網絡往返時延;
  • 原子操作:Redis會将整個腳本作為一個整體執行,中間不會被其他指令插入。這是一個重要特性,一定要拿小本本記好。至于為什麼是一個原子操作,我們以後再分析;
  • 複用:用戶端發送的腳本會永久存儲在Redis中。這樣其他用戶端就可以複用這一腳本,而不需要使用代碼完成同樣的邏輯。

是以現在流傳一句話:要想學好Redis,必會Lua Script。

三、通過Lua腳本實作compare and set

接下來我們就實作一個簡單的compare and set,并通過這個例子感受一下Lua腳本給Redis使用帶來的全新體驗。

首先看一下如何讓Redis執行Lua腳本。

3.1 Redis的EVAL

Redis 127.0.0.1:6379> EVAL script  numkeys key [key ...] arg [arg ...]           
  • script: 參數是一段 Lua 5.1 腳本程式。腳本不必(也不應該)定義為一個Lua函數。
  • numkeys: 用于指定鍵名參數的個數。
  • key [key ...]: 從 EVAL 的第三個參數開始算起,表示在腳本中所用到的Redis鍵(key)。在Lua中,這些鍵名參數可以通過全局變量 KEYS 數組,用1為基址的形式通路( KEYS[1] ,KEYS[2],依次類推)。
  • arg [arg ...]: 附加參數,在Lua中通過全局變量ARGV數組通路,通路的形式和KEYS變量類似( ARGV[1] 、 ARGV[2] ,諸如此類)。

    這裡借用一下官網的例子。

Redis進階應用:Redis+Lua腳本實作複合操作

上述腳本直接傳回了入參。

  • eval為Redis關鍵字;
  • 第一個引号中的内容就是Lua腳本;
  • 2為參數個數;
  • key1和key2是KEYS[1]、KEYS[2]的入參;
  • first和second是ARGV[1],ARGV[2]的入參。

大家可以簡單地将KEYS[1],KEYS[2], ARGV[1],ARGV[2]了解為占位符。

3.2 執行腳本檔案和緩存腳本

如果隻能在指令行中寫腳本執行,遇到複雜的腳本程式豈不是會抓狂?

下面我們來看一下,如何讓Redis執行Lua腳本檔案,同時也驗證一下lua腳本的複用特性(以後我們再也不需要定期批量删除某些符合特定規則的key了)。

Redis 127.0.0.1:6379> SCRIPT LOAD  script
Redis 127.0.0.1:6379> EVALSHA sha1  numkeys key [key ...] arg [arg ...]           

Redis提供了一個SCRIPTLOAD指令,指令後面的script即為Lua腳本。指令将腳本script添加到腳本緩存中,但并不立即執行這個腳本。執行指令後,Redis會傳回一個SHA1串,第二個EVALSHA指令即可執行。

需要注意的是,腳本可以在緩存中保留無限長的時間,直到執行完SCRIPT FLUSH。我們來看一下效果。

Redis進階應用:Redis+Lua腳本實作複合操作

Redis還支援直接執行Lua腳本檔案。首先編寫并存儲一個Lua腳本。

Redis進階應用:Redis+Lua腳本實作複合操作

然後調用Redis-cli –eval指令

Redis進階應用:Redis+Lua腳本實作複合操作

Redis-cli –eval指令文法基本與原eval文法相同。

3.3 使用Lua腳本實作compare and set

compareand set的實作邏輯是這樣的:首先擷取Redis中指定key的value,然後與給定值進行比較:如果相等,則将key設定為目标值并傳回一個辨別符;如果不相等,則不作任何操作并傳回一個辨別符。

if Redis.call('get', KEYS[1]) == ARGV[1]  then
     Redis.call('set', KEYS[1], ARGV[2]);
     return 1
else
     return 0 end           

下面我們來測試一下這個腳本。

首先向Redis的指定key compareAndSet:key寫入一個值value

Redis進階應用:Redis+Lua腳本實作複合操作

在Redis中執行lua腳本

Redis進階應用:Redis+Lua腳本實作複合操作

可以看到第一次執行傳回1,說明修改成功了;再使用原參數執行時傳回0,說明沒有做任何修改。我們再查詢一下compareAndSet:key這個key

Redis進階應用:Redis+Lua腳本實作複合操作

可以看到compareAndSet:key這個key已經被修改為new_value了。

總結

我們通過lua腳本實作了一個簡單的compareAndSet操作。

下面我們通過這個例子來驗證一下開篇提到的特性。

  • 減少網絡開銷:不使用腳本的情況下,我們實作一個compareAndSet至少需要與Redis互動兩次,而現在隻需要執行一次操作即可完成;
  • 原子操作:得益于Redis的設計,Redis會将整個腳本作為一個整體執行,中間不會被其他指令插入。是以在編寫腳本的過程中無需擔心出現競态條件,無需使用事務,感興趣的可以百度或等待以後後續文章更新;
  • 複用:可以将一系列操作封裝成一個Lua腳本,存儲在檔案或Redis上,下次使用時直接調用即可。

讀到這裡,希望你已經對Redis+Lua有了一定的了解,并能使用腳本完成一些簡單的複合操作。後續還會繼續更新一些基于Lua腳本+java程式實作的分布式資料結構,如延遲隊列、可重入鎖等,感興趣的小夥伴可以持續關注。

作者:李崇

原文首發 UAVStack智能運維

來源:宜信技術學院

繼續閱讀