本文中含有需要您注意的重要提示信息,忽略該信息可能對您的業務造成影響,請務必仔細閱讀。
云數據庫 Tair(兼容 Redis)實例支持Lua相關命令,通過Lua腳本可高效地處理CAS(compare-and-set)命令,進一步提升實例的性能,同時可以輕松實現以前較難實現或者不能高效實現的模式。本文介紹使用Lua腳本的基本語法與使用規范。
注意事項
數據管理服務DMS控制臺目前暫不支持使用Lua腳本等相關命令,請通過客戶端或redis-cli連接實例使用Lua腳本。
基本語法
命令 | 語法 | 說明 |
EVAL |
| 執行給定的腳本和參數,并返回結果。 參數說明:
說明
|
EVALSHA |
| 給定腳本的SHA1校驗和,實例將再次執行腳本。 使用EVALSHA命令時,若sha1值對應的腳本未緩存至Redis中,Redis會返回NOSCRIPT錯誤,請通過EVAL或SCRIPT LOAD命令將目標腳本緩存至Redis中后進行重試,詳情請參見處理NOSCRIPT錯誤。 |
SCRIPT LOAD |
| 將給定的script腳本緩存在實例中,并返回該腳本的SHA1校驗和。 |
SCRIPT EXISTS |
| 給定一個(或多個)腳本的SHA1,返回每個SHA1對應的腳本是否已緩存在當前實例中。腳本已存在則返回1,不存在則返回0。 |
SCRIPT KILL |
| 停止正在運行的Lua腳本。 |
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 架構限制 |
| 執行Lua時必須帶有Key,Proxy會根據Key決定將Lua轉發到哪個DB分片上執行。
|
| Lua腳本中的多個Key必須屬于同一個Slot。
| |
Proxy Lua語法檢查導致的額外限制 (關閉 script_check_enable 配置可以避免該檢查) |
| 不支持Redis嵌套方式調用,您可以使用局部變量的方式進行調用。
|
| redis.call/pcall中調用的命令必須是字符串常量。
| |
| 所有Key都應該由KEYS數組來傳遞,redis.call/pcall中調用的命令,Key的位置必須是KEYS array,且不能使用Lua變量替換KEYS,。 說明 僅Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本實例或Proxy代理版本較低(云原生版7.0.2以下 、經典版6.8.12以下)存在該限制。 如果實例版本、代理版本都符合要求但仍存在限制,請修改任意參數(例如query_cache_expire參數),等待1分鐘后重試。
| |
| ZUNIONSTORE、ZINTERSTORE命令的destination參數必須用KEYS傳遞。 說明 僅Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本實例或Proxy代理版本較低(云原生版7.0.2以下 、經典版6.8.12以下)存在該限制。 如果實例版本、代理版本都符合要求但仍存在限制,請修改任意參數(例如query_cache_expire參數),等待1分鐘后重試。 | |
| ZUNIONSTORE、ZINTERSTORE命令的numkeys參數不是常量。 說明 僅Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本實例或Proxy代理版本較低(云原生版7.0.2以下 、經典版6.8.12以下)存在該限制。 如果實例版本、代理版本都符合要求但仍存在限制,請修改任意參數(例如query_cache_expire參數),等待1分鐘后重試。 | |
| ZUNIONSTORE、ZINTERSTORE命令的numkeys參數不是數字。 說明 僅Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本實例或Proxy代理版本較低(云原生版7.0.2以下 、經典版6.8.12以下)存在該限制。 如果實例版本、代理版本都符合要求但仍存在限制,請修改任意參數(例如query_cache_expire參數),等待1分鐘后重試。 | |
| ZUNIONSTORE、ZINTERSTORE命令的所有Key必須通過KEYS傳遞。 說明 僅Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本實例或Proxy代理版本較低(云原生版7.0.2以下 、經典版6.8.12以下)存在該限制。 如果實例版本、代理版本都符合要求但仍存在限制,請修改任意參數(例如query_cache_expire參數),等待1分鐘后重試。 | |
-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及以下版本實例或Proxy代理版本較低(云原生版7.0.2以下 、經典版6.8.12以下)存在該限制。 如果實例版本、代理版本都符合要求但仍存在限制,請修改任意參數(例如query_cache_expire參數),等待1分鐘后重試。 | |
| SORT命令的Key必須通過KEYS傳遞。 說明 僅Redis開源版 5.0版本(小版本5.0.8以下)、4.0及以下版本實例或Proxy代理版本較低(云原生版7.0.2以下 、經典版6.8.12以下)存在該限制。 如果實例版本、代理版本都符合要求但仍存在限制,請修改任意參數(例如query_cache_expire參數),等待1分鐘后重試。 | |
讀寫權限問題 |
| 通過EVAL_RO命令發送的Lua中不能包含寫命令。 |
| 只讀賬號發送的Lua中不能包含寫命令。 | |
命令未支持 |
| Proxy當前不支持SCRIPT DEBUG命令。 |
| Lua中包含Proxy不支持的命令。更多信息請參見集群架構與讀寫分離架構實例的命令限制。 | |
Lua 語法錯誤 |
| Lua語法錯誤, |
| ZUNIONSTORE、ZINTERSTORE命令的numkeys參數必須大于0。 | |
| ZUNIONSTORE、ZINTERSTORE命令的實際Key數量小于numkeys值。 | |
| XREAD、XREADGROUP命令的語法不對,請檢查參數個數。 | |
| XREAD、XREADGROUP命令必須需要有streams參數。 | |
| SORT命令的語法錯誤。 |