Redis 7.x 系列【31】LUA 脚本

有道无术,术尚可求,有术无道,止于术。

本系列Redis 版本 7.2.5

源码地址:https://gitee.com/pearl-organization/study-redis-demo

文章目录

    • 1. 概述
    • 2. 常用命令
      • 2.1 EVAL
      • 2.2 SCRIPT LOAD
      • 2.3 EVALSHA
      • 2.4 SCRIPT FLUSH
      • 2.5 其他
    • 3. 脚本复制
    • 4. 案例演示
      • 4.1 限流
      • 4.2 分布式锁

1. 概述

官方文档

Lua 语言是一种小巧的、高效的、可嵌入的脚本编程语言,由巴西里约热内卢天主教大学的一个研究小组于 1993 年开发。设计目的是为了通过灵活嵌入应用程序中,为应用程序提供灵活的扩展和定制功能。

参考资料:

  • Lua 官网
  • 菜鸟教程

Redis 2.6 版本开始支持用户在服务器上传和执行 Lua 脚本,从而实现复杂的操作逻辑。脚本在 Redis 中是以字符串的形式传递给服务器的,然后服务器会执行这些脚本并返回结果。脚本在 Redis 中由内置的执行引擎执行。目前,仅支持一个脚本引擎,即 Lua 5.1 解释器。

Lua 脚本的优势:

  • 原子性:保证一个 Lua 脚本在执行期间是原子性的,即脚本执行期间不会被其他命令打断。在执行脚本期间,服务器的所有活动都会被阻塞,直到脚本运行结束。
  • 减少网络开销:可以将多个命令打包成一个 Lua 脚本执行,减少与 Redis 服务器的网络交互次数。
  • 复用性:可以将复杂的逻辑编写成 Lua 脚本,然后在多个地方重复使用。
  • 高效率:于脚本在服务器上执行,因此从脚本读取和写入数据非常高效。

注意事项:脚本被视为客户端应用程序的一部分,因此它们没有名称、版本或持久性。因此,如果脚本丢失(例如在服务器重新启动、故障切换到副本后等),所有脚本可能需要随时由应用程序重新加载

2. 常用命令

2.1 EVAL

在服务端执行 Lua 脚本。

语法格式:

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

参数说明:

  • scriptLua 编写的脚本源代码
  • numkeys :脚本中用到的 key 的数量
  • [key [key ...]]key 键名称列表,在脚本中可以使用 KEYS[] 进行占位,例如 EVAL 的第三个参数会被赋值给 KEYS[1] ,多个依次类推
  • [arg [arg ...]]:参数列表,在脚本中可以使用 ARGV[] 进行占位,例如 EVAL 的第四个参数会被赋值给 ARGV[1] ,多个依次类推

注意事项:

  • 禁止滥用 Lua EVAL ,例如,每次调用 EVAL 时生成不同的脚本。这些脚本将被添加到 Lua 解释器并缓存到服务端,随着时间的推移会消耗大量内存。
  • Redis 7.4 开始,使用 EVALEVAL_RO 加载的脚本将根据一定数量(按最近最少使用顺序)从 Redis 中删除。可以通过 INFO 命令查看被驱逐的脚本数量。

为了确保脚本在单机和集群环境中正确执行,还需要注意:

  • Lua 脚本的操作应该基于传递给脚本的参数进行,而不应该直接访问存储中的数据结构内容,例如哈希表、列表或集合。
  • 不支持动态键名访问或基于数据内容的键名计算,因此所有访问的键必须在执行脚本时已经确定。这些输入键的名称作为 KEYS 全局运行时变量提供给 Lua 脚本使用。

简单示例:

localhost:0>EVAL "return 'Hello, scripting!'" 0
"Hello, scripting!"

示例说明:

  • return 'Hello, scripting!':使用双引号包含的脚本源代码,返回一个字符串
  • 0:没有使用到键

包含参数示例:

localhost:0>EVAL "return ARGV[1]" 0 Hello
"Hello"

