diff --git a/中间件/redis/redis.md b/中间件/redis/redis.md index cdae8e6..48bc06a 100644 --- a/中间件/redis/redis.md +++ b/中间件/redis/redis.md @@ -162,6 +162,15 @@ - [Read-only scripts](#read-only-scripts) - [Sandboxed script context](#sandboxed-script-context) - [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 @@ -2665,4 +2674,136 @@ script仅应该操作`redis中存储的数据`和`脚本执行时传入的作为 - redis会在日志中添加`脚本执行时间过长` - redis开始从其他clients接收commands,但是会对所有发送normal commands的clients返回`BUSY error`。在该场景下,唯一被允许的命令为`SCRIPT_KILL, FUNCTION_KILL, SHUTDOWN NOSAVE` - 对于`read-only script`,可以使用`SCRIPT_KILL`和`FUNCTION_KILL`命令,因为该脚本未对数据造成任何修改 - - 如果`script`在执行过程中哪怕执行了一个write operation,那么只能使用`SHUTDOWN NOSAVE`命令,其会停止server,并且不会将当前的data set保存到磁盘中 \ No newline at end of file + - 如果`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`中指定的key,redis 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`的使用 +