全部產品
Search
文件中心

:Lua指令碼規範與常見報錯

更新時間:Aug 09, 2024

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

注意事項

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

基本文法

命令

文法

說明

EVAL

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

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

參數說明:

  • script:Lua指令碼。

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

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

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

說明
  • 與SCRIPT LOAD命令一樣,EVAL命令也會將Lua指令碼緩衝至Tair

  • 混用或濫用KEYS[]ARGV[]可能會導致Tair產生不符合預期的行為,尤其在叢集模式下,詳情請參見

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

EVALSHA

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

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

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

SCRIPT LOAD

SCRIPT LOAD script

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

SCRIPT EXISTS

SCRIPT EXISTS script [script ...]

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

SCRIPT KILL

SCRIPT KILL

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

SCRIPT FLUSH

SCRIPT FLUSH

清空當前Tair伺服器中的所有Lua指令碼緩衝。

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

以下為部分命令的樣本,本文在執行以下命令前執行了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

最佳化記憶體、網路開銷

現象:

Tair中緩衝了大量功能重複的指令碼,佔用大量記憶體空間甚至引發記憶體溢出(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指令碼緩衝將計入Tair的記憶體使用量量中,並會導致used_memory升高,當Tair的記憶體使用量量接近甚至超過maxmemory時,可能引發記憶體溢出(Out Of Memory),報錯樣本如下。

-OOM command not allowed when used memory > 'maxmemory'.

解決方案:

通過用戶端執行SCRIPT FLUSH命令清除Lua指令碼緩衝,但與FLUSHALL不同,SCRIPT FLUSH命令為同步操作。若Tair緩衝的Lua指令碼過多,SCRIPT FLUSH命令會阻塞Tair較長時間,可能導致執行個體不可用,請謹慎處理,建議在業務低峰期執行該操作。

說明

在控制台上單擊清除數據只能清除資料,無法清除Lua指令碼緩衝。

同時,請避免編寫過大的Lua指令碼,防止佔用過多的記憶體;避免在Lua指令碼中大批量寫入資料,否則會導致記憶體使用量急劇升高,甚至造成執行個體OOM。在業務允許的情況下,建議開啟資料逐出Tair預設開啟,模式為volatile-lru)節省記憶體空間。但無論是否開啟資料逐出,Tair均不會逐出Lua指令碼緩衝。

處理NOSCRIPT錯誤

現象:

使用EVALSHA命令時,若sha1值對應的指令碼未緩衝至Tair中,Tair會返回NOSCRIPT錯誤,報錯樣本如下。

(error) NOSCRIPT No matching script. Please use EVAL.

解決方案:

請通過EVAL命令或SCRIPT LOAD命令將目標指令碼緩衝至Tair中後進行重試。但由於Tair不保證Lua指令碼的持久化、複製能力,Tair在部分情境下仍會清除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:      # 檢查Tair是否已緩衝該指令碼。
            return self.evalsha(script_sha1, 1, key, value) # 如果已緩衝,則用EVALSHA執行指令碼
        else:
            return self.eval(script_content, 1, key, value) # 否則用EVAL執行指令碼,注意EVAL有將指令碼緩衝到Tair的作用。這裡也可以考慮採用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指令碼在Tair中是原子執行的,Lua慢請求可能會導致Tair阻塞。單個Lua指令碼阻塞Tair最多5秒,5秒後Tair會給所有其他命令返回如下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秒不會生效(Tair阻塞中)。

    • 建議您編寫Lua指令碼時預估指令碼的執行時間,同時檢查死迴圈等問題,避免過長時間阻塞Tair導致服務不可用,必要時請拆分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命令的情況下,Tair會一直緩衝執行過的Lua指令碼。但在部分情況下(例如執行個體遷移、變更配置、版本升級、切換等等),Tair無法保證Lua指令碼的持久化,也無法保證Lua指令碼能夠被同步至其他節點。

解決方案:

由於Tair不保證Lua指令碼的持久化、複製能力,請您在本機存放區所有Lua指令碼,在必要時通過EVAL或SCRIPT LOAD命令將Lua指令碼重新緩衝至Tair中,避免執行個體重啟、HA切換等操作時Tair中Lua指令碼被清空而帶來的NOSCRIPT錯誤。

