Redis整合Lua脚本

文章目录

  • 一、Lua介绍
    • 1.1 Lua特点
  • 二、在Redis里调用lua脚本
    • 2.1 redis-cli 命令执行脚本
    • 2.2 eval命令执行脚本
    • 2.3 return返回脚本运行结果
    • 2.4 Redis和lua相关的命令
    • 2.5 观察lua脚本阻塞Redis
  • 三、进阶
    • 3.1 参数传递
      • KEYS和ARGV参数
      • redis-cli --eval 和eval命令
    • 3.2 流程控制
      • 分支语句
      • while循环调用
      • for循环调用
  • 四、springboot结合redis实现lua脚本的操作
    • 4.1 springboot集成redis
    • 4.2 使用lua脚本实现cas操作
    • 4.3 Redis整合lua脚本实例
      • 限流

一、Lua介绍

  • Lua是一种用C语言编写而成的轻量级的脚本语言。

1.1 Lua特点

  • 轻量性:lua语言的官方版本只具有核心和最基本的库,启动速度很快,非常适合嵌入其他语言编写的程序里,比如将lua嵌入到Redis里。

  • 扩展性:在lua语言里,包含了非常便于开发使用的扩展接口,能非常方便地扩展实现其他语言的功能。

  • 结合Redis和lua脚本语言的特性,如果在Redis里遇到如下需求,就可以引入lua脚本。

    • 重复执行相同类型的命令,比如要缓存1到1000的数字到内存里。
    • 在高并发场景下减少网络调用的开销,一次性执行多条命令。
    • Redis会将lua脚本作为一个整体来执行,天然具有原子性。
  • 由于Redis是以单线程的形式运行的,如果运行的lua脚本没有响应或者不返回值,就会阻塞整个Redis服务,并且在运行时lua脚本一般很难调试,所以在Redis整合lua脚本时应该确保脚本里的代码尽量少且尽可能结构清晰,以免造成阻塞整个Redis服务的情况。

  • Redis对lua脚本的支持是从Redis 2.6.0版本开始引入的,它可以让用户在Redis服务器内置的Lua解释器中执行指定的lua脚本。

  • 被执行的lua脚本可以直接调用Redis命令,并使用lua语言以及内置的函数库处理命令结果。

二、在Redis里调用lua脚本

2.1 redis-cli 命令执行脚本

lua脚本是一种解释语言,所以可以安装解释器以后再运行lua脚本,但这里是在redis里引入lua脚本,所以就将给出redis-cli 命令运行lua脚本的相关步骤。

创建/opt/lua目录,在其中创建redis-demo.lua文件。注意,lua脚本的文件扩展名一般都是.lua

redis.call('set','name','zhangsan')

在lua脚本里,可以通过redis.call方法调用Redis的命令。

  • 该方法的第一个参数是Redis命令。
  • 第二个以及后继参数是该Redis命令的参数。

通过如下的docker命令创建一个名为redis-lua的Redis容器,在其中通过-v参数把包含lua脚本的/opt/lua目录映射为容器里的/lua-script目录。这样启动后该容器的/lua-script目录里就能看到在外部操作系统里创建的lua脚本。

docker run -itd --name redis-lua -v /opt/lua:/lua-script -p 6379:6379 redis:latest

启动该容器后,可以通过如下的命令进入该容器的命令行窗口里

docker exec -it redis-lua /bin/bash

可以通过如下的redis-cli命令执行刚才创建的lua脚本,其中--eval是redis里执行lua脚本的命令,/lua-script/redis-demo.lua则表示该脚本的路径和文件名。

redis-cli --eval /lua-script/redis-demo.lua

运行上述命令后,得到的返回值是空(nil),这是因为该lua脚本没有通过return返回值。

如果用redis-cli 命令进入该Redis服务器,在通过get name命令就能看到通过上述lua脚本设置到缓存的那么值。

