【redis】lua脚本

news2025/3/15 13:13:05

在分布式系统与高并发架构的战场中,开发者们始终在与两个永恒的命题博弈:数据一致性系统性能。当我们试图用Redis构建高速缓存、实现分布式锁或设计秒杀系统时,往往会陷入这样的困境——如何在保证原子性的同时,避免网络往返带来的性能损耗?如何让复杂的多命令操作像单一指令般高效执行?

这正是Redis Lua脚本闪耀的舞台。作为Redis的"核武器级"特性,Lua脚本不仅实现了原子性、隔离性的操作保障,更能将复杂的业务逻辑压缩成服务端的高性能执行单元。

为什么要选择Lua脚本?

在Redis中使用Lua脚本能带来以下核心优势:

原子性保证:Lua脚本在Redis中以单线程方式原子执行,避免多命令操作时的竞态条件。例如库存扣减、分布式锁等场景必须依赖这种特性。

减少网络开销:将多个Redis命令合并为一个脚本执行,减少客户端与服务端之间的网络往返次数(RTT)。对于高频操作性能提升显著。

逻辑复用与版本控制:脚本上传后可通过SHA摘要重复调用,结合SCRIPT LOAD/EVALSHA实现服务端逻辑复用,避免重复传输代码。

服务端计算能力:利用Redis服务端的计算资源处理数据,减少客户端计算压力。例如实现复杂统计、数据过滤等操作。

事务增强版:相比MULTI事务,Lua脚本提供更灵活的逻辑控制(支持if/else、循环等),且执行期间不会被其他命令打断。

Lua脚本命令的使用

有关事务的命令可以通过help @scripting命令来查看。有关命令的使用可以通过help 命令来查看,例如help eval

EVAL

eval:执行脚本。

语法:

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

参数说明:

  • script:Lua脚本代码
  • numkeys:后面key数组的数量
  • key:脚本代码中所需要操作的redis中的key数组
  • arg:脚本代码中所需要用到的变量参数数组

使用:

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 k1 k2 v1 v2
1) "k1"
2) "k2"
3) "v1"
4) "v2"

SCRIPT系列命令

语法:

# 设置执行脚本为调试模式
SCRIPT DEBUG YES|SYNC|NO

# 验证脚本是否存在
SCRIPT EXISTS sha1 [sha1 ...]

# 清空脚本缓存
SCRIPT FLUSH [ASYNC|SYNC]

# 终止运行中的脚本
SCRIPT KILL -

# 缓存脚本,并返回SHA摘要
SCRIPT LOAD script

使用:

127.0.0.1:6379> script load "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
"a42059b356c875f0717db19a51f6aaca9ae659ea"

127.0.0.1:6379> script exists "a42059b356c875f0717db19a51f6aaca9ae659ea"
1) (integer) 1

127.0.0.1:6379> script flush
OK

127.0.0.1:6379> script exists "a42059b356c875f0717db19a51f6aaca9ae659ea"
1) (integer) 0

EVALSHA

evalsha:通过脚本的SHA1摘要执行已缓存的脚本。

语法:

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

使用:

127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 k1 k2 v1 v2
(error) NOSCRIPT No matching script. Please use EVAL.

127.0.0.1:6379> script load "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"
"a42059b356c875f0717db19a51f6aaca9ae659ea"

127.0.0.1:6379> evalsha a42059b356c875f0717db19a51f6aaca9ae659ea 2 k1 k2 v1 v2
1) "k1"
2) "k2"
3) "v1"
4) "v2"

EVAL命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。Redis有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗,Redis实现了EVALSHA命令,它的作用和EVAL一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum)。

如果服务器还记得给定的SHA1校验和所指定的脚本,那么执行这个脚本,如果服务器不记得给定的SHA1校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用EVAL代替EVALSHA。

Lua中执行redis命令

在Lua中,可以通过内置的函数redis.call()和redis.pcall()来执行redis命令。

redis.call()和redis.pcall()两个函数的参数可以是任意的Redis命令:

127.0.0.1:6379> eval "return redis.call('set','foo','bar')" 0
OK

需要注意的是,上面这段脚本的确实现了将键foo的值设为bar的目的,但是,它违反了EVAL命令的语义,因为脚本里使用的所有键都应该由KEYS数组来传递,就像这样:

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

要求使用正确的形式来传递键(key)是有原因的,因为不仅仅是EVAL这个命令,所有的Redis命令,在执行之前都会被分析,借此来确定命令会对哪些键进行操作。

因此,对于EVAL命令来说,必须使用正确的形式来传递键,才能确保分析工作正确地执行。除此之外,使用正确的形式来传递键还有很多其他好处,它的一个特别重要的用途就是确保Redis集群可以将你的请求发送到正确的集群节点。

