doc: 阅读redis scripting with lua文档

This commit is contained in:
asahi
2025-09-24 15:03:01 +08:00
parent 4e3a3388d6
commit 4a0ee84e7e

View File

@@ -162,6 +162,15 @@
- [Read-only scripts](#read-only-scripts) - [Read-only scripts](#read-only-scripts)
- [Sandboxed script context](#sandboxed-script-context) - [Sandboxed script context](#sandboxed-script-context)
- [Maximum execution time](#maximum-execution-time) - [Maximum execution time](#maximum-execution-time)
- [Scripting with Lua](#scripting-with-lua)
- [Getting Started](#getting-started)
- [Script Parameterization](#script-parameterization)
- [Interact with redis from a script](#interact-with-redis-from-a-script)
- [Script Cache](#script-cache)
- [Cache volatility](#cache-volatility)
- [evalsha in context of pipelining](#evalsha-in-context-of-pipelining)
- [Script Cache Semantics](#script-cache-semantics)
- [Script Command](#script-command)
# redis # redis
@@ -2665,4 +2674,136 @@ script仅应该操作`redis中存储的数据`和`脚本执行时传入的作为
- redis会在日志中添加`脚本执行时间过长` - redis会在日志中添加`脚本执行时间过长`
- redis开始从其他clients接收commands但是会对所有发送normal commands的clients返回`BUSY error`。在该场景下,唯一被允许的命令为`SCRIPT_KILL, FUNCTION_KILL, SHUTDOWN NOSAVE` - redis开始从其他clients接收commands但是会对所有发送normal commands的clients返回`BUSY error`。在该场景下,唯一被允许的命令为`SCRIPT_KILL, FUNCTION_KILL, SHUTDOWN NOSAVE`
- 对于`read-only script`,可以使用`SCRIPT_KILL`和`FUNCTION_KILL`命令,因为该脚本未对数据造成任何修改 - 对于`read-only script`,可以使用`SCRIPT_KILL`和`FUNCTION_KILL`命令,因为该脚本未对数据造成任何修改
- 如果`script`在执行过程中哪怕执行了一个write operation那么只能使用`SHUTDOWN NOSAVE`命令其会停止server并且不会将当前的data set保存到磁盘中 - 如果`script`在执行过程中哪怕执行了一个write operation那么只能使用`SHUTDOWN NOSAVE`命令其会停止server并且不会将当前的data set保存到磁盘中
### Scripting with Lua
redis允许用户上传lua脚本到server并在server端执行故而在脚本执行时对数据的读取和写入操作十分高效并不需要网络开销。
redis保证脚本的执行是原子的。在执行脚本时server的其他活动都会被阻塞。
lua允许在脚本中集成部分属于应用的逻辑例如跨多个keys的条件更新等。
#### Getting Started
可以通过`EVAL`命令在redis中执行script示例如下
```redis-cli
> EVAL "return 'Hello, scripting!'" 0
"Hello, scripting!"
```
在上述示例中,`EVAL`命令接收两个参数:
- 第一个参数为lua script的source code内容该脚本内容将在redis engine's context下运行
- 第二个参数为`the number of arguments that follow the script's body`
#### Script Parameterization
虽然不建议这样做但是仍可以在应用程序中动态的生成script source code示例如下
```redis-cli
redis> EVAL "return 'Hello'" 0
"Hello"
redis> EVAL "return 'Scripting!'" 0
"Scripting!"
```
虽然`在应用程序中动态生成script source code`的操作并未被redis禁止但是其是不推荐的使用方式可能会在`script cache`方面造成问题。在该场景下不应该生成多个拥有微小差异的不同脚本而是应该将细微的差异提取为script param。
如下实例则展示了参数化后的标准实现方式:
```redis-cli
redis> EVAL "return ARGV[1]" 0 Hello
"Hello"
redis> EVAL "return ARGV[1]" 0 Parameterization!
"Parameterization!"
```
对于lua script执行的参数其分为如下两种
- input arguments that are names of keys
- input arguments that are not names of keys
> 为了确保lua script的正确执行不管是在standalone还是在clustered的部署模式下script访问的所有key names都必须`显式的作为input key arguments被指定`。
>
> script只应该访问那些在`input key argments`中被显式指定的key name。在编写script时应该遵守该要求。`在redis集群环境下应通过redis可能操作的键从而对脚本进行路由。`
>
> 如果用户编写的脚本使用了未在`input key arguments`中指定的keyredis script engine虽然不会在执行时对此进行校验但是可能导致script路由到错误的节点错误的节点执行脚本时仍可能报错。
在调用redis脚本时作为`非key name`被传递给redis脚本的参数即是`regular input argument`。
在上述示例中,`Hello`和`Parameterization!`都是作为常规参数被传递的上述示例中脚本并未用到任何redis key故而第二个参数被指定为`0`,代表`input key arguments`的个数为0。
在redis script context中可以通过`KEYS`和`ARGV`这两个global runtime variables来访问传递的参数。
- `KEYS`针对的是input key arguments
- `ARGV`针对regular argument
如下就示例展示了input arguments在`KEYS`和`ARGV`之间分配的情况:
```redis-cli
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"
```
#### Interact with redis from a script
在lua script中可以通过`redis.call`或`redis.pcall`来调用redis commands。
`call`和`pcall`大致是相同的都会执行redis command但是在针对runtime error的处理上这两个function之间有所不同
- `redis.call`:对于`redis.call`方法抛出的error会被直接返回给客户端
- `redis.pcall` 对于`redis.pcall`方法抛出的异常其会被返回给script's execution context`for possible handling`
lua script中和redis的交互示例如下所示
```redis-cli
> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OK
```
#### Script Cache
当调用`EVAL`时在请求中也会包含source code内容如果对相同的script content进行重复调用不仅会浪费网络带宽redis也会产生额外开销。为了节省网络带宽和计算资源redis对script提供了缓存机制。
在redis中每个通过`EVAL`执行的script都会被存储在由server维护的专用缓存中。缓存的内容按照script的`SHA1 digest sum`来进行组织SHA1 digest sum在cache中能够唯一标识脚本。
就像上述中提到的在应用中动态生成脚本内容是不推荐的。如果不对脚本进行参数化那么动态生成脚本的source code并多次对脚本进行调用将会占用redis额外的内存资源来对脚本内容进行缓存。脚本source code应该尽量通用。
脚本可以通过`SCRIPT LOAD`命令加载到cache中server并不会实际对脚本进行执行而是只会对其编译并加载到cache中。一旦完成加载后可以通过脚本的SHA1 digest sum来执行脚本。
`SCRIPT LOAD`的使用示例如下所示:
```redis-cli
redis> SCRIPT LOAD "return 'Immabe a cached script'"
"c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f"
redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
"Immabe a cached script"
```
在实际使用`eval`时,`org.springframework.data.redis.core.script.DefaultScriptExecutor#execute`方法中已经实现了`evalSha first, and eval it if script not exists`的逻辑故而无需手动eval。
##### Cache volatility
redis script cache是易失的。script cache并不会被看作是database的一部分且其不会被持久化。缓存的脚本随时都由可能丢失。
在如下场景下script cache会被清空
- when server restarts
- during fail-over
- 显式调用`SCRIPT FLUSH`时
在应用使用scripts时应该一直使用`EVALSHA`来执行脚本当sha1 digest不存在时redis cache会返回error例如
```redis-cli
redis> EVALSHA ffffffffffffffffffffffffffffffffffffffff 0
(error) NOSCRIPT No matching script
```
在cache中不存在脚本的场景下应用需要通过`SCRIPT LOAD`先导入脚本内容,然后调用`EVALSHA`命令来根据sha1 digest运行cache中的脚本。对于大多数的api都提供了自动执行该过程的API。
##### evalsha in context of pipelining
在pipeline场景下执行`EVALSHA`命令时pipeline中的commands是按顺序执行的但是执行的间隙中可能穿插来自其他clients的commands`
在pipeline执行时可能返回`NOSCRIPT error`,但是该错误无法被处理。
因此在pipeline环境下client library通常直接使用`eval`而不是`evalsha -> script load -> evalsha`
##### Script Cache Semantics
在执行常规操作时来自应用的脚本会被一直保存在cache中直到server被重启或缓存被清空。
清空缓存的唯一方式是通过显式调用`SCRIPT FLUSH`命令执行该命令会完全清空script cache移除目前执行过的所有脚本内容。通常来说该命令只在`当前redis实例需要初始化并后续为其他应用服务`时被调用。
#### Script Command
`SCRIPT`命令提供了一系列用于控制scripting subsystem的方法
- `SCRIPT FLUSH`: 该命令是清空redis script cache的唯一方式
- `SCRIPT EXISTS`: 可接收一个或多个sha1 digests作为参数该命令会返回值为`0 or 1`的数组。其中`1`代表脚本在cache中存在`0`代表脚本在cache中不存在。
- `SCRIPT LOAD`: 该命令会向redis cache中注册一个特定的脚本。在执行`EVALSHA`命令之前执行该命令,能够确保`EVALSHA`命令不会失败
- `SCRIPT KILL`: 该命令是打断`long-running script`的唯一方式。当script的执行时长超过限制默认为5s如果该脚本执行时未对数据进行任何修改则可以通过`SCRIPT KILL`命令来终止
- `SCRIPT DEBUG`: 用于控制`redis lua scripts debugger`的使用