一、背景

在使用redis的过程中,发现有些时候需要原子性去操作redis命令,而redis的lua脚本正好可以实现这一功能。比如: 扣减库存操作、限流操作等等。

redis的pipelining虽然也可以一次执行一组命令,但是如果在这一组命令的执行过程中,需要根据上一步执行的结果做一些判断,则无法实现。

二、使用lua脚本

Redis中使用的是 Lua 5.1 的脚本规范,同时我们编写的脚本的时候,不需要定义 Lua 函数。同时也不能使用全局变量等等。

1、lua脚本的格式和注意事项

1、格式

EVAL script numkeys key [key ...] arg [arg ...]

127.0.0.1:6379> eval "return {KEYS[1],ARGV[1],ARGV[2]}" 1 key1 arg1 arg2
1) "key1"
2) "arg1"
3) "arg2"
127.0.0.1:6379>

2、注意事项

Lua脚本中的redis操作的key最好都是通过 KEYS来传递,而不要写死。否则在Redis Cluster的情况下可能有问题.

1、好的写法

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'zhangsan')" 1 username
OK
127.0.0.1:6379> get username
"zhangsan"

redis命令操作的key是通过KEYS获取的。

2、差的写法

127.0.0.1:6379> eval "return redis.call('set','username','zhangsan')" 0
OK
127.0.0.1:6379> get username
"zhangsan"

redis命令操作的key是直接写死的。

2、将脚本加载到redis中

需求: 此处定义一个lua脚本,将输入的参数的值+1返回。

注意:

当我们把 lua脚本加载到redis中,这个脚本并不会马上执行,而是会缓存起来,并且返回sha1校验和,后期我们可以通过 EVALSHA 来执行这个脚本。

此处我们记住这个脚本加载后返回的hash值,在下一步执行的时候需要用到。

127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379>

3、执行lua脚本

1、通过eval执行

127.0.0.1:6379> eval "return tonumber(KEYS[1]) + 1" 1 100
(integer) 101
127.0.0.1:6379>

2、通过evalsha执行

ef424d378d47e7a8b725259cb717d90a4b12a0de的值为上一步通过 script load加载脚本后获取的。

127.0.0.1:6379> evalsha ef424d378d47e7a8b725259cb717d90a4b12a0de 1 100
(integer) 101
127.0.0.1:6379>

通过 evalsha 执行的好处是可以节省带宽。如果我们的lua脚本比较长,程序在执行的时候将lua脚本发送到redis服务器则可能耗费的带宽多,如果发送的是hash值的话,则耗费的带宽少。

4、判断脚本是否在redis服务器缓存中

127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 1
127.0.0.1:6379> script exists not-exists-sha1
1) (integer) 0
127.0.0.1:6379>

5、清空服务器上的脚本缓存

注意:

我们无法清除某一个脚本的缓存,只可以清楚所有的缓存,一般情况下没有必要清楚,因为即使有大量的脚本也不会太占用服务器内存。


127.0.0.1:6379> script load "return tonumber(KEYS[1])+1"
"ef424d378d47e7a8b725259cb717d90a4b12a0de"
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists ef424d378d47e7a8b725259cb717d90a4b12a0de
1) (integer) 0

6、杀死正在运行的脚本

127.0.0.1:6379> script kill

注意:

  1. 该命令只可以杀死正在运行的 只读脚本
  2. 对于修改了数据的脚本,无法使用此命令杀死,只能使用 shutdown nosave命令。
  3. 脚本执行的默认超时时间5分钟,可以通过redis.conf配置文件的lua-time-limit配置项修改。
  4. 脚本即使到达了超时时间,也不会停止执行,因为这违反了lua脚本的原子性。

三、lua和redis数据类型转换

Lua的数据类型和Redis的数据类型存在一对一的转换关系,如果将Redis类型转换成Lua类型,然后在转换成Redis类型,那么结果和初试值是一致的。

1、类型转换

Redis to Lua conversion table.

  • Redis integer reply -> Lua number
  • Redis bulk reply -> Lua string
  • Redis multi bulk reply -> Lua table (may have other Redis data types nested)
  • Redis status reply -> Lua table with a single ok field containing the status
  • Redis error reply -> Lua table with a single err field containing the error
  • Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type

Lua to Redis conversion table.

  • Lua number -> Redis integer reply (the number is converted into an integer)
  • Lua string -> Redis bulk reply
  • Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
  • Lua table with a single ok field -> Redis status reply
  • Lua table with a single err field -> Redis error reply
  • Lua boolean false -> Redis Nil bulk reply.