示例说明:

  • 0:没有使用到键
  • Hello:将 Hello 传递给 ARGV[1]

包含键、参数示例:

redis> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3
1) "key1"
2) "key2"
3) "arg1"
4) "arg2"
5) "arg3"

示例说明:

  • 2:包含两个键
  • key1:将 Hello 传递给 KEYS[1],按照位置顺序依次类推
  • arg1:将 arg1 传递给 ARGV[1],按照位置顺序依次类推

可以通过 redis.call()redis.pcall()Lua 脚本中调用 Redis 命令,区别在于处理运行时错误(例如语法错误)的方式:

  • call():如果发生错误,错误会直接返回给执行它的客户端
  • pcall():遇到的错误会返回到脚本的执行环境

示例:

EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OK

2.2 SCRIPT LOAD

每次调用 EVAL 命令时,请求中都会包括脚本的源代码,重复调用来执行相同的脚本集会浪费网络带宽,并且在 Redis 中也会产生一些额外开销,因此 Redis 提供了脚本的缓存机制。

使用 SCRIPT LOAD 可以将 Lua 脚本加载到 Redis 服务器,服务器不会执行脚本,而是仅编译并加载到服务器的缓存中。加载后返回一个 SHA1 摘要,唯一标识了它在缓存中的位置,该摘要可以用于执行已加载的脚本。

语法格式:

SCRIPT LOAD script

注意事项:

  • 脚本缓存始终是易失性的,它不被视为数据库的一部分,也不会持久化存储。当服务器重新启动、在故障转移时从副本切换为主服务器时,或者通过调用 SCRIPT FLUSH 命令时,缓存可能会被清空。
  • 应用程序应首先用 SCRIPT LOAD 加载脚本,然后再次调用 EVALSHA 运行缓存的脚本。大多数 Redis 客户端已经提供了自动执行此过程的 API

示例:

redis> SCRIPT LOAD "return 'Immabe a cached script'"
"c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f"
redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
"Immabe a cached script

2.3 EVALSHA

用于执行已加载的 Lua 脚本。

语法格式:

EVALSHA <sha1> <numkeys> <key> [key ...] <arg> [arg ...]

参数说明:

  • <sha1>:之前已加载缓存的 Lua 脚本的 SHA1 值。

示例:

127.0.0.1:6379> SCRIPT LOAD "local key = KEYS[1]\nlocal value = ARGV[1]\n\nredis.call('SET', key, value)"
"5b405e1d1f5c91b27e7e2b091380a848d38a99d6"
127.0.0.1:6379> EVALSHA 5b405e1d1f5c91b27e7e2b091380a848d38a99d6 1 mykey "myvalue"
OK

注意事项:

  • SHA1 对应的脚本不存在时,会报错
  • 在使用管道时,尽量不要使用 EVALSHA ,管道请求中的命令按发送顺序执行,但其他客户端的命令可能会在这些命令之间交错执行。因此,可能会从管道请求中返回NOSCRIPT错误,但无法进行处理。因此,在管道中应当使用普通的 EVAL 或带参数的 EVAL

2.4 SCRIPT FLUSH

清空 Lua 脚本缓存。默认情况下,SCRIPT FLUSH 命令会同步清空缓存。从 Redis 6.2 开始,设置 lazyfree-lazy-user-flush 配置指令为 “yes” 将改变默认的清空模式为异步。

语法格式:

SCRIPT FLUSH [ASYNC | SYNC]

参数说明:

  • ASYNC:异步清空缓存
  • SYNC:同步清空缓存

注意事项:

  • 运行命令将完全清空脚本缓存,删除到目前为止执行的所有脚本。
  • 在正常操作期间,脚本应该在缓存中无限期地保留。

示例:

localhost:0>SCRIPT FLUSH
"OK"

2.5 其他

