全部產品
Search
文件中心

Tair (Redis® OSS-Compatible):Lua指令碼規範與常見報錯

更新時間:Nov 06, 2024

Tair (Redis OSS-compatible)執行個體支援Lua相關命令,通過Lua指令碼可高效地處理CAS(compare-and-set)命令,進一步提升執行個體的效能,同時可以輕鬆實現以前較難實現或者不能高效實現的模式。本文介紹使用Lua指令碼的基本文法與使用規範。

注意事項

Data Management控制台目前暫不支援使用Lua指令碼等相關命令,請通過用戶端或redis-cli串連執行個體使用Lua指令碼。

基本文法

命令

文法

說明

EVAL

EVAL script numkeys [key [key ...]] [arg [arg ...]]

執行給定的指令碼和參數,並返回結果。

參數說明:

  • script:Lua指令碼。

  • numkeys:指定KEYS[]參數的數量,非負整數。

  • KEYS[]:傳入的Redis鍵參數。

  • ARGV[]:傳入的指令碼參數。KEYS[]與ARGV[]的索引均從1開始。

說明
  • 與SCRIPT LOAD命令一樣,EVAL命令也會將Lua指令碼緩衝至執行個體。

  • 混用或濫用KEYS[]ARGV[]可能會導致執行個體產生不符合預期的行為,尤其在叢集模式下,詳情請參見叢集架構中Lua指令碼的限制

  • 推薦使用KEYS[]ARGV[]的方式傳遞參數。不推薦將參數編碼進指令碼中,過多類似行為會導致LUA虛擬機器記憶體使用量量上升,且無法及時回收,極端情況下會導致執行個體主庫與備庫記憶體溢出(Out of Memory),造成資料丟失。

EVALSHA

EVALSHA sha1 numkeys key [key ...] arg [arg ...]

給定指令碼的SHA1校正和,執行個體將再次執行指令碼。

使用EVALSHA命令時,若sha1值對應的指令碼未緩衝至Redis中,Redis會返回NOSCRIPT錯誤,請通過EVAL或SCRIPT LOAD命令將目標指令碼緩衝至Redis中後進行重試,詳情請參見處理NOSCRIPT錯誤

SCRIPT LOAD

SCRIPT LOAD script

將給定的script指令碼緩衝在執行個體中,並返回該指令碼的SHA1校正和。

SCRIPT EXISTS

SCRIPT EXISTS script [script ...]

給定一個(或多個)指令碼的SHA1,返回每個SHA1對應的指令碼是否已緩衝在當前執行個體中。指令碼已存在則返回1,不存在則返回0。

SCRIPT KILL

SCRIPT KILL

停止正在啟動並執行Lua指令碼。

SCRIPT FLUSH

SCRIPT FLUSH

清空當前執行個體中的所有Lua指令碼緩衝。

更多關於Redis命令的介紹,請參見Redis官網

以下為部分命令的樣本,本文在執行以下命令前執行了SET foo value_test

  • EVAL命令樣本:

    EVAL "return redis.call('GET', KEYS[1])" 1 foo

    返回樣本:

    "value_test"
  • SCRIPT LOAD命令樣本:

    SCRIPT LOAD "return redis.call('GET', KEYS[1])"

    返回樣本:

    "620cd258c2c9c88c9d10db67812ccf663d96bdc6"
  • EVALSHA命令樣本:

    EVALSHA 620cd258c2c9c88c9d10db67812ccf663d96bdc6 1 foo

    返回樣本:

    "value_test"
  • SCRIPT EXISTS命令樣本:

    SCRIPT EXISTS 620cd258c2c9c88c9d10db67812ccf663d96bdc6 ffffffffffffffffffffffffffffffffffffffff

    返回樣本:

    1) (integer) 1
    2) (integer) 0
  • SCRIPT FLUSH命令樣本:

    警告

    該命令會清空執行個體中的所有Lua指令碼緩衝,請提前備份Lua指令碼。

    SCRIPT FLUSH

    返回樣本:

    OK

最佳化記憶體、網路開銷

現象:

在執行個體中緩衝了大量功能重複的指令碼,佔用大量記憶體空間甚至引發記憶體溢出(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執行的原子性,Lua命令不可拆分,只能在叢集架構的一個DB分區上執行。通常會根據Key來決定路由到哪個DB分區執行,所以在叢集架構中執行Lua命令時至少需要指定一個Key。如果讀寫多個Key,則同一個Lua指令碼中的Key必須屬於同一個Slot,否則會導致執行結果異常。對於KEYS、SCAN、FLUSHDB等無Key的命令,雖然能正常執行,但返回結果只包含單個分區的資料。上述限制由Redis Cluster架構導致。

  • 對單個節點執行SCRIPT LOAD命令時,不保證將該Lua指令碼存入至其他節點中。

代理模式(Proxy)自訂的Lua錯誤碼及原因

Proxy會通過語法檢查來提前識別Key跨越多個Slot的情況,提前暴露異常,方便問題排查。Proxy檢查方法和Lua虛擬機器存在差異,這導致了在Proxy中執行Lua命令會存在額外限制(例如不支援UNPACK命令、不支援在MULTI、EXEC事務中使用EVAL、EVALSHA、SCRIPT系列命令等)。您也可以通過關閉script_check_enable參數配置關閉Proxy對Lua文法的部分檢查。

說明

關閉script_check_enable參數配置對執行個體有什麼影響?

  • 當執行個體為相容Redis 5.0版本(小版本5.0.8以下)、4.0及以下版本,不推薦關閉,可能會導致指令碼執行結果錯誤但返回正確。

  • 其他版本關閉後,Proxy將不再檢查Lua文法,但資料節點仍會正常檢查Lua文法。

同時,讀寫分離架構執行個體如果開啟了readonly_lua_route_ronode_enable配置,Proxy會檢查Lua是否只包含唯讀命令並決定能否將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 配置可以避免該檢查)

-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 bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array\r\n

所有Key都應該由KEYS數組來傳遞,redis.call/pcall中調用的命令,Key的位置必須是KEYS array,且不能使用Lua變數替換KEYS,。

說明

Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體存在該限制。

# 正確樣本:
EVAL "return redis.call('mget', KEYS[1], KEYS[2])" 2 foo {foo}bar

# 錯誤樣本:
EVAL "return redis.call('mget', KEYS[1], '{foo}bar')" 1 foo                      # '{foo}bar'作為Key,應該使用KEYS數組進行傳遞。
EVAL "local i = 2 return redis.call('mget', KEYS[1], KEYS[i])" 2 foo {foo}bar    # 在代理模式(Proxy)不允許執行此指令碼,因為KEYS資料的索引是變數,但在直連模式中無此限制。
EVAL "return redis.call('mget', KEYS[1], ARGV[1])" 1 foo {foo}bar                # 不應該使用ARGV[1]資料元素作為Key。

-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, include destination, and KEYS should not be in expression

ZUNIONSTORE、ZINTERSTORE命令的destination參數必須用KEYS傳遞。

說明

Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體存在該限制。

-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE numkeys parameter should be a single number and not expression

ZUNIONSTORE、ZINTERSTORE命令的numkeys參數不是常量。

說明

Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體存在該限制。

-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE numkeys value is not an integer or out of range

ZUNIONSTORE、ZINTERSTORE命令的numkeys參數不是數字。

說明

Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體存在該限制。

-ERR bad lua script for redis cluster, ZUNIONSTORE/ZINTERSTORE all the keys that the script uses should be passed using the KEYS array

ZUNIONSTORE、ZINTERSTORE命令的所有Key必須通過KEYS傳遞。

說明

Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體存在該限制。

-ERR bad lua script for redis cluster, XREAD/XREADGROUP all the keys that the script uses should be passed using the KEYS array

XREAD、XREADGROUP命令的所有Key必須通過KEYS傳遞。

說明

Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體存在該限制。

-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, and KEYS should not be in expression, sort command store key does not meet the requirements

SORT命令的Key必須通過KEYS傳遞。

說明

Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體存在該限制。

讀寫權限問題

-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命令的語法錯誤。