Redis:Redis脚本

文章目录

Redis 脚本

Redis 提供了一个编程接口,允许你在服务器本身执行自定义脚本。在 Redis 7 及更高版本中,你可以使用 Redis 函数来管理和运行脚本。在 Redis 6.2 及更低版本中,你使用带有 EVAL 命令的 Lua 脚本进行编程。

  • 核心价值
    • 数据局部性:代码在数据所在的地方运行,无需通过网络将大量数据搬运到客户端处理后再传回。
    • 原子性:脚本执行期间,服务器被阻塞,类似于事务,保证操作的完整性。
    • 减少网络 RTT:一次脚本调用可以完成数十次 Redis 命令的操作。
    • 封装业务逻辑:构建特定于应用程序的 API。

运行脚本的两种方式

Redis 提供了两种运行脚本的方式,这在架构设计上有着本质的区别。

  • Eval 脚本 (EVAL - Redis 2.6.0+)

    • 模式:即发即用。客户端直接发送 Lua 源代码给 Redis 执行。
    • 特点
      • 代码跟随应用:脚本源代码存储在应用程序代码库中,Redis 只是做临时缓存。
      • 缺点:随着应用规模扩大,代码维护变得困难。每次执行可能需要重新传输代码,或者依赖不稳定的缓存机制。
  • Redis 函数 (Redis Functions - Redis 7.0+)

    • 模式:存储过程式。函数作为数据库的一等公民存在。
    • 特点
      • 代码跟随数据库:函数通过管理员操作(如加载模块一样)加载到服务器,所有客户端只需通过函数名调用。
      • 优势:实现了脚本与应用逻辑的解耦。函数可以被独立地部署、测试和版本管理。

Functions模式 函数在服务端
上传函数库
调用函数名 f1
调用函数名 f1
已加载
管理员部署
Redis
App A
App B
函数库
优势: 逻辑集中管理, 易维护
EVAL模式 脚本在应用端
发送源码
发送源码
发送源码
缓存
App A
Redis
App B
App C
临时缓存
缺点: 每个应用都需要维护代码

原子性与性能陷阱

Redis 保证脚本执行的原子性。在脚本运行期间,阻塞 所有服务器活动。这意味着脚本的所有效果要么全部发生,要么都不发生。这种语义与 MULTI/EXEC 事务相同。

  • 警告 :这种阻塞模式是针对所有连接的客户端的。如果你写了一个慢脚本,整个 Redis 实例将停摆,所有其他业务请求都会超时或报错。
  • 原则:脚本运行通常在微秒级完成,但应避免在脚本中执行耗时算法。

只读脚本

Redis 7.0 引入了只读脚本的严格定义和优化。

  • 定义:只执行不修改数据的命令。
  • 触发方式 :使用 no-writes 标志,或使用 _RO 后缀的命令(如 EVAL_RO, FCALL_RO)。
  • 高级特性
    1. 可在副本上执行:非常适合利用副本进行读扩展。
    2. 可安全杀死 :当脚本超时时,可以使用 SCRIPT KILL 强制终止。
    3. 不受 OOM 阻塞:当 Redis 内存超限时,只读脚本不会失败(因为它们不分配数据)。
    4. 故障切换时可用:在主从切换导致写暂停期间,只读脚本仍可执行。
    5. 权限控制:可以配置 ACL 用户仅允许执行只读脚本。

注意:PUBLISH, SPUBLISH, PFCOUNT 在脚本中被视为写命令,因为它们涉及数据传播。

沙箱与执行时间限制

脚本在沙箱中运行。

  • 限制:无法访问文件系统、网络或其他系统调用。
  • 目的:防止恶意代码破坏服务器或窃取数据。
  • 原则:脚本只能操作 Redis 内部的数据。

默认超时时间为 5 秒 (通过 busy-reply-threshold 配置)。当脚本超时后,Redis 的处理流程非常特殊:

  1. Redis 记录日志,警告脚本运行时间过长。
  2. Redis 开始接受其他客户端的命令,但回复 BUSY 错误(除了特定的管理命令)。

如何终止超时脚本?

  • 情况 1:脚本只读,尚未写入数据
    • 操作SCRIPT KILLFUNCTION KILL
    • 结果:脚本被立即终止,服务恢复正常。
  • 情况 2:脚本已执行了写操作
    • 操作SCRIPT KILL 失效
    • 原因:为了保证原子性,不能让脚本停在半路,否则数据状态会被破坏。
    • 唯一解法SHUTDOWN NOSAVE
    • 结果 :直接终止服务器进程,不保存当前数据集。这是为了数据一致性而牺牲可用性的最后手段。



其他客户端请求
尝试杀死
否 (只读)
是 (已写)
必须执行
脚本开始执行
运行时间 > 5秒?
继续执行
进入 BUSY 状态
返回 BUSY 错误
脚本是否修改了数据?
SCRIPT KILL 成功

服务恢复
SCRIPT KILL 失败

违反原子性
SHUTDOWN NOSAVE

服务器重启,数据丢失

Lua脚本

