
Redis Lua定义
Redis 从 2.6 版本开始支持 Lua 脚本,客户端通过 Lua 脚本,可以将多个 Redis 命令组合成一个原子性操作在服务器上执行。
Lua脚本是一种由C编写的可嵌入的轻量级语言, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Redis Lua作用
1.保证操作原子性
例如:为了保证操作原子性,使用 Redis 实现分布式锁时,通常就会使用 Lua 脚本封装锁相关的原子操作。
2.减少网络开销
Lua脚本可以将多个指令组合到一个脚本中,这样可以将与服务器的交互次数,从多次变为一次,从而减少网络开销。
3.可重复使用
客户端发送的脚本会永久存储在Redis中,这样其他客户端就可以复用这一脚本,而不需要使用代码完成同样的逻辑,从而达到了重复使用的目的。
所以现在流传一句话:要想学好Redis必会Lua Script。
Redis使用Lua
Redis中执行lua可以通过两种方式:eval和evalsha。
1.eval
Redis从2.6开始,就内置了lua编译器,可以使用EVAL命令对lua脚本进行求值。
eval语法
EVAL script numkeys key [key ...] arg [arg ...]
参数说明:
- eval :代表执行 Lua 语言的命令;
- script: 就是Lua脚本内容;
- numkeys: 用于指定键名参数的个数;
- key [key …]: key列表,作为参数传递给Lua语言,lua中是用KEYS[n]来获取对应的参数;
- arg [arg …]: 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。
简单来说就是:
eval lua脚本片段 参数个数(假设参数个数=2) 参数1 参数2 参数1值 参数2值
eval实例
# set
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 lua-key lua-value
OK
# get
127.0.0.1:6379> EVAL "return redis.call('get', KEYS[1])" 1 lua-key
"lua-value"
127.0.0.1:6379> lpush person a b c
(integer) 3
# 多参数
127.0.0.1:6379> EVAL "return redis.call('lrange', KEYS[1], ARGV[1], ARGV[2])" 1 person 0 -1
1) "c"
2) "b"
3) "a"
redis.call:就是redis命令执行的返回值,如果出错,则返回错误信息,不继续执行;
2.evalsha
eval命令每次都要发送一次脚本本身的内容,从而每次都会编译脚本,所以Redis提供了一个缓存机制,不会每次都重新编译脚本,这就是evaklsha命令。
Redis实现了evaklsha命令,它的作用和eval一样,只是它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum),避免每次去发送Lua脚本。
evalsha语法
EVALSHA SHA1 numkeys key [key …] arg [arg …]
evalsha示例
# 使用script load将脚本内容加载到缓存中,返回sha的值
127.0.0.1:6379> script load "return redis.call('set',KEYS[1],ARGV[1])"
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
# 使用evalsha和返回的sha的值 + 参数个数 参数名称和值执行
127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 eval_02 002
OK
# 获取结果
127.0.0.1:6379> get eval_02
"002"
Redis Lua实例
典型的Redis分布式锁就会涉及到Lua脚本,以下就是Redisson中RedissonLock加/解锁使用lua的示例。
1.加锁Lua脚本
// 先执行exists key1命令,判断KEYS[1]是否存在
if (redis.call('exists', KEYS[1]) == 0)
then
// 不存在,执行hincrby key1 field命令,将ARGV[2]的值原子加1
// 并设置KEYS[1]的过期时间为ARGV[1]
// 返回nil
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 执行hexists key field命令,判断ARGV[2]是否存在
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
then
// 存在,与上面then逻辑一致
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 若key存在且key field也存在,执行pttl key命令,返回KEYS[1]的过期时间
return redis.call('pttl', KEYS[1]);
2.解锁Lua脚本
// 执行hexists key1 field命令,判断判断ARGV[3]是否存在
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)
then
// 不存在,返回nil
return nil;
end;
// 执行hincrby key1 field命令,获取结果
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0)
then
// counter大于0,执行pexpire key1 命令,设置key过期时间为ARGV[2]
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// 否则,执行del key命令,删除KEYS[1]
// 执行publish channel message命令,向KEYS[2]发送消息ARGV[1]
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
从这两段脚本可以看出Redis集成lua脚本的强大之处,Lua脚本可谓是Redis能力倍增器。
如果是一个非常耗时的脚本:操作大key、循环等,很有可能严重影响到服务器的吞吐量,如果不得不这么做,那么一定要记得设置执行超时时间。
mikechen睿哥
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。