root@c1beeb888673:/data# redis-cli --eval /lua-script/redis-demo.lua
(nil)
root@c1beeb888673:/data# redis-cli 
127.0.0.1:6379> get name
"zhangsan"
127.0.0.1:6379> 

2.2 eval命令执行脚本

在实际项目里,如果lua脚本里包含的语句较多,那么一般会以lua脚本文件的方式来维护。

如果lua脚本里的语句很少,那么可以直接通过eval命令来执行脚本。

通过redis-cli命令进入Redis服务器的客户端里,随后运行如下的eval命令

EVAL 脚本内容 key参数的数量 [key...] [args...]
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1  age 22

通过keyarg这两类参数向脚本传递数据,他们的值可以在脚本中分别使用KEYSARGV两个表类型的全局变量访问。

key参数的数量是必须要指定的,没有key参数时必须设为0,EVAL会依据这个数值将传入的参数分别传入KEYSARGV两个表类型的全局变量。

2.3 return返回脚本运行结果

在刚才redis整合lua脚本的场景里,都是通过redis.call方法执行redis命令,并没有返回结果,在一些场景里,需要返回结果,此时就需要在脚本里引入return语句。

/opt/lua目录,在其中创建return-lua.lua文件。在其中加入如下的一句return代码。返回1这个结果。

return redis.call('set','name','tom')

进入该容器的命令窗口,在其中在运行命令,就能看到返回结果。

redis-cli --eval /lua-script/return-lua.lua

2.4 Redis和lua相关的命令

可以通过SCRIOPT LOAD命令事先装置脚本,随后可以用EVALSHA命令多次运行该脚本。

SCRIOPT LOAD '脚本内容'
EVALSHA 'id' 0
127.0.0.1:6379> SCRIPT LOAD "return 1"
"e0e1f9fabfc9d4800c877a703b823ac0578ff8db"
127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0
(integer) 1
127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0
(integer) 1
127.0.0.1:6379> EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff8db 0
(integer) 1
127.0.0.1:6379> 

通过EVALSHA 命令执行已经缓存到内存中的lua脚本时,第一个参数是该脚本的ID号,第二个参数0表示该脚本的参数个数是0。

可以通过SCRIPT FLUSH命令清空缓存里的所有lua脚本。

SCRIPT  FLUSH

可以通过SCRIPT KILL命令终止正在运行的脚本,如果当前没有脚本在运行,该命令会返回错误提示。

127.0.0.1:6379> SCRIPT KILL
(error) NOTBUSY No scripts in execution right now.

2.5 观察lua脚本阻塞Redis

Redis服务是单线程的,所以如果在lua脚本里代码编写不当,比如引入了死循环,就会阻塞住当前Redis线程,也就是说该Redis服务器就无法在对外提供服务了。

比如运行如下所示的eval命令,由于在脚本里引入了while死循环,之后就无法继续输入其他Redis命令了,也就是说当前Redis服务被阻塞了。

eval "while true do end" 0

可以使用script kill命令结束lua脚本

127.0.0.1:6379> eval "while true do end" 0
root@c1beeb888673:/data# redis-cli 
127.0.0.1:6379> get name
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
127.0.0.1:6379> script kill
OK
127.0.0.1:6379> get name
"tom"

所以,在Redis里整合lua脚本时需要非常小心

  • 需要确保该脚本尽量短小。
  • 如果逻辑相对复杂,一定要反复测试,以确保不会因为长时间运行而阻塞Redis缓存服务。

三、进阶

3.1 参数传递

KEYS和ARGV参数

Redis在调用lua脚本时,可以传入KEYS和ARGV这两种类型的参数,它们的区别是前者表示要操作的键名,后者表示非键名参数,但是这一要求并不是强制的,比如设置键值的脚本。

EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1  age 22

也可以写成

EVAL "return redis.call('SET',ARGV[1],ARGV[2])" 0  age 22

