Redis使用Lua

笔记来自

node使用redis-lua

Java使用redis-lua

两种 Lua 脚本

真正的node和java使用笔记移步【node/redis】和【Java/redis】笔记

支持

Redis从2.6.0版本开始提供了eval命令,通过内置的Lua解释器,可以让用户执行一段Lua脚本并返回数据,所以不需要本地安装lualit服务

两种脚本

  • eval,可以使用 EVAL 命令对 Lua 脚本进行求值
# script:执行的脚本
# numkeys:指定键名参数个数
# key:键名,可以多个(key1、key2),通过 KEYS[1] KEYS[2] 的形式访问
# atg:键值,可以多个(val1、val2),通过 ARGS[1] ARGS[2] 的形式访问
EVAL script numkeys key [key ...] arg [arg ...]

# 通过 KEYS[] 数组的形式访问 ARGV[],这里下标是以 1 开始,KEYS[1] 对应的键名为 name1,ARGV[2] 对应的值为 val2
EVAL "return redis.call('SET', KEYS[1], ARGV[2])" 2 name1 name2 val1 val2
# 通过 get 查看 name1 对应的值为 val2
get name1 // val2
  • EVALSHA,EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体 (script body)。Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,通过 EVALSHA 来实现,根据给定的 SHA1 校验码,对缓存在服务器中的脚本进行求值。SHA1 怎么生成呢?通过 script 命令,可以对脚本缓存进行操作
SCRIPT FLUSH:清除所有脚本缓存
SCRIPT EXISTS:检查指定的脚本是否存在于脚本缓存
SCRIPT LOAD:将一个脚本装入脚本缓存,但并不立即运行它
SCRIPT KILL:杀死当前正在运行的脚本

# script 换成了 EVALSHA sha1
EVALSHA sha1 numkeys key [key ...] arg [arg ...]

# 生成hash缓存
SCRIPT LOAD "redis.pcall('SET', KEYS[1], ARGV[2]);"
// "2a3b189808b36be907e26dab7ddcd8428dcd1bc8"
# 执行hash
EVALSHA 2a3b189808b36be907e26dab7ddcd8428dcd1bc8 2 name1 name2 val1 val2
# 通过 get 查看 name1 对应的值为 val2
get name1 // val2

内置了cjson

  • 序列化
# 创建一个文件 RedisLuaCjsonEncode.lua
local userName = ARGV[1];
local userObject = {
            name = userName,
            age = 14,
            address = 'China'
        }
local userJson = cjson.encode(userObject);
if redis.call('set', KEYS[1], userJson) == 0
then
        return -1
else
        return userJson
end

# 不用连接redis,直接执行,英文的逗号,前后都要有空格
redis-cli --eval D:\redis\lua\RedisLuaCjsonEncode.lua name , value
  • 反序列化
# 创建一个文件 RedisLuaCjsonEncode.lua
local userInfo = redis.call('get', KEYS[1])
local userJson = cjson.decode(userInfo)
return userJson.name;

# 不用连接redis,直接执行
redis-cli --eval D:\redis\lua\RedisLuaCjsonDecode.lua name

防止超卖

该场景下,如果设计方案中用户抢红包的行为不是放入队列的, 而是简单并发, 那么查询redis计数器这一步操作,很可能就会在临界点,例如已经被抢了9999个了,此时两个用户几乎同时抢红包,都查询到还能继续抢,最后发放了两个红包出去。当然,这还是乐观的,实际情况可能是瞬间并发量非常大,导致发放了更多的红包出去。

查询计数器跟存储抢到红包的信息两个操作是原子性的,redis为我们提供了multi事务,实际上也是无法做到的,multi只适合可以并行的操作,对于这种需要先执行A再能决定是否执行B的串行操作不适用。

此时lua就可以帮我们忙了,在lua里查询,判断,确定抢到红包完全是一个原子操作,也不需要把抢红包动作设计为一个队列,更不需要去担心并发的影响。

此外,两个操作合并为一个lua脚本去执行时,还节省了一步redis的io耗时。

进一步讲,redis缓存了所有执行过的lua脚本,只要设计得当,这个操作向redis传送脚本的带宽可以节省到一个sha码的大小。

所以lua脚本对于redis来说可以是一把利刃

Redis使用Lua的好处

  • 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。
  • 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
  • 复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。

Redis使用Lua的注意点

  • Lua脚本的bug特别可怕,由于Redis的单线程特点,一旦Lua脚本出现不会返回(不是返回值)得问题,那么这个脚本就会阻塞整个redis实例。
  • Lua脚本应该尽量短小实现关键步骤即可。(原因同上)
  • Lua脚本中不应该出现常量Key,这样会导致每次执行时都会在脚本字典中新建一个条目,应该使用全局变量数组KEYS和ARGV, KEYS和ARGV的索引都从1开始
  • 传递给lua脚本的的键和参数:传递给lua脚本的键列表应该包括可能会读取或者写入的所有键。传入全部的键使得在使用各种分片或者集群技术时,其他软件可以在应用层检查所有的数据是不是都在同一个分片里面。另外集群版redis也会对将要访问的key进行检查,如果不在同一个服务器里面,那么redis将会返回一个错误。(决定使用集群版之前应该考虑业务拆分),参数列表无所谓。。
  • lua脚本跟单个redis命令和事务段一样都是原子的已经进行了数据写入的lua脚本将无法中断,只能使用SHUTDOWN NOSAVE杀死Redis服务器,所以lua脚本一定要测试好。