Redis 允许用户在服务器端上传并执行 Lua 脚本。脚本可以使用程序化控制结构并调用大多数 Redis 命令来访问数据库。由于脚本在服务器内部执行,因此读写数据的效率非常高。

  • 原子性保证 :Redis 保证脚本的原子执行。在脚本运行期间,整个服务器的所有活动都会被阻塞。这意味着脚本对数据产生的所有修改要么全部发生,要么都不发生。
  • 核心优势
    • 数据局部性:直接在数据所在的位置执行逻辑,减少网络延迟。
    • 复杂逻辑封装:可以在服务器端实现简单的条件更新、跨键操作或组合多种数据类型的原子操作。

Redis 使用嵌入式的 Lua 5.1 解释器作为执行引擎。

基础用法:EVAL 命令

使用 EVAL 命令可以执行脚本。其基本语法如下:

text 复制代码
EVAL script num_keys [key1 ... keyN] [arg1 ... argN]
  • script: Lua 脚本的源代码字符串。
  • num_keys : 关键参数 。指定后面紧跟的参数中有多少个是键名,剩下的则是普通参数。
  • [key1 ... keyN]: 传递给脚本的键名。
  • [arg1 ... argN]: 传递给脚本的普通参数。

参数传递机制:KEYS vs ARGV

在 Lua 脚本内部,可以通过两个全局变量访问参数:

  • KEYS : 数组,包含了所有在 num_keys 中指定的键名。
  • ARGV: 数组,包含了所有剩余的普通参数。
bash 复制代码
# 注意这里的 1,表示有 1 个 key (user:1000),后面是参数
> EVAL "return { KEYS[1], ARGV[1], ARGV[2] }" 1 user:1000 name "Alice"
1) "user:1000"
2) "name"
3) "Alice"

深度解析:为什么必须区分 Keys 和 Args?(集群模式)

在 Redis 集群模式下,数据是分片存储的。客户端发送命令时,必须明确知道哪些参数是 Key,以便根据 Key 计算出哈希槽,从而路由到正确的节点。 如果你在 Lua 脚本里动态拼接 Key 名,或者 num_keys 填写错误,会导致以下问题:

  1. 路由错误:Redis 无法确定脚本在哪里执行,可能发送到错误的节点。
  2. CROSSSLOT 错误:如果脚本试图访问不同节点上的 Key,Redis 会直接拒绝执行。

参数解析
计算哈希槽
客户端执行 EVAL
解析 num_keys=1
Keys: user:1000
Args: name Alice
Redis 集群节点 A
执行 Lua 脚本

KEYS[1]=user:1000

ARGV[1]=name...

脚本与 Redis 的交互

在 Lua 脚本中,主要通过两个函数调用 Redis 命令:

  1. redis.call() :
    • 遇 Redis 命令时,如果发生错误(例如对 String 类型执行 LPUSH),错误会直接返回给客户端,并中断脚本执行。
  2. redis.pcall() :
    • 命令发生错误时,错误会被捕获并返回给 Lua 环境作为返回值,脚本不会中断,可以继续执行后续代码(类似于 try-catch)。

脚本缓存与优化

每次都传输完整的 Lua 源码会消耗网络带宽和 CPU。为此,Redis 提供了脚本缓存机制。 当 Redis 执行一个脚本时,它会计算脚本内容的 SHA1 摘要,并将其缓存在服务器内存中。

  • SCRIPT LOAD script: 将脚本加载到缓存,返回 SHA1 摘要。脚本不会立即执行。
  • EVALSHA sha1 num_keys ...: 使用 SHA1 摘要执行缓存中的脚本。

缓存执行 EVALSHA

  1. 加载 SCRIPT LOAD 2. 返回 SHA1 3. 后续只发 SHA1 客户端
    Redis
    直接执行 EVAL
    每次发送完整脚本
    客户端
    Redis

注意 :脚本缓存是易失 的。服务器重启、故障切换或手动执行 SCRIPT FLUSH 都会清空缓存。因此,智能的客户端库通常会在遇到 NOSCRIPT 错误时,自动回退到 EVAL 或重新加载脚本。

脚本复制模式

由于脚本可能修改数据,Redis 必须将这些修改同步到副本(Replica)。在 Redis 的发展过程中,有两种复制模式:

逐字复制(Verbatim Replication - 旧模式)

这是 Redis 5.0 之前的默认模式。

  • 机制 :主节点将完整的 Lua 脚本代码发送给副本,副本重新执行该脚本。
  • 要求 :脚本必须是确定性 的。因为副本会再次运行脚本,所以不能使用 TIMERANDOMKEYSRANDMEMBER 或 Lua 的 math.random(),否则主从数据会不一致。
  • 缺点:浪费 CPU(副本也要计算一遍)。
效果复制(Effects Replication - 现代默认模式)

从 Redis 5.0 开始,并在 7.0 强制启用。

  • 机制 :主节点执行完脚本后,只将脚本产生的实际 Redis 命令 (如 SET, INCR)打包成事务发送给副本。副本不需要执行 Lua 代码,只执行这些命令。
  • 优势
    • 允许非确定性 :可以使用 TIME 等命令,因为主从只执行产生的 SET 命令。
    • 节省 CPU:副本只需执行简单的 Redis 命令。

