云数据库 Tair(兼容 Redis)实例支持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脚本。