最近琢磨分佈式鎖時接觸到的知識點,簡單記一下。
1. Redis中的Lua
Redis支持Lua,代碼直接發送完整腳本即可。基本語法(redis客戶端可以直接執行):
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
注:{KEYS[1],KEYS[2],ARGV[1],ARGV[2]}
都是一系列的 key -value 的佔位符(KEYS
和ARGV
都是全局變量),根據實際情況編寫,可以是很多這樣形式變量。後面的2 key1 first key2 second
,首位數字2
表示的是 Key 的數量,後面的變量和前面的佔位符意義對應(key1
對應於KEYS[1]
,執行時key1
自動注入)
2. 利用Lua操作Redis
上述執行借用redis的客戶端執行lua腳本,要真正對redis進行讀寫操作,需要調用redis.call()
函數或者redis.pcall()
函數,這兩個函數比較相似,但如果發生錯誤時,redis.call()
函數將引發一個Lua錯誤,這又將迫使EVAL
向命令調用者返回錯誤;而redis.pcall()
則會捕獲異常並返回Lua異常的某個信息碼。使用示例如下:
# 同樣的最後的 0 表示的全局變量 KEYS 中有0個元素 key
eval "return redis.call('set','foo','bar')" 0
向redis中寫入一個 key 爲foo
,value爲bar
的對象。但eval
命令將會驗證這條命令的語法,具體的驗證方式爲:將腳本中所有的key替換爲全局變量數據KEYS
,所以最終驗證語法的腳本爲eval "return redis.call('set',KEYS[1],'bar')" 1 foo
。
上述2種方式調用出錯的情況
當使用redis.call()
函數或者redis.pcall()
函數操作redis時,Redis返回的值對象將會被轉換成Lua語言中的數據類再返回。相似了當執行redis.call()
函數或者redis.pcall()
函數時,Lua變量的數據類型將會被轉成Redis中支持的數據類型(string,list,set,hash,Sorted Set)。
3. Lua腳本的原子性
Lua腳本在redis中的操作是原子性,redsi使用同一個Lua解釋器執行腳本中的所有命令,Redis本身也保證了這個腳本執行的原子性:當該腳本在執行時間區間內,不會有其他的腳本或者命令執行。所以使用Lua執行一個慢腳本是一個很扯的事情(除非你很清楚這樣的慢腳本是十分有必要的),一般情況下,因爲腳本的開銷非常低會很快。
4. 關於 EVALSHA
EVAL
命令會迫使我們反覆發送腳本,但Redis不需要每次都重新編譯腳本(因爲自己內部的緩存機制),但每次發送腳本需要耗費額外的帶寬在很多場景中並不是一種友好的方式。另一方面,使用特殊命令或者通過redis.conf
來定義命令也會有一些問題:
- 不同的實例可能有該命令的不同實現;
- 如果需要確保所有的實例都包含所給的命令,那部署將會很困難,尤其是在分佈式環境中;
- 讀取應用程序代碼後,由於應用程序將調用定義在服務器端的命令,因此完整的語義可能不清楚;
爲了解決上述的問題,Redis實現了EVALSHA
命令,EVALSHA
命令和EVAL
命令很相似,但EVALSHA
沒有以腳本本身作爲第一個參數,而是將該腳本的SHA1摘要作爲第一個參數,具體行爲:
- 如果服務端仍然記得和 SHA1 摘要匹配的腳本,那直接執行腳本;
- 如果忘了和 SHA1 摘要匹配的腳本,那將會拋出一個異常告訴客戶端,使用
EVAL
命令來執行,不要使用EVALSHA
;
所以客戶端最好的方式還是使用EVALSHA
命令來執行腳本(即使客戶端使用EVAL
,腳本實際已經被服務端看到,如果返回NOSCRIPT
的錯誤就會在使用EVAL
命令)。
腳本緩存機制:已執行的腳本會永遠存在於所執行的Redis實例的腳本緩存中。這就意味着如果一個EVAL
在Redis實例中執行,則後續所有的EVALSHA
命令都會調用成功。顯式的調用SCRIPT FLUSH
將會刷新腳本緩存,也會清除到目前爲止所有執行過的腳本(這也是清除腳本緩存的唯一方式)。
5. 常用SCRIPT
命令
Redis中腳本本身操作的命令有:
SCRIPT FLUSH
:清除Redis中到目前爲止所有執行過的腳本緩存;SCRIPT EXISTS sha1 sha2 ... shaN
:驗證腳本是否存在緩存中,返回會對應個數的0(緩存中不存在)和1(緩存中存在);SCRIPT LOAD script
:將腳本註冊進入服務端的存儲而不需要執行,確保EVALSHA
可以找到對應的腳本正常執行,返回該腳本的SHA1加密值;SCRIPT KILL
:打斷正在執行的腳本;
# 註冊腳本
script load "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
# 返回sha1值
"a42059b356c875f0717db19a51f6aaca9ae659ea"
6. 腳本本地化
可以本地編寫Lua腳本,直接通過客戶端調用redis-cli --eval xxx.lua
執行執行xxx.lua
腳本。如實際Linux中操作經常用到redis-cli -h hostname -p port -a password SCRIPT LOAD "$(cat lua_script_file_location)"
類似的命令將指定腳本緩存到Redis的服務端,已便下次直接通過返回的摘要直接執行腳本。
t lua_script_file_location)"`類似的命令將指定腳本緩存到Redis的服務端,已便下次直接通過返回的摘要直接執行腳本。