Redis Replica Redis Master Client Redis Replica Redis Master Client 效果复制模式 (Redis 5.0+) 主节点执行 Lua 只打包产生的写命令 副本只执行 Redis 命令 无需重新计算 Lua EVAL script (含 TIME 命令) TIME ->> 获取当前时间 SET key value MULTI ... SET key value ... EXEC EXEC

EVAL 标志位 (Redis 7.0+)

默认情况下,Redis 认为脚本既读又写。从 Redis 7.0 开始,可以通过在脚本第一行添加 Shebang 注释来声明标志位,从而优化脚本行为。

lua 复制代码
#!lua flags=no-writes,allow-stale
-- Lua 脚本内容

常用标志:

  • no-writes: 声明脚本不修改数据(只读)。这允许脚本在副本上执行,或在内存溢出时继续运行。
  • allow-stale: 允许脚本在从节点数据过期(Replication Offset 太大)时执行。

其他 SCRIPT 命令

  • SCRIPT FLUSH: 清空整个脚本缓存。仅用于多租户场景或测试。
  • SCRIPT EXISTS sha1 [sha2 ...]: 检查给定的 SHA1 摘要是否存在于缓存中。
  • SCRIPT KILL: 强制终止一个运行时间过长的慢脚本。注意:仅适用于尚未执行写命令的脚本,以免破坏原子性。
  • SCRIPT DEBUG: 控制 Redis Lua 调试器。

Redis Functions

Redis Functions 是 Redis 7 引入的一套全新 API,用于在服务器端管理和执行自定义代码。它是对旧版 EVAL 脚本机制的全面升级,旨在解决代码管理、持久化和部署维护的痛点。Redis Functions 基于底层 Lua 引擎(未来可能支持其他语言),但通过"库"的概念进行了封装。

  • 函数:一段具体的执行逻辑,有唯一的名字。
  • :包含一个或多个函数的容器,不可变。更新库时必须整体替换。

开发实战:从编写到调用

  • 编写 :Redis Functions 的代码必须以 Shebang 行开头,声明引擎(如 Lua)和库名。
lua 复制代码
#!lua name=mylib

local function my_hset(keys, args)
  -- keys[1] 是我们传入的第一个 Key (即 Hash 的名字)
  local hash = keys[1]
  
  -- 获取当前服务器时间
  local time = redis.call('TIME')[1]
  
  -- 执行 HSET: 插入时间戳,然后插入用户传入的字段
  -- unpack(args) 将数组展开成独立参数
  return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
end

-- 将函数注册到库中
redis.register_function('my_hset', my_hset)
  • 加载:
bash 复制代码
# -x 表示从标准输入读取文件内容,REPLACE 表示覆盖同名旧库
redis-cli -x FUNCTION LOAD REPLACE < mylib.lua
  • 调用
bash 复制代码
# 1 表示有 1 个 Key (user:1000),剩下的 "name" "Alice" 是普通参数
> FCALL my_hset 1 user:1000 name "Alice"
(integer) 2

> HGETALL user:1000
1) "_last_modified_"
2) "1678123456"  <-- 自动添加的时间戳
3) "name"
4) "Alice"

集群环境下的部署

Redis Cluster 不会自动将函数同步到新节点。

  • 操作 :需要管理员手动使用 redis-cli 工具将函数加载到所有主节点。

    bash 复制代码
    redis-cli --cluster-only-masters --cluster call host:port FUNCTION LOAD ...
  • 新节点 :使用 --cluster add-node 时,会自动从现有节点复制函数库。

临时实例与冷启动

对于作为纯缓存使用的 Redis 实例(重启不加载 RDB/AOF),如果需要启动时就加载好函数,可以使用:

bash 复制代码
redis-cli --functions-rdb

这个命令会导出一个只包含函数的 RDB 文件,供 Redis 启动时预加载。

Redis Lua API

详情请看官方文档

相关推荐
想唱rap2 小时前
表的约束条件
linux·数据库·mysql·ubuntu·bash
超级种码2 小时前
Redis:Redis 命令详解
数据库·redis·bootstrap
qq_401700412 小时前
Qt 事件处理机制
java·数据库·qt
Elastic 中国社区官方博客2 小时前
使用 jina-embeddings-v3 和 Elasticsearch 进行多语言搜索
大数据·数据库·人工智能·elasticsearch·搜索引擎·全文检索·jina
深海小黄鱼3 小时前
mysql 导入csv文件太慢, Error Code: 1290.
数据库·mysql
小宇的天下3 小时前
Calibre Connectivity Extraction(21-1)
数据库·oracle
rannn_1113 小时前
【Java项目】中北大学Java+数据库课设|校园食堂智能推荐与反馈系统
java·数据库·后端·课程设计·中北大学
DBA小马哥3 小时前
从Oracle到信创数据库:一场技术迁移的探索之旅
数据库·oracle
2501_944521003 小时前
rn_for_openharmony商城项目app实战-语言设置实现
javascript·数据库·react native·react.js·harmonyos