其他脚本相关的命令:

  • EVAL_ROEVAL 命令的只读变体,不能执行修改数据的命令。
  • EVALSHA_ROEVALSHA 命令的只读变体,不能执行修改数据的命令。
  • SCRIPT DEBUG:控制内置的 Redis Lua 脚本调试器。
  • SCRIPT EXISTS:判断脚本已存在于缓存中,一个或多个 SHA1 摘要作为参数。返回 1 表示存在, 0 不存在。
  • SCRIPT KILL:中断长时间运行脚本(即慢脚本)的唯一方式,除非关闭服务器。脚本在执行时间超过配置的最大执行时间阈值后被视为慢脚本。只能用于在执行期间未修改数据集的脚本(因为停止只读脚本不会违反脚本引擎的原子性保证)。

3. 脚本复制

集群部署环境下,至少有三个主节点,每个主节点有一个或多个从节点,主从之间使用完全复制保持数据一致性。由于脚本可以修改数据,Redis 确保脚本执行的所有写操作也会被发送到从节点以保持一致性。

脚本复制有以下两种方式:

  • 逐字复制:主节点将脚本的源代码发送到副本,副本然后执行脚本并应用写入效果。
  • 效果复制:只复制脚本的数据修改命令,副本随后运行这些命令而不执行任何脚本。

逐字复制模式意味着副本会重新执行主节点已完成的工作,这是一种浪费。更重要的是,它还要求所有写入脚本都是确定性的。效果复制模式虽然在网络流量方面可能更为冗长,但这种复制模式是确定性的,因此不需要特别考虑其他情况。

直到 Redis 3.2 版本之前,逐字脚本复制是唯一支持的模式,在 Redis 3.2 中添加了效果复制。在 Redis 5.0 中,效果复制成为默认模式,截至 Redis 7.0 ,不再支持逐字复制。

在效果复制模式下,当 Lua 脚本执行时, Redis 会收集 Lua 脚本引擎实际修改数据集的所有命令。当脚本执行结束时,脚本生成的命令序列会被封装成一个 MULTI/EXEC 事务,并发送到副本和 AOF

效果复制的优势:

  • 当脚本计算速度较慢,可以由少数写入命令替换时,重新在副本或重新加载 AOF上计算脚本是一种浪费。在这种情况下,仅复制脚本的效果要好得多。
  • 解除了非确定性函数的限制。例如,您可以在脚本中任意使用 TIMESRANDMEMBER 命令。
  • Lua 伪随机数生成器在每次调用时都是随机种子。

4. 案例演示

Redis 命令的执行是单线程的,Lua 脚本在执行期间是原子性的,并且可以同时执行多个命令。特别适用于多线程环境下,需要保证多个复杂操作的原子性的场景。

4.1 限流

演示需求: 基于固定时间窗口进行 IP 限流, 同一 IP 在一分钟内,只允许访问固定的次数。

resources 目录下添加限流脚本文件:

local ipKey = KEYS[1]; -- IP地址
local rate = tonumber(ARGV[1]); -- 允许的请求次数
local requestCount = redis.call('incr', ipKey); -- 每次请求 + 1
if (requestCount == 1) then -- 第一次请求redis.call('expire', ipKey, 60); -- 设置过期时间(60S)return true ; -- 返回是否允许访问
else -- 不是第一次请求if (requestCount > rate) then -- 如果当前请求次数大于允许的请求次数return false ;end
end