2、额外的转换规则

  1. Lua的布尔类型,Lua的True会转换成Redis的1

3、3个重要规则

1. 数字类型

在Lua中,只有一个number类型,整数和浮点数之间没有区别,如果我们在Lua中返回一个浮点数,实际返回的是一个整数,如果要返回浮点数,需要以字符串的方式返回。

127.0.0.1:6379> eval "return 3.98" 0
(integer) 3
127.0.0.1:6379> eval "return '3.98'" 0
"3.98"

2. lua数组存在nil

当 Redis 将 Lua 数组转换为 Redis 协议时,如果遇到 nil,则转换会停止。即 nil 后的值都不会返回。

127.0.0.1:6379> eval "return {1,2,'data',nil,'can not return value','vv'}" 0
1) (integer) 1
2) (integer) 2
3) "data"
127.0.0.1:6379>

3. Lua的Table类型包含建和值

出现这种情况返回的redis的是一个空数组

127.0.0.1:6379> eval "return {key1 ='value1',key2='value2'}" 0
(empty array)
127.0.0.1:6379>

四、lua脚本中输出日志

这个一般调试我们的脚本的时候比较有用。

redis.log(loglevel,message)

loglevel的取值范围:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

举例:

五、一个简单限流的案例

1、需求

在 1s 之内,方法最大的并发只能是 5。

1s 和 5 当作参数传递。

2、实现步骤

1、编写lua脚本


-- 输出用户传递进来的参数
for i, v in pairs(KEYS) do
redis.log(redis.LOG_NOTICE, "limit: key" .. i .. " = " .. v)
end
for i, v in pairs(ARGV) do
redis.log(redis.LOG_NOTICE, "limit: argv" .. i .. " = " .. v)
end -- 限流的key
local limitKey = tostring(KEYS[1])
-- 限流的次数
local limit = tonumber(ARGV[1])
-- 多长时间过期
local expireMs = tonumber(ARGV[2]) -- 当前已经执行的次数
local current = tonumber(redis.call('get', limitKey) or '0') -- 设置一个断点
redis.breakpoint() redis.log(redis.LOG_NOTICE, "limit key: " .. tostring(limitKey) .. " 在[" .. tostring(expireMs) .. "]ms内已经访问了 " .. tostring(current) .. " 次,最多可以访问: " .. limit .. " 次") -- 限流了
if (current + 1 > limit) then
return { true }
end -- 未达到访问限制
-- 访问次数+1
redis.call("incrby", limitKey, "1")
if (current == 0) then
-- 设置过期时间
redis.call("pexpire", limitKey, expireMs)
end return { false }

2、程序中执行lua脚本

完整代码: https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/springboot-redis-lua

六、lua脚本的debug

当我们编写好了lua脚本后,如果在执行的过程中发生了错误,那么我们如何该如何解决呢?此处我们来了解下如何debug lua 脚本。

1、lua脚本中的几个小命令

在 脚本中打一个断点

redis.breakpoint()

2、断点调试

1、执行命令


redis-cli --ldb --eval limit.lua invoked , 1 1000 limit.lua 需要debug的lua文件
invoked 为传递到 lua 脚本中 KEYS 的值
1 和 1000 为传递到 lua 脚本中 ARGV 的值 , 分割 出 KEYS 和 ARGV 的值

2、一些debug指令

  • help: 列出可用的debug指令
  • sn: 运行到当前行并停止 (此时当前行还未执行)
  • c:运行到下个断点,即运行到lua脚本中存在 redis.breakpoint()方法的地方
  • list:列出当前行周围的一些源码
  • p:打印出所有的 local 变量的值
  • p <var>:打印具体的某个 local 变量的值
  • r:执行 redis 命令
-- eg:
r set key value
r get key

3、debug运行结果

七、参考文档

  1. https://redis.io/topics/ldb
  2. https://redis.io/commands/eval

