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年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》