redis.call()与redis.pcall()很类似,他们唯一的区别是当redis命令执行结果返回错误时,redis.call()将返回给调用者一个错误,而redis.pcall()会将捕获的错误以Lua表的形式返回。

下面的例子演示了redis.call()与redis.pcall()的区别:

127.0.0.1:6379> eval "return redis.call('set1',KEYS[1],'bar')" 1 foo
(error) ERR Error running script (call to f_d968406ee98123006fa91fd2ee764d4f7f859dd7): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script

127.0.0.1:6379> eval "return redis.pcall('set1',KEYS[1],'bar')" 1 foo
(error) @user_script: 1: Unknown Redis command called from Lua script

127.0.0.1:6379> eval "return type(redis.call('set1',KEYS[1],'bar'))" 1 foo
(error) ERR Error running script (call to f_c62b83c8313fd8f2557865e37d2bb5133f1789af): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script

127.0.0.1:6379> eval "return type(redis.pcall('set1',KEYS[1],'bar'))" 1 foo
"table"

Lua数据类型和Redis数据类型之间转换

当Lua通过call()或pcall()函数执行Redis命令的时候,命令的返回值会被转换成Lua数据结构。

同样地,当Lua脚本在Redis内置的解释器里运行时,Lua脚本的返回值也会被转换成Redis协议(protocol),然后由EVAL将值返回给客户端。

数据类型之间的转换遵循这样一个设计原则:如果将一个Redis值转换成Lua值,之后再将转换所得的Lua值转换回Redis值,那么这个转换所得的Redis 值应该和最初时的Redis值一样。

换句话说,Lua类型和Redis类型之间存在着一一对应的转换关系。

LuaRedis示例
number(整型)integer3 → 3
number(浮点型)bulk string3.3 -> “3.3”
stringbulk string“value” → “value”
table (数组形式)multi-bulk{1,2,3} → [“1”,“2”,“3”]
table(键值对形式)multi-bulk{name=“Bob”} → (empty array),无法转换需使用json库序列化为字符串
table(只带一个ok的键值对)status{ok=‘success’} -> success
table(只带一个err的键值对)error{err=‘My Error’} -> (error) My Error
booleanintegertrue → 1, false → (nil)
nilnilnil -> 无返回

Lua中整数和浮点数之间没有什么区别。因此,我们始终将Lua的数字转换成整数的回复,这样将舍去小数部分。如果你想从Lua返回一个浮点数,你应该将它作为一个字符串,比如ZSCORE命令。

以下是几个类型转换的例子:

127.0.0.1:6379> eval "return 10" 0
(integer) 10

127.0.0.1:6379> eval "return {1,2,{3,'Hello World!'}}" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 3
   2) "Hello World!"

127.0.0.1:6379> eval "return redis.call('get','foo')" 0
"bar"

最后一个例子展示如果是Lua直接命令调用它是如何可以从redis.call()或redis.pcall()接收到准确的返回值。

下面的例子我们可以看到浮点数和nil将怎么样处理:

127.0.0.1:6379> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"

正如你看到的3.333被转换成了3,并且nil后面的字符串bar没有被返回回来。

可以使用tostring()函数将数字转字符串:

127.0.0.1:6379> eval "return tostring(3.3333)" 0
"3.3333"

有两个辅助函数从Lua返回Redis的类型:

  • redis.error_reply(error_string):returns an error reply. This function simply returns the single field table with the err field set to the specified string for you.
  • redis.status_reply(status_string):returns a status reply. This function simply returns the single field table with the ok field set to the specified string for you.

使用redis.error_reply()函数与直接返回一个table效果一样:

127.0.0.1:6379> eval "return {err='My Error'}" 0
(error) My Error

127.0.0.1:6379> eval "return redis.error_reply('My Error')" 0
(error) My Error

可用库

Redis Lua解释器可用加载以下Lua库:

  • base lib.
  • table lib.
  • string lib.
  • math lib.
  • debug lib.
  • struct lib.
  • cjson lib.
  • cmsgpack lib.
  • bitop lib.
  • redis.sha1hex function.

每一个Redis实例都拥有以上的所有类库,以确保您使用脚本的环境都是一样的。

struct,CJSON和cmsgpack都是外部库,所有其他库都是标准Lua库。

CJSON库为Lua提供极快的JSON处理:

127.0.0.1:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0
"{\"foo\":\"bar\"}"

127.0.0.1:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}"
"bar"

127.0.0.1:6379> eval "local table = {} table['foo']='bar' table['hello']='world' return cjson.encode(table)" 0
"{\"hello\":\"world\",\"foo\":\"bar\"}"

在java中的使用

lua脚本:

-- 库存扣减脚本
local key = KEYS[1]
local quantity = tonumber(ARGV[1])

local stock = tonumber(redis.call('GET', key))
if not stock then
    return -1
end

if stock >= quantity then
    return redis.call('DECRBY', key, quantity)
