redis中lua脚本的简单使用
wptr33 2025-01-21 21:56 13 浏览
一、背景
在使用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
注意:
- 该命令只可以杀死正在运行的 只读脚本。
- 对于修改了数据的脚本,无法使用此命令杀死,只能使用 shutdown nosave命令。
- 脚本执行的默认超时时间为 5分钟,可以通过redis.conf配置文件的lua-time-limit配置项修改。
- 脚本即使到达了超时时间,也不会停止执行,因为这违反了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、额外的转换规则
- 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指令
- s 或 n: 运行到当前行并停止 (此时当前行还未执行)
- c:运行到下个断点,即运行到lua脚本中存在 redis.breakpoint()方法的地方
- list:列出当前行周围的一些源码
- p:打印出所有的 local 变量的值
- p <var>:打印具体的某个 local 变量的值
- r:执行 redis 命令 -- eg: r set key value r get key
3、debug运行结果
七、参考文档
- https://redis.io/topics/ldb
- https://redis.io/commands/eval
相关推荐
- 【推荐】一款开源免费、美观实用的后台管理系统模版
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍...
- Android架构组件-App架构指南,你还不收藏嘛
-
本指南适用于那些已经拥有开发Android应用基础知识的开发人员,现在想了解能够开发出更加健壮、优质的应用程序架构。首先需要说明的是:AndroidArchitectureComponents翻...
- 高德地图经纬度坐标批量拾取(高德地图批量查询经纬度)
-
使用方法在桌面上新建一个index.txt文件,把下面的代码复制进去保存,再把文件名改成index.html保存,双击运行打开即可...
- flutter系列之:UI layout简介(flutter ui设计)
-
简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。...
- Android开发基础入门(一):UI与基础控件
-
Android基础入门前言:...
- iOS的布局体系-流式布局MyFlowLayout
-
iOS布局体系的概览在我的CSDN博客中的几篇文章分别介绍MyLayout布局体系中的视图从一个方向依次排列的线性布局(MyLinearLayout)、视图层叠且停靠于父布局视图某个位置的框架布局(M...
- TDesign企业级开源设计系统越发成熟稳定,支持 Vue3 / 小程序
-
TDesing发展越来越好了,出了好几套组件库,很成熟稳定了,新项目完全可以考虑使用。...
- WinForm实现窗体自适应缩放(winform窗口缩放)
-
众所周知,...
- winform项目——仿QQ即时通讯程序03:搭建登录界面
-
上两篇文章已经对CIM仿QQ即时通讯项目进行了需求分析和数据库设计。winform项目——仿QQ即时通讯程序01:原理及项目分析...
- App自动化测试|原生app元素定位方法
-
元素定位方法介绍及应用Appium方法定位原生app元素...
- 61.C# TableLayoutPanel控件(c# tabcontrol)
-
摘要TableLayoutPanel在网格中排列内容,提供类似于HTML元素的功能。TableLayoutPanel控件允许你将控件放在网格布局中,而无需精确指定每个控件的位置。其单元格...
- 12个python数据处理常用内置函数(python 的内置函数)
-
在python数据分析中,经常需要对字符串进行各种处理,例如拼接字符串、检索字符串等。下面我将对python中常用的内置字符串操作函数进行介绍。1.计算字符串的长度-len()函数str1='我爱py...
- 如何用Python程序将几十个PDF文件合并成一个PDF?其实只要这四步
-
假定你有一个很无聊的任务,需要将几十个PDF文件合并成一个PDF文件。每一个文件都有一个封面作为第一页,但你不希望合并后的文件中重复出现这些封面。即使有许多免费的程序可以合并PDF,很多也只是简单的将...
- Python入门知识点总结,Python三大数据类型、数据结构、控制流
-
Python基础的重要性不言而喻,是每一个入门Python学习者所必备的知识点,作为Python入门,这部分知识点显得很庞杂,内容分支很多,大部分同学在刚刚学习时一头雾水。...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
面试官:git pull是哪两个指令的组合?
-
git pull命令使用实例 git pull--rebase
-
git 执行pull错误如何撤销 git pull fail
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mysql max (33)
- vba instr (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)