Jason Pan

Redis Lua脚本 遇到问题和解决

潘忠显 / 2020-11-20


集群上脚本执行Luaexists和直接执行的结果“不一致”

redis> EVAL "local key = 'prefix::' .. KEYS[1]; return {key, redis.call('exists', key)};" 1 "panzhongxian"
1) "prefix::panzhongxian"
2) (integer) 0

redis> EXISTS prefix::panzhongxian
(integer) 1

这里说的“不一致”是发送Redis集群上的情况。这里“不一致”是加引号,因为这现象并非有什么BUG,而是使用脚本的方法不对。

EVAL and EVALSHA support is limited to scripts that take at least 1 key. If multiple keys are used, all keys must hash to the same server. You can ensure this by using the same hashtag for all keys. If you use more than 1 key, the proxy does no checking to verify that all keys hash to the same server, and the entire command is forwarded to the server that the first key hashes to

EVAL指令发送到Redis的代理机器上,会进行解析,根据key参数去hash到不同的实例上。

上边所使用的Lua脚本中,会先根据key hash到server上,但当server执行Lua脚本时,操作的真实的key,并非传入的KEYS[1],而是进行加工之后的新key,而根据新key hash到的server并不一定是本台server。上述例子中就不是,所以lua脚本判断存在时返回了0。

简言之,EVALEXISTS两条指令是在不同Redis Server上执行的,所以得到了不同的结果。

如何判断ok与err

Redis中有一些指令是可以返回OK的,比如SETHMSET,其返回值的说明是:

Return value Simple string reply: OK if SET was executed correctly

但是执行redis.call之后得到的lua变量并不是字符串"OK",直接与字符串比较会返回nil:

redis> eval "return redis.call('set', KEYS[1], '1')" 1 panzhongxian
OK

redis> eval "return redis.call('set', KEYS[1], '1') == 'OK'" 1 panzhongxian
(nil)

原因是,Redis和Lua之间进行数据结构转换的时候有一定的约定,在 Conversion between Lua and Redis data types 一小节中有说明。

Redis status reply -> Lua table with a single ok field containing the status Redis error reply -> Lua table with a single err field containing the error

所以正确的判断是否设置成功的方法应该是使用变量的.ok的内容来判断

redis> eval "return redis.call('set', KEYS[1], '1').ok == 'OK'" 1 panzhongxian
(integer) 1

传入数字参数比较

使用 Hash 的脚本,比较典型的场景是先判断这个 Hash 长度是否超过某个值,不过不超过,则设置进去;如果超过了,则返回长度或特定值:

redis> eval "local l=redis.call('HLEN', KEYS[1]); if l > tonumber(ARGV[1]) then return -l; end; redis.call('HSET', KEYS[1], ARGV[2], ARGV[3]); return redis.call('HLEN', KEYS[1]);" 1 hello 10 hk h

这里需要注意的是,ARGV 数组是被认为是 string 类型的数组,因此上边跟数字进行比较的时候,需要先将 ARGV[1] 转换成数字,不然会报下边的错误:

(error) ERR user_script:1: attempt to compare string with number script: xxxxxx, on @user_script:1.