else
    return 0
end

在java中使用lua脚本:

package com.morris.redis.demo.lua;

import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Collections;

/**
 * RedisTemplate中lua脚本的使用
 */
@Service
public class RedisTemplateLuaDemo {

    @Resource
    private RedisTemplate redisTemplate;

    public void testLua() {

        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/deduct_stock.lua")));
        redisScript.setResultType(Long.class);

        Object object = redisTemplate.execute(
                redisScript,
                Collections.singletonList("stock:10086"),
                Integer.toString(1)
        );
        System.out.println(object);
    }


}

RedisTemplate底层源码已经实现了evalsha+eval,无需手动加载脚本,源码如下:

org.springframework.data.redis.core.script.DefaultScriptExecutor#eval

	protected <T> T eval(RedisConnection connection, RedisScript<T> script, ReturnType returnType, int numKeys,
			byte[][] keysAndArgs, RedisSerializer<T> resultSerializer) {

		Object result;
		try {
			result = connection.evalSha(script.getSha1(), returnType, numKeys, keysAndArgs);
		} catch (Exception e) {

			if (!ScriptUtils.exceptionContainsNoScriptError(e)) {
				throw e instanceof RuntimeException ? (RuntimeException) e : new RedisSystemException(e.getMessage(), e);
			}

			result = connection.eval(scriptBytes(script), returnType, numKeys, keysAndArgs);
		}

		if (script.getResultType() == null) {
			return null;
		}

		return deserializeResult(resultSerializer, result);
	}

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

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

相关文章

OpenHarmony-XTS测试

OpenHarmony-XTS测试 OpenHarmony-XTS测试环境搭建测试准备开始运行PS OpenHarmony-XTS测试 针对OpenHarmony版本进行XTS测试使用记录。 windows环境。 以acts套件为例。 环境搭建 获取测试套件&#xff0c;两种方法 1&#xff09;官网下载&#xff1a;https://www.openharm…

【物联网-WIFI】

物联网-WIFI ■ ESP32-C3-模块简介■ ESP32-C3-■ ESP32-C3-■ WIFI-模组■ WIFI-■ WIFI- ■ ESP32-C3-模块简介 ■ ESP32-C3- ■ ESP32-C3- ■ WIFI-模组 ■ WIFI- ■ WIFI-

linux常用基本指令汇总

文章目录 01. ls指令02. pwd指令03. cd指令04. touch指令05. mkdir指令06. rmdir指令07. rm指令08. man指令09. cp指令10. mv指令11. cat指令11. more指令12. less指令13. head指令14. tail指令15. time指令16. cal指令17. find指令18. grep指令19. zip/unzip指令20.tar指令21.…

Docker Desktop 安装与使用详解

目录 1. 前言2. Docker Desktop 安装2.1 下载及安装2.2 登录 Docker 账号2.3 进入 Docker Desktop 主界面 3. Docker 版本查看与环境检查3.1 查看 Docker Desktop 支持的 Docker 和 Kubernetes 版本3.2 检查 Docker 版本 4. Docker Hub 和常用镜像管理方式4.1 使用 Docker Hub4…

【HarmonyOS Next】鸿蒙应用常规面试题和答辩思路参考

【HarmonyOS Next】鸿蒙应用常规面试题和答辩思路参考 一、充分了解岗位JD要求 根据招聘发布的岗位JD&#xff0c;进行自我匹配分析。了解基本要求和加分项&#xff0c;以及项目节奏和英文要求等。 技术不匹配的点&#xff0c;是否会影响应聘岗位加分项自己是否掌握&#xf…

《计算机图形学》第二课笔记-----二维变换的推导

前言&#xff1a;为什么这么突兀的把这一节内容放在了第二课&#xff0c;第一是因为我急于求成&#xff0c;第二是因为这一章节太重要了&#xff0c;这几乎是二维三维变换的最核心的东西&#xff0c;理解了这一章节内容&#xff0c;后面的就会像打通了任督二脉一样&#xff0c;…

机器学习(七)

一&#xff0c;监督学习和无监督学习聚类的数据集比较&#xff1a; 监督学习&#xff1a; 数据集包括输入的数据和与之对应的标签 无监督学习&#xff1a; 数据集仅含有输入的数据&#xff0c;要求算法自己通过所给的数据集来确定决策边界 二&#xff0c;聚类(Clustering): 聚…

利用labelimg实现yolov8数据集的制作

我们在使用yolov8进行物体检测识别的时候&#xff0c;由于其内置的n,s,m等模型只包含90多种物体&#xff08;很多其他物品并未包含在其中&#xff09;&#xff0c;导致我们无法直接使用其模型进行视频或者图片的检测识别。这个时候&#xff0c;我们就需要自己制作数据集进行训练…

【0x80070666】-已安装另一个版本...(Tableau 安装失败)

