Tair (Redis OSS-compatible)執行個體支援Lua相關命令,通過Lua指令碼可高效地處理CAS(compare-and-set)命令,進一步提升執行個體的效能,同時可以輕鬆實現以前較難實現或者不能高效實現的模式。本文介紹使用Lua指令碼的基本文法與使用規範。
基本文法
效能最佳化實踐
最佳化記憶體、網路開銷
當執行個體中緩衝了大量重複功能的指令碼時,將佔用大量記憶體空間,甚至可能引發記憶體溢出(Out of Memory)。以下為錯誤樣本。
EVAL "return redis.call('set', 'k1', 'v1')" 0
EVAL "return redis.call('set', 'k2', 'v2')" 0解決方案:
請避免將參數作為常量寫在Lua指令碼中,以減少記憶體空間的浪費。
# 與錯誤樣本實現相同功能但僅需緩衝一次指令碼。 EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 k1 v1 EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 k2 v2更加建議採用如下寫法,在減少記憶體的同時,降低網路開銷。
SCRIPT LOAD "return redis.call('set', KEYS[1], ARGV[1])" # 執行後,Redis將返回"55b22c0d0cedf3866879ce7c854970626dcef0c3" EVALSHA 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 k1 v1 EVALSHA 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 k2 v2
清理Lua指令碼的記憶體佔用
由於Lua指令碼緩衝將計入執行個體的記憶體使用量量中,並會導致used_memory升高,當執行個體的記憶體使用量量接近甚至超過maxmemory時,可能引發記憶體溢出(Out Of Memory),報錯樣本如下。
-OOM command not allowed when used memory > 'maxmemory'.解決方案:
通過用戶端執行SCRIPT FLUSH命令清除Lua指令碼緩衝,但與FLUSHALL不同,SCRIPT FLUSH命令為同步操作。若執行個體緩衝的Lua指令碼過多,SCRIPT FLUSH命令會阻塞執行個體較長時間,可能導致執行個體不可用,請謹慎處理,建議在業務低峰期執行該操作。
在控制台上單擊清除數據只能清除資料,無法清除Lua指令碼緩衝。
同時,請避免編寫過大的Lua指令碼,防止佔用過多的記憶體;避免在Lua指令碼中大批量寫入資料,否則會導致記憶體使用量急劇升高,甚至造成執行個體OOM。在業務允許的情況下,建議開啟資料逐出(執行個體預設開啟,模式為volatile-lru)節省記憶體空間。但無論是否開啟資料逐出,執行個體均不會逐出Lua指令碼緩衝。
錯誤處理指南
NOSCRIPT錯誤
使用EVALSHA命令時,若sha1值對應的指令碼未緩衝至執行個體中,執行個體會返回NOSCRIPT錯誤,報錯樣本如下。
(error) NOSCRIPT No matching script. Please use EVAL.解決方案:
請通過EVAL命令或SCRIPT LOAD命令將目標指令碼緩衝至執行個體中後進行重試。但由於執行個體不保證Lua指令碼的持久化、複製能力,在部分情境下仍會清除Lua指令碼緩衝(例如執行個體遷移、變更配置等),這要求您的用戶端需具備處理該錯誤的能力,詳情請參見持久化與複製問題。
以下為一種處理NOSCRIPT錯誤的Python Demo樣本,該demo利用Lua指令碼實現了字串prepend操作。
您可以考慮通過Python的redis-py解決該類錯誤,redis-py提供了封裝Redis Lua的一些底層邏輯判斷(例如NOSCRIPT錯誤的catch)的Script類。
import redis
import hashlib
# strin是一個Lua指令碼的字串,函數以字串的格式返回strin的sha1值。
def calcSha1(strin):
sha1_obj = hashlib.sha1()
sha1_obj.update(strin.encode('utf-8'))
sha1_val = sha1_obj.hexdigest()
return sha1_val
class MyRedis(redis.Redis):
def __init__(self, host="localhost", port=6379, password=None, decode_responses=False):
redis.Redis.__init__(self, host=host, port=port, password=password, decode_responses=decode_responses)
def prepend_inLua(self, key, value):
script_content = """\
local suffix = redis.call("get", KEYS[1])
local prefix = ARGV[1]
local new_value = prefix..suffix
return redis.call("set", KEYS[1], new_value)
"""
script_sha1 = calcSha1(script_content)
if self.script_exists(script_sha1)[0] == True: # 檢查Redis是否已緩衝該指令碼。
return self.evalsha(script_sha1, 1, key, value) # 如果已緩衝,則用EVALSHA執行指令碼
else:
return self.eval(script_content, 1, key, value) # 否則用EVAL執行指令碼,注意EVAL有將指令碼緩衝到Redis的作用。這裡也可以考慮採用SCRIPT LOAD與EVALSHA的方式。
r = MyRedis(host="r-******.redis.rds.aliyuncs.com", password="***:***", port=6379, decode_responses=True)
print(r.prepend_inLua("k", "v"))
print(r.get("k"))
Lua指令碼逾時錯誤
由於Lua指令碼在執行個體中是原子執行的,Lua慢請求可能會導致執行個體阻塞。單個Lua指令碼阻塞執行個體最多5秒,5秒後執行個體會給所有其他命令返回如下BUSY error報錯,直到指令碼執行結束。
BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.解決方案:
您可以通過SCRIPT KILL命令終止Lua指令碼或等待Lua指令碼執行結束。
說明SCRIPT KILL命令在執行慢Lua指令碼的前5秒不會生效(阻塞中)。
建議您編寫Lua指令碼時預估指令碼的執行時間,同時檢查死迴圈等問題,避免過長時間阻塞執行個體導致服務不可用,必要時請拆分Lua指令碼。
若當前Lua指令碼已執行寫命令,則SCRIPT KILL命令將無法生效,報錯樣本如下。
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.解決方案:
請在控制台的執行個體列表中單擊對應執行個體重啟。
持久化與複製問題
在不重啟、不調用SCRIPT FLUSH命令的情況下,執行個體會一直緩衝執行過的Lua指令碼。但在部分情況下(例如執行個體遷移、變更配置、版本升級、切換等等),執行個體無法保證Lua指令碼的持久化,也無法保證Lua指令碼能夠被同步至其他節點。
解決方案:
由於執行個體不保證Lua指令碼的持久化、複製能力,請您在本機存放區所有Lua指令碼,在必要時通過EVAL或SCRIPT LOAD命令將Lua指令碼重新緩衝至執行個體中,避免執行個體重啟、HA切換等操作時執行個體中的Lua指令碼被清空而帶來的NOSCRIPT錯誤。
叢集架構特殊限制
叢集架構約束
為了保證Lua執行的原子性,Lua命令不可拆分,只能在叢集架構的一個DB分區上執行。通常會根據Key來決定路由到哪個DB分區執行,所以在叢集架構中執行Lua命令時至少需要指定一個Key。如果讀寫多個Key,則同一個Lua指令碼中的Key必須屬於同一個Slot,否則會導致執行結果異常。對於KEYS、SCAN、FLUSHDB等無Key的命令,雖然能正常執行,但返回結果只包含單個分區的資料。上述限制由Redis Cluster架構導致。
對單個節點執行SCRIPT LOAD命令時,不保證將該Lua指令碼存入至其他節點中。
代理模式(Proxy)錯誤碼
Proxy會通過語法檢查來提前識別Key跨越多個Slot的情況,提前暴露異常,方便問題排查。Proxy檢查方法和Lua虛擬機器存在差異,這導致了在Proxy中執行Lua命令會存在額外限制(例如不支援UNPACK命令、不支援在MULTI、EXEC事務中使用EVAL、EVALSHA、SCRIPT系列命令等)。
您也可以通過關閉script_check_enable參數配置關閉Proxy對Lua文法的部分檢查。
同時,讀寫分離架構執行個體如果開啟了readonly_lua_route_ronode_enable配置,Proxy會檢查Lua是否只包含唯讀命令並決定能否將Lua轉寄到唯讀節點,該檢查邏輯對Lua文法存在限制。
關閉script_check_enable參數配置對執行個體有什麼影響?
當執行個體為相容Redis 5.0版本(小版本5.0.8以下)、4.0及以下版本,不推薦關閉,可能會導致指令碼執行結果錯誤但返回正確。
其他版本關閉後,Proxy將不再檢查Lua文法,但資料節點仍會正常檢查Lua文法。
具體錯誤碼和原因如下。
Redis Cluster 架構限制
錯誤碼:
-ERR for redis cluster, eval/evalsha number of keys can't be negative or zero\r\n說明:執行Lua時必須帶有Key,Proxy會根據Key決定將Lua轉寄到哪個DB分區上執行。
# 正確樣本 EVAL "return redis.call('get', KEYS[1])" 1 fooeval # 錯誤樣本 EVAL "return redis.call('get', 'foo')" 0錯誤碼:
-ERR 'xxx' command keys must in same slot說明:Lua指令碼中的多個Key必須屬於同一個Slot。
# 正確樣本: EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 foo {foo}bar # 錯誤樣本: EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 foo foobar
Proxy Lua語法檢查導致的額外限制
關閉 script_check_enable 參數配置可以避免Proxy對Lua文法額外檢查。
錯誤碼:
-ERR bad lua script for redis cluster, nested redis.call/redis.pcall說明:不支援Redis嵌套方式調用,您可以使用局部變數的方式進行調用。
# 正確樣本 EVAL "local value = redis.call('GET', KEYS[1]); redis.call('SET', KEYS[2], value)" 2 foo bar # 錯誤樣本 EVAL "redis.call('SET', KEYS[1], redis.call('GET', KEYS[2]))" 2 foo bar錯誤碼:
-ERR bad lua script for redis cluster, first parameter of redis.call/redis.pcall must be a single literal string說明:redis.call/pcall中調用的命令必須是字串常量。
# 正確樣本 eval "redis.call('GET', KEYS[1])" 1 foo # 錯誤樣本 eval "local cmd = 'GET'; redis.call(cmd, KEYS[1])" 1 foo
讀寫權限問題
錯誤碼:
-ERR Write commands are not allowed from read-only scripts說明:通過EVAL_RO命令發送的Lua中不能包含寫命令。
錯誤碼:
-ERR bad write command in no write privilege說明:唯讀帳號發送的Lua中不能包含寫命令。
命令未支援
錯誤碼:
-ERR script debug not support說明:Proxy當前不支援SCRIPT DEBUG命令。
錯誤碼:
-ERR bad lua script for redis cluster, redis.call/pcall unkown redis command xxx說明:Lua中包含Proxy不支援的命令。更多資訊請參見叢集架構與讀寫分離執行個體的命令限制。
Lua 語法錯誤
錯誤碼:
-ERR bad lua script for redis cluster, redis.call/pcall expect '('或-ERR bad lua script for redis cluster, redis.call/redis.pcall definition is not complete, expect ')'說明:Lua語法錯誤,
redis.call後面必須包含完整的(與)。錯誤碼:
-ERR bad lua script for redis cluster, at least 1 input key is needed for ZUNIONSTORE/ZINTERSTORE說明:ZUNIONSTORE、ZINTERSTORE命令的numkeys參數必須大於0。
錯誤碼:
-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE key count < numkeys說明:ZUNIONSTORE、ZINTERSTORE命令的實際Key數量小於numkeys值。
錯誤碼:
-ERR bad lua script for redis cluster, xread/xreadgroup command syntax error說明:XREAD、XREADGROUP命令的文法不對,請檢查參數個數。
錯誤碼:
-ERR bad lua script for redis cluster, xread/xreadgroup command syntax error, streams must be specified說明:XREAD、XREADGROUP命令必須需要有streams參數。
錯誤碼:
-ERR bad lua script for redis cluster, sort command syntax error說明:SORT命令的語法錯誤。
常見問題
Q:DMS支援執行Lua指令碼嗎?
A:DMS控制台目前暫不支援使用Lua指令碼等相關命令,請通過用戶端或redis-cli串連執行個體使用Lua指令碼。