Redis Lua脚本的使用,自定义命令解决分布式锁的抢占

一、Lua 的介绍

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入到应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 的特性
- 轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
- 可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。
其它特性:
- 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
- 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
- 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。

Lua 虽然简单小巧,但是功能十分强大,很多大公司和中间件都用到了Lua,比如说:
- 暴雪公司将Lua引入到“魔兽世界”中
- Rovio公司将Lua引入到“愤怒的小鸟”中
- Nginx将Lua作为扩展语言
- Redis支持执行Lua脚本

这里就不对 Lua 做过多的介绍了,想全面学习Lua的可以点击Lua官网或者Lua教程进行学习。

二、Redis中使用Lua

Redis支持执行Lua脚本,Lua脚本提供了Redis命令更多的灵活性。

1.Redis中执行Lua脚本

在Redis中执行Lua脚本有两种方法:eval和evalsha。

eval 命令

eval 命令的语法规则是:

eval Lua脚本内容 key个数 key列表 参数列表

比如说:

192.168.1.4:0>eval 'return "I " .. KEYS[1] .. ARGV[1]' 1 am lebron
"I amlebron"

上面这条命令中Lua脚本内容是’return “I ” .. KEYS[1] .. ARGV[1]’,其中 .. 表示连接两个字符串,key个数是 1 ,KEYS[1] = am,ARGV[1] = lebron。

evalsha 命令

有时候Lua脚本内容太长放在eval命令中不合适,并且每次执行eval命令中都要带上Lua脚本,会增大请求的网络开销。所以Redis提供了 evalsha 命令来解决这个的问题。

我们可以将需要执行的Lua脚本加载到Redis内存中,Redis会返回这个Lua脚本的SHA1校验和,之后每次执行Lua脚本的时候只需要使用 evalsha 命令搭配上这个SHA1校验和就行了。

evalsha 命令的参数传递格式和 eval 命令一样,两个命令的唯一区别就在于是使用SHA校验和还是原生Lua脚本。

将Lua脚本加载到Redis内存之后,脚本功能就能得到复用,从而降低网络开销。

可以使用 script load 命令将脚本内容加载到Redis内存,并且返回SHA1校验和

192.168.1.4:0>script load 'return "I " .. KEYS[1] .. ARGV[1]'
"b4702819a82ced22ed16c94fb06f73ee21acbaba"

也可以将脚本内容写到文件中,通过加载Lua脚本文件来将Lua脚本加载到Redis内存

[root@localhost temp]# redis-cli script load "$(cat hello.lua)"
"a5260dd66ce02462c5b5231c727b3f7772c0bcc5"

Lua脚本加载到Redis内存中之后就可以使用 evalsha 命令去执行Lua脚本了

192.168.1.4:0>evalsha b4702819a82ced22ed16c94fb06f73ee21acbaba 1 am lebron
"I amlebron"
3、Lua脚本中使用Redis命令

上面案例中的Lua脚本只是最简单的字符串拼接和返回,没有涉及到Redis命令。

在Lua脚本中可以使用 redis.call 来调用Redis的命令:

192.168.1.4:0>eval 'return redis.call("set", KEYS[1], ARGV[1])' 1 hello world
"OK"
192.168.1.4:0>eval 'return redis.call("get", KEYS[1])' 1 hello
"world"

在Lua脚本中也可以使用 redis.pcall 来调用Redis的命令,两个命令的区别在于如果 redis.call 命令执行失败,那么Lua脚本结束并且返回错误;如果 redis.pcall 命令执行失败,会忽略错误继续执行脚本。

使用分布式锁的时候一般需要使用到 setnx 命令来抢占锁,然后需要使用 expire 命令对锁设置一个有效期,防止发生异常导致锁永远不被释放。但是Redis没有提供一个命令来同时实现这两个功能,我们可以使用Lua脚本来帮我们做到这个事情。

下面开始使用Lua脚本来帮我们实现这个原子操作:

创建Lua脚本文件

创建 setnxex.lua 文件,写入Lua脚本:

local setnx = redis.call("setnx", KEYS[1], ARGV[1])
if setnx == 1
then
    return redis.call("expire", KEYS[1], 10)
else
    return 0
end
将Lua脚本文件写到Redis内存
[root@localhost temp]# redis-cli script load "$(cat setnxex.lua)"
"98e3404350b75fc868964bf30068df1cc3fdd543"

得到Lua脚本文件的SHA1校验和。

使用 evalsha 命令抢占分布式锁
192.168.1.4:0>evalsha 98e3404350b75fc868964bf30068df1cc3fdd543 1 lockkey value
"1"
  • 返回 1 表示抢占分布式锁成功
  • 返回 0 表示抢占分布式锁失败
4、管理Redis内存中的Lua脚本

Redis提供了 4 个命令实现对Lua脚本的管理。

script load
script load Lua脚本内容

这个命令用于将Lua脚本加载到Redis内存,前面已经介绍过了。

scripts exists
scripts exists sha1 [sha1 ...]

这个命令用于校验 sha1 [sha1 …] 被加载到Redis内存的个数。

只传递一个参数就可以校验这个脚本有没有被加载到Redis内存。

script flush
script flush

这个命令用于清除Redis内存中的所有Lua脚本。

script kill
script kill

这个命令用于杀死正在执行的Lua脚本,如果我们的Lua脚本执行比较耗时或者Lua脚本有问题导致进入了死循环,可以调用这个命令来杀死正在进行的Lua脚本命令。

三、总结

优点
  • Lua脚本在Redis中是原子执行的,执行过程不会受其他客户端命令影响
  • Lua脚本可以帮助我们定制化符合自己业务的命令,并且可以常驻内存以达到复用的效果
  • 可以将一些批处理任务写到Lua脚本中,减少网络开销
缺点

Redis中提供了一个 lua-time-limit 的配置,默认 5 秒,它是Redis Lua脚本的“超时时间”。

一旦Lua脚本执行时间超过这个值之后,Redis会停止对其他客户端的正常服务。其他客户端在执行命令的时候会收到下面的错误:

“Busy Redis is busy running a script. 
You can only call SCRIPT KILL or SHUTDOWN NOSAVE.”

此时Redis已经阻塞,无法对外正常提供服务,可以使用 script kill 命令停止Lua脚本命令或者直接使用 shutdown save 命令停止Redis服务。

一般遇到这种情况我们肯定倾向于使用 script kill 命令命令来停止Lua脚本而不是直接停止Redis服务。但是如果Lua脚本中已经执行过写操作的情况下,执行 script kill 命令会报错并且不会停止Lua脚本的执行,只能够停止Redis服务。

可见Redis Lua脚本虽然好用,但是如果使用姿势不正确,后果也是十分严重的。


喜欢这篇文章的朋友,欢迎扫描下图关注公众号lebronchen,第一时间收到更新内容。
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值