当前位置: 首页 > article >正文

【redis】lua脚本

在分布式系统与高并发架构的战场中,开发者们始终在与两个永恒的命题博弈:数据一致性系统性能。当我们试图用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) 1127.0.0.1:6379> script flush
OK127.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 script127.0.0.1:6379> eval "return redis.pcall('set1',KEYS[1],'bar')" 1 foo
(error) @user_script: 1: Unknown Redis command called from Lua script127.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 script127.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) 10127.0.0.1:6379> eval "return {1,2,{3,'Hello World!'}}" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 32) "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 Error127.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 thenreturn -1
endif stock >= quantity thenreturn redis.call('DECRBY', key, quantity)
elsereturn 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 {@Resourceprivate 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);}

相关文章:

【redis】lua脚本

在分布式系统与高并发架构的战场中&#xff0c;开发者们始终在与两个永恒的命题博弈&#xff1a;数据一致性与系统性能。当我们试图用Redis构建高速缓存、实现分布式锁或设计秒杀系统时&#xff0c;往往会陷入这样的困境——如何在保证原子性的同时&#xff0c;避免网络往返带来…...

Oracle中的INHERIT PRIVILEGES权限

Oracle中的INHERIT PRIVILEGES权限 存储过程和用户函数的AUTHID属性调用者权限vs定义者权限一个简单的示例INHERIT PRIVILEGES权限的含义INHERIT PRIVILEGES权限的安全隐患注意到Oracle 19c数据库中有如下权限信息: SQL> select grantor,grantee,table_name,privilege fro…...

Kafka相关的面试题

以下是150道Kafka相关的面试题及简洁回答&#xff1a; Kafka基础概念 1. 什么是Kafka&#xff1f; Kafka是一个分布式、可扩展、容错的发布-订阅消息系统&#xff0c;最初由LinkedIn开发&#xff0c;现为Apache项目。它适用于高吞吐量的场景&#xff0c;如大数据处理和实时数据…...

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-...

作业9 (2023-05-05 数组的定义和初始化)

第1题/共11题【单选题】 关于一维数组初始化,下面哪个定义是错误的?( ) A.int arr[10] = {1,2,3,4,5,6}; B.int arr[] = {1,2,3,4,5,6}; C.int arr[] = (1,2,3,4,5,6); D.int arr[10] = {0}; A:正确,10个int的一段连续空间,前6个位置被初始化为1,2,3,4,5,6,其他…...

C语言中的流程控制语句

一.流程控制语句的分类&#xff1a; 1.顺序结构 概念&#xff1a;从上往下依次执行&#xff0c;也是程序默认的执行顺序 2.分支结构 概念&#xff1a;程序在执行的过程中出现了岔路&#xff08;我们只能选择一条支线进行执行&#xff09; &#xff08;1&#xff09;.if语句…...

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.…...

Python 与 JavaScript 交互及 Web 逆向分析全解析

一、引言 在当今数字化时代,软件开发的复杂性和多样性不断增加,不同编程语言之间的交互与协作变得愈发重要。Python 凭借其简洁易读的语法、丰富的库和强大的数据处理能力,在数据科学、自动化脚本编写等领域占据着重要地位。而 JavaScript 作为前端开发的核心语言,以其在网…...

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…...

鬼泣:移动系统3

能帮到你的话&#xff0c;就给个赞吧 &#x1f618; 文章目录 run/Sprint混合空间输入&#xff1a;(leanAngle, maxWalkSpeed) 按布尔混合和选择的区别&#xff1a;自动生成过渡动画dead blending&#xff1a;惯性化节点疑问run/Sprint混合空间为什么速度为0时也设置奔跑动画为…...

解决 HTTP 请求中的编码问题:从乱码到正确传输

