Redis Lua脚本详解(使用语法及分布式锁实例)

Redis Lua脚本详解(使用语法及分布式锁实例)-mikechen

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面试题总结》,后台回复架构,即可获取《阿里架构师进阶专题全部合集

评论交流
    说说你的看法