使用 Lettuce 客户端执行脚本:

    public static void main(String[] args) {// 创建客户端RedisClient redisClient = RedisClient.create("redis://:123456@127.0.0.1:6379/0");// 获取连接try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {// 执行命令RedisCommands<String, String> sync = connection.sync();// 获取脚本流byte[] lua = getResourceAsByteArray("limit.lua"); // 执行脚本boolean eval = sync.eval(lua, ScriptOutputType.BOOLEAN, new String[]{"127.0.0.1"}, "1");// 结果判断if (!eval) {System.out.println("当前访问过于频繁,请稍后重试");} else {System.out.println("允许访问");}} catch (Exception e) {throw new RuntimeException(e);} finally {redisClient.shutdown();}}public static byte[] getResourceAsByteArray(String resourceName) throws IOException {// 使用ClassLoader获取资源作为输入流try (InputStream inputStream = LuaTest.class.getClassLoader().getResourceAsStream(resourceName)) {if (inputStream == null) {return null;}// 将输入流转换为字节数组ByteArrayOutputStream outputStream = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int length;while ((length = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, length);}return outputStream.toByteArray();}}

4.2 分布式锁

演示需求: 商品秒杀防止超卖,使用 Redis +Lua 实现分布式锁(生产环境请使用成熟的框架)。

获取锁的命令:

SET key value NX EX expire_time

简单的实现原理:

  • 使用 SET NX... 命令设置值,设置成功表示获取到的锁,反之未获取(直接返回失败或循环重试),并设置一个过期时间(避免死锁)
  • 获取到锁后,执行业务逻辑
  • 执行完成后释放锁

resources 目录下添加尝试获取锁脚本文件 tryLock.lua

local lockKey = KEYS[1] -- 锁的key
local lockValue = ARGV[1]   -- 锁的值
local lockExpireTime = tonumber(ARGV[2])  -- 锁的过期时间(秒)
local acquiredLock = redis.call('SET', lockKey, lockValue, 'NX', 'EX', lockExpireTime) -- 获取锁
if acquiredLock thenreturn true   -- 获取锁成功
elsereturn false -- 获取锁失败
end

添加删除锁脚本文件 unlock.lua

local lockKey = KEYS[1] -- 锁的key
redis.call('DEL', lockKey) -- 删除锁

创建工具类封装相关分布式锁逻辑:

public class RedisLockUtils {/*** 尝试获取锁** @param sync   连接* @param key    锁的 Key* @param value  锁的值* @param second 过期时间* @return 是否获取到锁* @throws IOException*/public static boolean tryLock(RedisCommands<String, String> sync, String key, String value, String second) throws IOException {byte[] lua = RedisLockUtils.getResourceAsByteArray("tryLock.lua"); // 获取脚本流return sync.eval(lua, ScriptOutputType.BOOLEAN, new String[]{key}, value, second);      // 执行脚本}/*** 删除锁** @param sync 连接* @param key  锁的 Key* @return 是否获取到锁* @throws IOException*/public static void unlock(RedisCommands<String, String> sync, String key) throws IOException {byte[] lua = RedisLockUtils.getResourceAsByteArray("unlock.lua");boolean eval = sync.eval(lua, ScriptOutputType.BOOLEAN, new String[]{key});}public static byte[] getResourceAsByteArray(String resourceName) throws IOException {// 使用ClassLoader获取资源作为输入流try (InputStream inputStream = LuaLimitTest.class.getClassLoader().getResourceAsStream(resourceName)) {if (inputStream == null) {return null;}// 将输入流转换为字节数组ByteArrayOutputStream outputStream = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int length;while ((length = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, length);}return outputStream.toByteArray();}}
}

模拟秒杀,简单测试:

public class LuaLockTest {private static Integer stockCount = 100; // 商品库存数量private static String goodsId = "899632563356632489"; // 商品库存数量public static void main(String[] args) {// 创建客户端RedisClient redisClient = RedisClient.create("redis://:123456@127.0.0.1:6379/0");// 获取连接try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {// 秒杀逻辑try {// 获取到锁if (RedisLockUtils.tryLock(connection.sync(), goodsId, "123456", "10")) {// 减库存if (stockCount > 0) {stockCount = stockCount - 1;System.out.println("减库存成功,剩余库存:" + stockCount);} else {System.out.println("库存不足");}} else {System.out.println("库存不足");}} finally {// 释放锁RedisLockUtils.unlock(connection.sync(), goodsId);}} catch (Exception e) {throw new RuntimeException(e);} finally {redisClient.shutdown();}}
}

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

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

相关文章

【C++题解】1069. 字符图形5-星号梯形

问题&#xff1a;1069. 字符图形5-星号梯形 类型&#xff1a;嵌套循环、图形输出 题目描述&#xff1a; 打印字符图形。 输入&#xff1a; 一个整数&#xff08; 0<n<10 &#xff09;。 输出&#xff1a; 一个字符图形。 样例&#xff1a; 输入&#xff1a; 3输…

【公式解释】《系统论》《控制论》《信息论》的共同重构:探索核心公式与深度解析

《系统论》《控制论》《信息论》的共同重构&#xff1a;探索核心公式与深度解析 关键词&#xff1a;系统论、控制论、信息论、状态空间方程、系统矩阵。 Keywords: System theory, Control theory, Information theory, State-space equations, System matrices. 核心公式与…

访问控制列表(ACL)

文章目录 ACL原理与基本配置ACL分类ACL组成ACL规则的匹配与应用 ACL原理与基本配置 ACL(Access Control List&#xff0c;访问控制列表) 读取二层、三层、四层报文信息根据预先定义好的规则对报文进行过滤和分类实现网络访问控制、防止网络攻击和提高网络带宽利用率等目的提高…

Linux(虚拟机)的介绍

Linux介绍 常见的操作系统 Windows&#xff1a;微软公司开发的一款桌面操作系统&#xff08;闭源系统&#xff09;。版本有dos&#xff0c;win98&#xff0c;win NT&#xff0c;win XP , win7, win vista. win8, win10&#xff0c;win11。服务器操作系统&#xff1a;winserve…

论文阅读【检测】:商汤 ICLR2021 | Deformable DETR

文章目录 论文地址AbstractMotivation技术细节多尺度backbone特征MSDeformAttention 小结 论文地址 Deformable DETR 推荐视频&#xff1a;bilibili Abstract DETR消除对目标检测中许多手工设计的组件的需求&#xff0c;同时表现出良好的性能。然而&#xff0c;由于Transfor…

学习笔记之JAVA篇(0724)

p 方法 方法声明格式&#xff1a; [修饰符1 修饰符2 ...] 返回值类型 方法名&#xff08;形式参数列表&#xff09;{ java语句;......; } 方法调用方式 普通方法对象.方法名&#xff08;实参列表&#xff09;静态方法类名.方法名&#xff08;实参列表&#xff09; 方法的详…

软考:软件设计师 — 7.软件工程

七. 软件工程 1. 软件工程概述 &#xff08;1&#xff09;软件生存周期 &#xff08;2&#xff09;软件过程 软件开发中所遵循的路线图称为 "软件过程"。 针对管理软件开发的整个过程&#xff0c;提出了两个模型&#xff1a;能力成熟度模型&#xff08;CMM&#…

unity2D游戏开发06稳定,材质,碰撞器

稳定性 在操控玩家时,我们会发现玩家移动时,摄像头会有抖动,这是摄像机过度精确造成的。 创建名为RoundCameraPos的C#脚本,用Visual Studio打开 代码 using System.Collections; using System.Collections.Generic; using UnityEngine; using Cinemachine;//导入Cinemac…

DC系列靶场---DC 3靶场的渗透测试(一)

信息收集 Nmap扫描 nmap -sS -sV -T4 -p- -O 172.30.1.142//-sS TCP的SYN扫描 //-sV 服务版本检测 //-T4 野蛮的扫描&#xff08;常用&#xff09; //-O 识别操作系统 使用Nmap扫描只看到一个80端口&#xff0c;Apache的2.4.18版本。 http探测 使用Wappalyzer插件可以到…

SN65MLVD080使用手册

8通道半双工M-LVDS线路收发器 特性 低压差分30欧姆至55欧姆线路驱动器和接收器&#xff0c;支持信号速率高达250 Mbps&#xff1b;时钟频率高达125 MHz 满足或超过M-LVDS标准TIA/EIA-899多点数据交换规范 受控驱动器输出电压转换时间&#xff0c;提高信号质量 -1V至3.4V共模…

QQ微信头像制图工具箱小程序纯前端源码

QQ微信头像制图工具箱小程序纯前端源码&#xff0c;主要功能有文字九格、头像挂件生成、爆趣九宫格、形状九宫格、创意长图、情侣头像、猫狗交流器。 这个QQ微信小程序源码是纯前端的&#xff0c;基本上拿去就可以用&#xff0c;不过好像调用了很多API&#xff0c;由于最近时间…

前端web开发HTML+CSS3+移动web(0基础,超详细)——第1天

一、开发坏境的准备 1&#xff0c;在微软商店下载并安装VS Code 以及谷歌浏览器或者其他浏览器&#xff08;我这里使用的是Microsoft Edge&#xff09; 2&#xff0c;打开vs code &#xff0c;在电脑桌面新建一个文件夹命名为code&#xff0c;将文件夹拖拽到vs code 中的右边…

空气处理机组系统中的设计和选型参考

1、静压的选择&#xff1a; 1.机组所承受的正压值和负压值既不是指机组的机外静压&#xff0c;也不是指风机的压头&#xff0c;而是指机组内部与机组外部大气压的差值&#xff0c;具体的计算方法如下&#xff1a; 如图所示&#xff0c;机组的新、回、送风管阻力分别为A、B、C帕…

【轨物方案】开关柜在线监测物联网解决方案

随着物联网技术的发展&#xff0c;电力设备状态监测技术也得到了迅速发展。传统的电力成套开关柜设备状态监测方法主要采用人工巡检和定期维护的方式&#xff0c;这种方法不仅效率低下&#xff0c;而且难以保证设备的实时性和安全性。因此&#xff0c;基于物联网技术的成套开关…

Qt自定义MessageToast

效果&#xff1a; 文字长度自适应&#xff0c;自动居中到parent&#xff0c;会透明渐变消失。 CustomToast::MessageToast(QS("最多添加50张图片"),this);1. CustomToast.h #pragma once#include <QFrame>class CustomToast : public QFrame {Q_OBJECT pub…

图——“多对多”的逻辑结构

目录 1.什么是图&#xff1f; 图包含&#xff1a; 2.图的基本术语 无向图&#xff1a; 有向图&#xff1a; 权重&#xff1a;边上的数字 度&#xff1a; 邻接点&#xff1a; 完全图&#xff1a; 3.图的抽象数据类型定义 4.怎么在程序中表示一个图&#xff1f; 邻接矩…

Java的日期类

1.第一代日期类 ① Date类&#xff1a;精确到毫秒&#xff0c;代表特定的瞬间 public static void main(String[] args) { // 获取当前系统时间 // 这里的Date类是在java.util包 // 默认输出的格式是国外的格式Date date new Date();System.out.println…

C#体检系统源码,医院健康体检系统PEIS,C#+VS2016+SQLSERVER

体检中心/医院体检科PEIS系统源码&#xff0c;C#健康体检信息系统源码&#xff0c;PEIS源码 开发环境&#xff1a;C/S架构C#VS2016SQLSERVER 2008 检前&#xff1a; 多种预约方式网站预约、电话预约、微信平台预约及检前沟通&#xff0c;提前制作套餐&#xff0c;客人到达体检…

【原创】java+ssm+mysql医生信息管理系统设计与实现

个人主页&#xff1a;程序员杨工 个人简介&#xff1a;从事软件开发多年&#xff0c;前后端均有涉猎&#xff0c;具有丰富的开发经验 博客内容&#xff1a;全栈开发&#xff0c;分享Java、Python、Php、小程序、前后端、数据库经验和实战 开发背景&#xff1a; 随着信息技术的…

【七】Hadoop3.3.4基于ubuntu24的分布式集群安装

文章目录 1. 下载和准备工作1.1 安装包下载1.2 前提条件 2. 安装过程STEP 1: 解压并配置Hadoop选择环境变量添加位置的原则检查环境变量是否生效 STEP 2: 配置Hadoop2.1. 修改core-site.xml2.2. 修改hdfs-site.xml2.3. 修改mapred-site.xml2.4. 修改yarn-site.xml2.5. 修改hado…