文章目录 解决 HTTP 请求中的编码问题&#xff1a;从乱码到正确传输1. **问题背景**2. **乱码问题的原因**2.1 **客户端编码问题**2.2 **请求头缺失**2.3 **服务器编码问题** 3. **解决方案**3.1 **明确指定请求体编码**3.2 **确保请求头正确**3.3 **动态获取响应编码** 4. **调…...

golang从入门到做牛马:第二十篇-Go语言接口:行为的“契约”

Go语言接口:行为的“契约” 在Go语言中,接口(interface)是一种非常强大的类型,用于定义行为的集合。接口通过描述类型必须实现的方法,规定了类型的行为契约。Go语言的接口设计简单而功能强大,是实现多态和解耦的重要工具。接下来,让我们一起深入了解Go语言中的接口。 …...

HTML5 Web SQL

HTML5 Web SQL 引言 随着互联网技术的飞速发展,HTML5 作为新一代的网页技术,已经逐渐成为网页开发的主流。在 HTML5 中,Web SQL 是一种轻量级的数据库存储技术,它允许开发者直接在网页中存储数据。本文将详细介绍 HTML5 Web SQL 的概念、特点、应用场景以及使用方法。 一…...

【品铂科技工业生产应用案例解析】

品铂科技&#xff08;Pinpoint&#xff09;在工业领域的高精度定位解决方案已广泛应用于电力、钢铁、仓储、化工、地铁等场景&#xff0c;以下为典型应用案例及技术方案&#xff1a; 一、‌电力行业&#xff1a;上海闵行电厂人员定位‌ 白鹤滩水力发电站 ‌项目需求‌&#x…...

pjsip dtmf发送和接收(pjsua)

DTMF(双音多频,Dual-Tone Multi-Frequency)是一种用于电话系统的信号技术,通过组合两个不同频率的音频信号来表示数字和符号。以下是DTMF的主要使用背景和应用场景: 电话拨号 DTMF最常见的用途是电话拨号。当用户按下电话键盘上的数字或符号时,电话会生成两个特定频率的音…...

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

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

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

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

无需微调的对齐方法URIAL

无需微调的对齐方法URIAL 研究背景与目的:LLMs的对齐调优通常采用监督微调(SFT)和强化学习从人类反馈(RLHF),但LIMA研究表明少量示例的SFT也能实现较好对齐,暗示对齐调优可能存在“表面性质”。本研究旨在探究对齐调优对基础LLMs的具体影响,并提出不依赖SFT或RLHF的对齐…...

机器学习(七)

一&#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;卸载它。重新启动电脑后再尝试安装。 …...

在rv1106上部署vue3

创建vue3项目 $ npm create vuelatest Need to install the following packages: create-vue3.15.1 Ok to proceed? (y) y > npx > create-vue┌ Vue.js - The Progressive JavaScript Framework │ ◇ 请输入项目名称&#xff1a; │ ip_cam │ ◇ 请选择要包含的…...

海量数据查询加速:Presto、Trino、Apache Arrow

1. 引言 在大数据分析场景下,查询速度往往是影响业务决策效率的关键因素。随着数据量的增长,传统的行存储数据库难以满足低延迟的查询需求,因此,基于列式存储、向量化计算等技术的查询引擎应运而生。本篇文章将深入探讨 Presto、Trino、Apache Arrow 三种主流的查询优化工…...

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

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

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

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

kali之nmap

kali之nmap Nmap&#xff08;Network Mapper&#xff09;是 Kali Linux 中最著名的网络扫描工具之一&#xff0c;广泛用于网络发现、端口扫描、服务识别、操作系统检测等任务。它是一个功能强大且灵活的开源工具&#xff0c;适用于渗透测试、网络管理和安全审计。 1. Nmap 的主…...

Kubernetes学习笔记-移除Nacos迁移至K8s

项目服务的配置管理和服务注册发现由原先的Nacos全面迁移到Kubernetes上。 一、移除Nacos 移除Nacos组件依赖。 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <…...

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

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

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

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