虽然规则不是强制的,但是不遵守这样的规则,可能会为后续带来不必要的麻烦,比如Redis 3.0 之后支持集群功能,开启集群后会将键发布到不同的节点上,所以在脚本执行前就需要知道脚本会操作那些键以便找到对应的节点,而如果脚本中的键名没有使用KEYS参数传递则无法兼容集群。

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

在第1行运行的脚本里,KEYS[1]表示KEYS类型的第一个参数。ARGV[1]ARGV[2]分别表示ARGV类型的第一个和第二个参数,注意,相关下标是从1开始的,不是从0开始。

第1行脚本双引号之后的1表示该脚本KEYS类型的参数是1个,这里在统计参数个数时,并不把ARGV自定义类型的参数统计在内,随后的key1 ,one 和two分别按次序指向KEYS[1],ARGV[1]和ARGV[2]。

执行该return语句后,输出了KEYS[1],ARGV[1]和ARGV[2]这三个参数具体的值。

第二个脚本与第一个脚本的差异在于:表示参数的个数的值从1变成2,所以这里表示KEYS类型的参数个数有两个。

redis-cli --eval 和eval命令

官网说明:

redis-cli  --eval 脚本文件 0
root@c1beeb888673:/data# cat /lua-script/script.lua 
return redis.call('SET',KEYS[1],ARGV[1])
root@c1beeb888673:/data# redis-cli --eval /lua-script/script.lua location:hastings:temp , 23
OK
root@c1beeb888673:/data# 

redis-cli --eval 不需要指定keys的数量,并且keys和argv之间使用,分隔,同时,两侧必须使用空格

3.2 流程控制

分支语句

在lua脚本里,可以用if...else语句来控制分支流程,具体语法如下

if (condition) then...
else...
end

注意,其中if 、then、else和end等关键字的写法,在如下的ifDemo.lua脚本里将演示在lua脚本里使用分支语句的做法。

if redis.call('exists','name')==1 thenreturn 'existed'
elseredis.call('set','name','tom')return 'not existed'
end

通过if语句判断redis.call命令执行的exists name语句是否返回1,如果返回1,就表示name键存在,执行第二行的return 'existed' 语句。否则执行第4行和第5行的else语句,给name键设值并返回not existed

root@c1beeb888673:/data# redis-cli --eval /lua-script/ifDemo.lua 
"existed"

while循环调用

在lua脚本里,可以使用while关键字实现循环调用的效果,具体语法如下所示:

while (condition)
do...
end

condition条件为true时,会执行do部门的语句块,否则退出该while循环语句。

local i=0
while(i<10)
doredis.call('set',i,i)i=i+1
end

在第1行里定义了i变量,在第2行的while循环条件里会判断i变量是否小于10,如果小于就进入第4行执行set操作,随后通过第5行的代码给i进行加1操作并退出本次while循环。

redis-cli --eval /lua-script/while-demo.lua

for循环调用

在lua脚本里,也可以使用for关键字来实现循环的调用,具体语法如下

for var=start ,end,step do...
end

在执行for循环前,首先会给var赋予start所演示的值,在执行每次循环语句时,会以step为步长递增start,当递增到end所示的值后会退出for循环,实例如下

for i=0,10,1 doredis.call('del',i)
end

四、springboot结合redis实现lua脚本的操作

4.1 springboot集成redis

  1. 添加Redis依赖项到你的pom.xml文件:
<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-data-redis</artifactId>  
</dependency>
  1. 在application.properties或application.yml文件中配置Redis连接参数:
spring.redis.host=localhost  
spring.redis.port=6379
  1. 使用StringRedisTemplate或RedisTemplate来执行Lua脚本

    首先我们要初始化成员变量:

  //lua脚本private DefaultRedisScript<Boolean> casScript;@Resourceprivate RedisTemplate redisTemplate;@PostConstructpublic void init(){casScript=new DefaultRedisScript<>();//lua脚本类型casScript.setResultType(Boolean.class);//lua脚本在哪加载casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));}
  1. 使用:
    public Boolean compareAndSet(String key,Long oldValue,Long newValue){List<String> keys=new ArrayList<>();keys.add(key);//参数一为lua脚本   //参数二为keys集合    对应KEYS[1]、KEYS[2]....//参数三为可变长参数  对应 ARGV[1]、ARGV[2]...return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue);}

如果对springboot集成redis有问题,可以看我之前的文章SpringBoot集成Redis

4.2 使用lua脚本实现cas操作

初始化:

 @Resourceprivate RedisTemplate redisTemplate;//lua脚本private DefaultRedisScript<Boolean> casScript;@PostConstructpublic void init(){casScript=new DefaultRedisScript<>();//lua脚本类型casScript.setResultType(Boolean.class);//lua脚本在哪加载casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));}public Boolean compareAndSet(String key,Long oldValue,Long newValue){List<String> keys=new ArrayList<>();keys.add(key);return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue);}

lua脚本:

local key=KEYS[1]
local oldValue=ARGV[1]
local newValue=ARGV[2]
local redisValue=redis.call('get',key)
if(redisValue==false or tonumber(redisValue)==tonumber(oldValue))
thenredis.call('set',key,newValue)return true
elsereturn false
end

使用:

  public Boolean compareAndSet(String key,Long oldValue,Long newValue){List<String> keys=new ArrayList<>();keys.add(key);return (Boolean) redisTemplate.execute(casScript,keys,oldValue,newValue);}

4.3 Redis整合lua脚本实例

基于Redis的lua脚本能确保Redis命令执行时的顺序性和原子性,所以在高并发的场景里会用两者整合的方法实现限流和防超卖等效果。

限流

限流是指某应用模块需要限制指定IP(或指定模块,指定应用)在单位时间内的访问次数。

这里将给出用lua脚本实现的基于计数模式的限流效果,示例如下

编写lua脚本

local obj=KEYS[1]
local limitNum=tonumber(ARGV[1])
local curVisitNum=tonumber(redis.call('get',obj) or '0')
if(limitNum == curVisitNum) thenreturn 0
elseredis.call('incrby',obj,'1')redis.call('expire',obj,ARGV[2])return curVisitNum+1
end

该脚本有三个参数:

  • KEYS[1]用来接收待限流的对象
  • ARGV[1]表示限流的次数
  • ARGV[2]表示限流的时间单位

该脚本的功能是限制KEYS[1]对象在ARGV[2]时间范围内只能访问ARGV[1]次。

  • 首先用KEYS[1]接收待限流的对象,比如模块或应用等,并把它赋给obj变量。
  • 用ARGV[1]参数接收到的表示限流次数的对象赋给limitNum ,注意这里需要用tonumber方法把包含限流次数的ARGV[1]参数转换成数值类型。
  • 通过if语句判断待限流对象的访问次数是否达到限流标准,如果达到,则返回0。
  • 如果没有达到限流标准,首先通过incrby命令对访问次数加1,然后通过expire命令设置表示访问次数的键值对的生存时间,即限流的时间范围,最后通过return语句返回当前对象的访问次数。
  • 在调用该lua脚本时,如果返回值是0,就说明当前访问量已经达到限流标准,否则还可以继续访问。
  private DefaultRedisScript<Long> limitScript;@PostConstructpublic void init(){limitScript=new DefaultRedisScript<>();//lua脚本类型limitScript.setResultType(Long.class);//lua脚本在哪加载limitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));}@GetMapping("testLimit")public String testLimit(){Boolean aBoolean = canVisit("limit", 3, 10);if (aBoolean){return "可以访问";}else {return "不可以访问";}}public Boolean canVisit(String key,int oldValue,int newValue){List<String> keys=new ArrayList<>();keys.add(key);Long execute = (Long)redisTemplate.execute(limitScript, keys, oldValue , newValue);System.out.println(execute);return !(0==execute);}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/231331.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

tp8 模型save保存方法 method not exist:think\db\Query->record

1.$schema 有一个字段存在&#xff0c;但是实际表中是缺少这个字段的 2.必填值&#xff0c;没有值

在 C++ 中局部变量和全局变量

在C中&#xff0c;局部变量和全局变量的地址值有以下区别&#xff1a; 存储位置&#xff1a; 局部变量&#xff1a;局部变量存储在栈&#xff08;stack&#xff09;上。每当函数被调用时&#xff0c;局部变量的存储空间会被分配&#xff0c;并在函数执行完毕后自动释放。全局变…

什么是关键词排名蚂蚁SEO

关键词排名是指通过搜索引擎优化&#xff08;SEO&#xff09;技术&#xff0c;将特定的关键词与网站相关联&#xff0c;从而提高网站在搜索引擎中的排名。关键词排名对于网站的流量和用户转化率具有至关重要的影响&#xff0c;因此它是SEO工作中最核心的部分之一。 如何联系蚂…

二叉树的最大深度(LeetCode 104)

文章目录 1.问题描述2.难度等级3.热门指数4.解题思路方法一&#xff1a;深度优先搜索GolangC 方法二&#xff1a;广度优先搜索GolangC 参考文献 1.问题描述 给定一个二叉树 root &#xff0c;返回其最大深度。 叉树的「最大深度」是指从根节点到最远叶子节点的最长路径上的节…

【SpringMVC】SpringMVC简介、过程分析、bean的加载和控制

文章目录 1. SpringMVC简介2. SpringMVC入门案例文件结构第一步&#xff1a;坐标导入第二步&#xff1a;创建SpringMVC容器的控制器类第三步&#xff1a;初始化SpringMVC环境&#xff0c;设定Spring加载对应的bean第四步&#xff1a;初始化Servlet容器&#xff0c;加载SpringMV…

AI电子秤---顶尖电子秤对接+AI服务

上文我们说道,要实现整个AI识别过程,我们需要电子秤+AI服务,本文将以顶尖OS2电子秤+某AI服务为例完成整个过程 1、电子秤通信 通常电子秤是以串口形式进行通信,这里需要注意的是,某些设备可能是有信号才进行数据通信,有些则可能是持续输出的,具体取决于对接电子秤品牌…

Leetcode sql50基础题最后的4题啦

算是结束了这个阶段了&#xff0c;之后的怎么学习mysql的方向还没确定&#xff0c;但是不能断掉&#xff0c;而且路是边走边想出来的。我无语了写完了我点进去看详情都不让&#xff0c;还得重新开启计划&#xff0c;那我之前的题解不都没有了&#xff01;&#xff01; 1.第二高…

软件测试的魅力何在?为什么很多人选择测试一行而不做开发?

术有专攻&#xff0c;开发和测试都有自己的技术栈领域&#xff0c;谁也代替不了谁。 接下来我就首先说说本人为什么不做开发&#xff0c;而是选择了测试&#xff1b;其次再谈谈测试的魅力。 问题1&#xff1a;为什么选择测试一行而不做开发&#xff1f; 个人工作12年&#xf…

《一书读懂物联网》前言

我们对知识的认知是有规律可循的&#xff0c;大都是从问题开始&#xff0c;对问题的界定、归纳等都是为解决知识增长或进化而服务的&#xff0c;正如波普尔知识进化图&#xff08;见图 i-1&#xff09;所示的那样。 科学始于问题&#xff0c;发现问题是科学知识增长的起点&…

软件开发模型

软件的“生命周期”一般分为6个阶段&#xff0c;即制定计划、需求分析、设计、编码、测试、运行和维护。在软件工程中&#xff0c;这个复杂的过程一般用软件开发模型来描述和表示。常见的软件开发模型有&#xff1a;以软件需求为前提的瀑布模型&#xff0c;渐进式开发模型(如螺…

华为OD机试 - 跳格子3(Java JS Python C)

题目描述 小明和朋友们一起玩跳格子游戏, 每个格子上有特定的分数 score = [1, -1, -6, 7, -17, 7], 从起点score[0]开始,每次最大的步长为k,请你返回小明跳到终点 score[n-1] 时,能得到的最大得分。 输入描述 第一行输入总的格子数量 n 第二行输入每个格子的分数 sc…

【C++】POCO学习总结(十九):哈希、URL、UUID、配置文件、日志配置、动态库加载

【C】郭老二博文之&#xff1a;C目录 1、哈希 1.1 说明 std::map和std::set 的性能是&#xff1a;O(log n) POCO哈希的性能比STL容器更好&#xff0c;大约快两&#xff1b; POCO中对应std::map的是&#xff1a;Poco::HashMap&#xff1b; POCO中对应std::set的是 Poco::Hash…

k8s-ingress 8

ExternalName类型 当集群外的资源往集群内迁移时&#xff0c;地址并不稳定&#xff0c;访问域名或者访问方式等会产生变化&#xff1b; 使用svc的方式来做可以保证不会改变&#xff1a;内部直接访问svc&#xff1b;外部会在dns上加上解析&#xff0c;以确保访问到外部地址。 …

AUTOSAR StbM模块的配置以及代码实现

AUTOSAR StbM模块的配置以及代码实现 1、AUTOSAR配置 2、StbM_Init 初始化各个变量。 3、StbM_MainFunction StbM_Rb_IsSyncTimeBase 同步的TimeBase的id范围是0-15 StbM_Rb_IsOffsetTimeBase offset的TimeBase的id范围是16-31 StbM_Rb_IsPureLocalTimeBase pure的Time…

接口自动化测试框架【AIM】

最近在做公司项目的自动化接口测试&#xff0c;在现有几个小框架的基础上&#xff0c;反复研究和实践&#xff0c;搭建了新的测试框架。利用业余时间&#xff0c;把框架总结了下来。 AIM框架介绍 AIM&#xff0c;是Automatic Interface Monitoring的简称&#xff0c;即自动化…

【华为数据之道学习笔记】5-8多维模型设计

多维模型是依据明确的业务关系&#xff0c;建立基于维度、事实表以及相互间连接关系的模型&#xff0c;实现多角度、多层次的数据查询和分析。如何设计出稳定、易扩展、高可用的数据模型来支持用户消费对数据主题联接至关重要。 多维模型设计有4个主要步骤&#xff0c;包括确定…

xv6 文件系统(下)

〇、前言 计算机崩溃后如何恢复&#xff0c;是一个很重要的话题。对于内存中的数据无关痛痒&#xff0c;开机后重新载入就能解决问题&#xff1b;但是对于持久化存储设备&#xff0c;当你尝试修改一个文件&#xff0c;突然断电当你重新打开文件后&#xff0c;这个文件的状态是…

基于VUE3+Layui从头搭建通用后台管理系统(前端篇)十五:基础数据模块相关功能实现

一、本章内容 本章使用已实现的公共组件实现系统管理中的基础数据中的验证码管理、消息管理等功能。 1. 详细课程地址: 待发布 2. 源码下载地址: 待发布 二、界面预览 三、开发视频 3.1 B站视频地址: 基于VUE3+Layui从头搭建通用后台管理系统合集-验证码功能实现 3.2 西瓜…

每日OJ题_算法_滑动窗口③_力扣1004. 最大连续1的个数 III

目录 力扣1004. 最大连续1的个数 III 解析代码 力扣1004. 最大连续1的个数 III 1004. 最大连续1的个数 III - 力扣&#xff08;LeetCode&#xff09; 难度 中等 给定一个二进制数组 nums 和一个整数 k&#xff0c;如果可以翻转最多 k 个 0 &#xff0c;则返回 数组中连续 …

uniAPP里面有router吗

uni-app 是一个使用 Vue.js 开发所有前端应用的框架&#xff0c;开发者编写一次代码&#xff0c;可发布到iOS、Android、H5、以及各种小程序等多个平台。 在 uni-app 中&#xff0c;路由器是用于进行页面跳转和传参的重要工具。然而&#xff0c;由于 uni-app 的跨平台特性&…