全部產品
Search
文件中心

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

更新時間:Jul 09, 2025

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

基本文法

展開查看詳情

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

命令

文法

說明

EVAL

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

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

參數說明:

  • script:Lua指令碼。

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

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

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

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

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

  • 推薦使用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指令碼緩衝。

以下為部分命令的樣本,本文在執行以下命令前執行了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命令不可拆分,只能在叢集架構的一個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

部分版本存在的限制(僅Redis開源版 5.0小版本5.0.8以下、4.0及以下版本執行個體或Proxy代理版本雲原生版7.0.2以下 、Proxy代理版本經典版6.8.12以下)

說明
  • Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本執行個體或Proxy代理版本較低(雲原生版7.0.2以下 、經典版6.8.12以下)存在以下限制。

  • 通常情況下,若執行個體版本、代理版本高於上述版本,可以忽略以下內容。但若執行個體高於上述版本仍存在限制,請修改任意Proxy參數(例如query_cache_expire參數),等待1分鐘後重試。

  • 錯誤碼:-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。

    # 正確樣本:
    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傳遞。

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

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

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

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

  • 錯誤碼:-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傳遞。

  • 錯誤碼:-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傳遞。

  • 錯誤碼:-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傳遞。

讀寫權限問題

  • 錯誤碼:-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指令碼。