第一种是之前安装过tableau相关软件&#xff0c;但是没卸载干净。 方法1&#xff1a;卸载旧版本 打开 控制面板 → 程序和功能&#xff08;或 添加/删除程序&#xff09;。查找 Tableau Desktop&#xff0c;如果已安装旧版本&#xff0c;卸载它。重新启动电脑后再尝试安装。 …

Word填写窗口功能详解:如何让文档填写更高效?

在日常办公中&#xff0c;我们经常需要让他人填写一些固定格式的文档&#xff0c;比如合同、申请表、调查问卷等。如果直接使用普通文本编辑&#xff0c;填写时可能会破坏排版&#xff0c;甚至修改了不该改动的内容。这时候&#xff0c;Word的填写窗口&#xff08;即“内容控件…

Oracle数据库存储结构--逻辑存储结构

数据库存储结构&#xff1a;分为物理存储结构和逻辑存储结构。 物理存储结构&#xff1a;操作系统层面如何组织和管理数据 逻辑存储结构&#xff1a;Oracle数据库内部数据组织和管理数据&#xff0c;数据库管理系统层面如何组织和管理数据 Oracle逻辑存储结构 数据库的逻…

简单创建一个Django项目并配置neo4j数据库

创建项目&#xff0c;项目的文件夹就是项目的名称 创建项目的基本框架 安装djangorestframework 单击运行 查看浏览器运行效果&#xff1a; 运行效果如下&#xff1a; 创建应用(假如说是创建一个名为myapp的应用)&#xff1a; python manage.py startapp myapp创建之后的…

java实现智能家居控制系统——入门版

文章目录 一、需求二、业务分析三、具体实现创建一个功能接口&#xff0c;实现设备的开关创建一个家电类&#xff0c;作为功能接口的实现类&#xff0c;定义名字和状态分别创建电视机、洗衣机、电灯的类&#xff0c;继承家电类Tv类WashMachine类Lamp类 定义智能控制系统类&…

VSCode C/C++ 开发环境完整配置及常见问题(自用)

这里主要记录了一些与配置相关的内容。由于网上教程众多&#xff0c;部分解决方法并不能完全契合我遇到的问题&#xff0c;因此我选择以自己偏好的方式&#xff0c;对 VSCode 进行完整的配置&#xff0c;并记录在使用过程中遇到的问题及解决方案。后续内容也会持续更新和完善。…

实用小工具——快速获取数据库时间写法

最近我遇到了一个比较棘手的问题&#xff1a;在工作中&#xff0c;各个项目所使用的数据库类型各不相同。这导致我习惯性地使用Oracle的SQL语句进行编写&#xff0c;但每次完成后都会遇到报错&#xff0c;最终才意识到项目的数据库并非Oracle。为了避免这种情况&#xff0c;我需…

基于ssm的宠物医院信息管理系统(全套)

一、系统架构 前端&#xff1a;html | layui | vue | element-ui 后端&#xff1a;spring | springmvc | mybatis 环境&#xff1a;jdk1.8 | mysql | maven | tomcat | idea | nodejs 二、代码及数据库 三、功能介绍 01. web端-首页1 02. web端-首页…

从 YOLOv1 到 YOLOv2:目标检测的进化之路

引言 你有没有想过&#xff0c;当你用手机拍一张照片&#xff0c;里面的人、车、狗是怎么被自动识别出来的&#xff1f;这背后靠的就是目标检测技术。目标检测是计算机视觉中的一个重要领域&#xff0c;它不仅要回答“图片里有什么”&#xff0c;还要告诉你“这些东西在哪里”…

RTDETR融合[CVPR205]ARConv中的自适应矩阵卷积

RT-DETR使用教程&#xff1a; RT-DETR使用教程 RT-DETR改进汇总贴&#xff1a;RT-DETR更新汇总贴 《Adaptive Rectangular Convolution for Remote Sensing Pansharpening》 一、 模块介绍 论文链接&#xff1a;https://arxiv.org/pdf/2503.00467 代码链接&#xff1a;https:/…

项目-个人博客测试报告

目录 一、项目背景 二、项目功能 三、测试计划 &#xff08;1&#xff09;功能测试 &#xff08;2&#xff09;自动化测试 &#xff08;3&#xff09;性能测试 一、项目背景 1、个人博客系统是一个操作简单的基于Spring前后端分离的项目&#xff0c;同时使用MySQL数据库来进…

软考计算机知识-流水线

计算机流水线类似工业生产过程的流水线&#xff0c;在同一时间&#xff0c;m个部件进行不同的操作&#xff0c;完成对不同对象的处理。 理解重叠&#xff1a;让不同的指令在时间上重叠地解释。在解释第k条指令的操作完成之前&#xff0c;就可以开始解释第k1条指令。 题1&#…