redis中lua脚本的简单使用的更多相关文章

  1. PHP中使用redis执行lua脚本示例

    摸索了一下在PHP中如何使用redis执行lua脚本,写了一个脚本如下,供以后参考 <?php $redis = new Redis(); #实例化redis类 $redis->conne ...

  2. Redis结合Lua脚本实现高并发原子性操作

    从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis … 案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚 ...

  3. .Net Core使用分布式缓存Redis:Lua脚本

    一.前言 运行环境window,redis版本3.2.1.此处暂不对Lua进行详细讲解,只从Redis的方面讲解. 二.Redis的Lua脚本 在Redis的2.6版本推出了脚本功能,允许开发者使用L ...

  4. 要想用活Redis,Lua脚本是绕不过去的坎

    前言 Redis 当中提供了许多重要的高级特性,比如发布与订阅,Lua 脚本等.Redis 当中也提供了自增的原子命令,但是假如我们需要同时执行好几个命令的同时又想让这些命令保持原子性,该怎么办呢?这 ...

  5. 快速入门Redis调用Lua脚本及使用场景介绍

    Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储.大多数开发人员可能听说过redis可以运行 Lua 脚本,但是可能不知道redis在什么情况下需要使用到Lua脚本. 一.阅读本文 ...

  6. 【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁

    spring boot基于redis的LUA脚本 实现分布式锁[都是基于redis单点下] 一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁 1.pom.xml &l ...

  7. redis之lua脚本

    背景介绍 redis数据库提供了一些管理功能比如 流水线:打包发送多条命令,并在一个回复里面接收所有被执行命令的结果.事务:一次执行多条命令,被执行的命令要么就全部都被执行,要么就一个也不执行.并且事 ...

  8. Redis学习-LUA脚本

    最近在做K线的项目中,需要计算商品的分时数据.为了保证多台机器对同一商品的计算的有序性,所以在Redis中进行计算,同时为了保证在分时数据计算过程的原子性所以使用了LUA脚本,Redis内置了对LUA ...

  9. Redis 的 Lua 脚本支持

    Redis 2.6.0 内置的Lua Script支持,可以在Redis的Server端一次运行大量逻辑. 整个Script默认是在一个事务里的. Script里涉及的所有Key尽量用变量,从外面传入 ...

随机推荐

  1. Linux常用命令 - head命令详解

    21篇测试必备的Linux常用命令,每天敲一篇,每次敲三遍,每月一循环,全都可记住!! https://www.cnblogs.com/poloyy/category/1672457.html 显示文 ...

  2. Abp VNext权限定义

    在Shop.Application.Contracts项目中Permissions目录下ShopPermissions定义权限名 namespace Shop.Permissions { public ...

  3. C# 多线程编程之锁的使用【互斥锁(lock)和读写锁(ReadWriteLock)】

    多线程编程之锁的使用[互斥锁(lock)和读写锁(ReadWriteLock)] http://blog.csdn.net/sqqyq/article/details/18651335 多线程程序写日 ...

  4. find用法 以及和exec xargs 的组合使用

    1.查找当前目录下所有的txt的文件 [root@master1 ~]# find . -name "*.txt" -type f ./a.txt ./b.txt 2.exec 结 ...

  5. 机器学习——EM算法

    1 数学基础 在实际中,最小化的函数有几个极值,所以最优化算法得出的极值不确实是否为全局的极值,对于一些特殊的函数,凸函数与凹函数,任何局部极值也是全局极致,因此如果目标函数是凸的或凹的,那么优化算法 ...

  6. Consul+Ocelot+Polly在.NetCore中使用(.NET5)-网关Ocelot+Consul

    相关文章 Consul+Ocelot+Polly在.NetCore中使用(.NET5)-Consul服务注册,服务发现 Consul+Ocelot+Polly在.NetCore中使用(.NET5)-网 ...

  7. 引人遐想,用 Python 获取你想要的 “某个人” 摄像头照片

    仅用来学习,希望给你们有提供到学习上的作用. 1.安装库 需要安装python3.5以上版本,在官网下载即可.然后安装库opencv-python,安装方式为打开终端输入命令行. 2.更改收件人和发件 ...

  8. PHP中的MySQLi扩展学习(二)mysqli类的一些少见的属性方法

    虽说是少见的一些属性方法,但是可能还是有不少同学在日常的开发中使用过,这里只是学习了可能相对来说我们用得比较少的一些 mysqli 的属性或方法.就当是扩展一下自己的知识体系. 切换用户 首先就是切换 ...

  9. DEDEcms手机网站添加详情内页上一页/下一页的翻页功能

    修改文件include/arc.archives.class.php文件. 1.搜索 function GetPreNext($gtype='') 2.将这个函数的所有内容替换为 function G ...

  10. Python中“if __name__=='__main__':”

    在Python当中,如果代码写得规范一些,通常会写上一句"if name=='main:"作为程序的入口,但似乎没有这么一句代码,程序也能正常运行.这句代码多余吗?原理又在哪里? ...