叢集中Lua指令碼的限制

Redis Cluster對使用Lua指令碼增加了一些限制,在此基礎上,Tair叢集版對使用Lua指令碼存在如下額外限制:

  • 小版本限制,若無法執行EVAL的相關命令,並報錯ERR command eval not support for normal user時,請升級小版本後重試,具體操作請參見升級小版本

  • 所有Key必須在一個slot上,否則報錯-ERR eval/evalsha command keys must be in same slot\r\n

    您可以通過CLUSTER KEYSLOT命令擷取目標Key的雜湊槽(Hash Slot)進行確認。

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

  • 不支援發布訂閱命令,包括PSUBSCRIBEPUBSUBPUBLISHPUNSUBSCRIBESUBSCRIBEUNSUBSCRIBE

  • 不支援UNPACK函數。

說明

若您能夠在代碼中確保所有操作都在相同slot(如果不能保障這一點,執行會出錯),且希望打破Tair叢集的Lua限制,可以在控制台將script_check_enable修改為0,則後端不會對指令碼進行校正,但仍需要使用KEYS數組至少傳遞一個key,供代理節點執行路由轉寄。具體操作,請參見設定執行個體參數

代理模式(Proxy)對Lua的額外檢測項

您也可以通過script_check_enable參數關閉以下檢查項(不推薦)。

  • 所有key都應該由KEYS數組來傳遞,redis.call/pcall中調用的Tair命令,key的位置必須是KEYS array,且不能使用Lua變數替換KEYS,否則返回錯誤資訊:-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array\r\n

    說明

    僅Tair記憶體型(相容Redis 5.0),且小版本低於5.0.9的執行個體存在該限制。

    正確與錯誤命令樣本如下:

    # 本樣本的準備工作需執行如下命令。
    SET foo foo_value
    SET {foo}bar bar_value
    
    # 正確樣本:
    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。
  • redis.call/pcall中調用的Tair命令必須是字串常量,否則返回錯誤資訊:-ERR bad lua script for redis cluster, first parameter of redis.call/redis.pcall must be a single literal string

    正確與錯誤命令樣本如下:

    # 正確樣本
    eval "redis.call('GET', KEYS[1])" 1 foo
    
    # 錯誤樣本
    eval "local cmd = 'GET'; redis.call(cmd, KEYS[1])" 1 foo
  • 調用必須要帶有Key,否則返回錯誤資訊:-ERR for redis cluster, eval/evalsha number of keys can't be negative or zero\r\n

    說明

    僅Tair記憶體型(相容Redis 5.0),且小版本低於5.0.9的執行個體存在該限制。

    正確與錯誤命令樣本如下:

    # 正確樣本
    EVAL "return redis.call('get', KEYS[1])" 1 fooeval
    
    # 錯誤樣本
    EVAL "return redis.call('get', 'foo')" 0
  • 不支援Redis嵌套方式調用,否則直接返回錯誤資訊:-ERR bad lua script for redis cluster, nested redis.call/redis.pcall

    您可以使用局部變數的方式進行調用,正確與錯誤命令樣本如下:

    # 正確樣本
    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
  • 不支援在MULTI、EXEC事務中使用EVAL、EVALSHA、SCRIPT系列命令。

  • 不支援在Lua中執行跨Tair節點的命令,例如KEYS、SCAN等。

    為了保證Lua執行的原子性,Proxy會根據KEYS參數將Lua發送到一個Tair節點執行並擷取結果,從而導致該結果與全域結果不一致。

說明

若您需要使用代理模式下受限的部分功能,您可以嘗試開通使用Tair叢集版的直連模式。但是由於Tair叢集版在遷移、變更配置時都會通過proxy代理遷移資料,直連模式下不符合代理模式的Lua指令碼會遷移、變更配置失敗。

建議您在直連模式下使用Lua指令碼時應儘可能符合代理模式下的限制規範,避免後續Lua指令碼遷移、變更配置失敗。