赞
踩
结合上一期 Redis(一) Redis简介(Redis(一) Redis简介-CSDN博客)
目录
使用 Lua 和 Redis 函数扩展 Redis供了一个编程接口,允许您在服务器本身上执行自定义脚本。在 Redis 7 及更高版本中,您可以使用 Redis functions 来管理和运行脚本。在 Redis 6.2 及更低版本中,您可以使用 带有EVAL命令的 Lua 编写脚本对服务器进行编程。
Redis 是一种“抽象数据类型的特定领域语言”。 Redis 使用的语言由其命令组成。 大多数命令专门用于以不同的方式操作核心数据类型。 在许多情况下,这些命令提供了开发人员在 Redis 中管理应用程序数据所需的所有功能。
Redis 中的术语可编程性是指能够由服务器执行任意用户定义的逻辑。 我们将这些逻辑片段称为脚本。 在我们的例子中,脚本可以处理它所在的数据,也就是数据局部性。 此外,在 Redis 服务器中负责任地嵌入编程工作流有助于减少网络流量并提高整体性能。 开发人员可以使用此功能来实现可靠的、特定于应用程序的 API。 此类 API 可以封装业务逻辑,并跨多个键和不同数据结构维护数据模型。
用户脚本由嵌入式沙盒脚本引擎在 Redis 中执行。 目前,Redis 支持单个脚本引擎,即 Lua 5.1 解释器。
Redis 提供了两种运行脚本的方法。
首先,从 Redis 2.6.0 开始,EVAL命令允许运行服务器端脚本。 Eval 脚本提供了一种快速而直接的方法,让 Redis 临时运行您的脚本。 但是,使用它们意味着脚本逻辑是应用程序的一部分(而不是 Redis 服务器的扩展)。 运行脚本的每个应用程序实例都必须具有随时可加载脚本的源代码。 这是因为脚本仅由服务器缓存,并且是易失性的。 随着应用程序的增长,此方法可能更难开发和维护。
其次,在 v7.0 中添加的 Redis 函数本质上是作为一类数据库元素的脚本。 因此,函数将脚本与应用程序逻辑分离,并支持脚本的独立开发、测试和部署。 要使用函数,需要先加载它们,然后它们可供所有连接的客户端使用。 在这种情况下,将函数加载到数据库将成为管理部署任务(例如加载 Redis 模块),这会将脚本与应用程序分开。
有关详细信息,请参阅以下页面:
在运行脚本或函数时,Redis 保证其原子执行。 脚本的执行会在整个时间内阻止所有服务器活动,这与事务的语义类似。 这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。 已执行脚本的阻塞语义始终适用于所有连接的客户端。
请注意,这种阻塞方法的潜在缺点是执行慢速脚本不是一个好主意。 创建快速脚本并不难,因为脚本的开销非常低。 但是,如果您打算在应用程序中使用慢速脚本,请注意,所有其他客户端都会被阻止,并且在运行时无法执行任何命令。
只读脚本是仅执行不修改 Redis 中任何键的命令的脚本。 可以通过向脚本添加标志或使用只读脚本命令变体之一(EVAL_RO、EVALSHA_RO 或 FCALL_RO )执行脚本来执行只读脚本。 它们具有以下属性:no-writes
除了所有只读脚本提供的好处外,只读脚本命令还具有以下优点:
Redis 7.0 中引入了只读脚本和只读脚本命令
no-writes
allow-oom
no-writes
脚本受最大执行时间的约束(默认设置为 5 秒)。 此默认超时时间很大,因为脚本通常在不到一毫秒的时间内运行。 该限制已到位,用于处理在开发过程中创建的意外无限循环。
可以以毫秒精度修改脚本可以执行的最长时间, 通过或使用 CONFIG SET 命令。 影响最大执行时间的配置参数称为 。redis.conf
busy-reply-threshold
当脚本达到超时阈值时,Redis 不会自动终止该脚本。 这样做将违反 Redis 与脚本引擎之间的约定,该约定确保脚本是原子的。 中断脚本的执行可能会使数据集留下半写更改。
因此,当脚本的执行时间超过配置的超时时,会发生以下情况:
SHUTDOWN NOSAVE
SHUTDOWN NOSAVE
Redis 函数是从临时脚本演变而来的一步。
函数提供与脚本相同的核心功能,但却是数据库的一流软件工件。 Redis 将函数作为数据库的一个组成部分进行管理,并通过数据持久化和复制来确保其可用性。 由于函数是数据库的一部分,因此在使用之前声明,因此应用程序不需要在运行时加载它们,也不需要冒中止事务的风险。 使用函数的应用程序仅依赖于其 API,而不依赖于数据库中的嵌入式脚本逻辑。
虽然临时脚本被视为应用程序域的一部分,但函数使用用户提供的逻辑扩展数据库服务器本身。 它们可用于公开由核心 Redis 命令组成的更丰富的 API,类似于模块,开发一次,启动时加载,并被各种应用程序/客户端重复使用。 每个函数都有一个唯一的用户定义名称,可以更轻松地调用和跟踪其执行。
Redis 函数的设计还试图在用于编写函数的编程语言和服务器管理函数之间进行划分。 Lua 是 Redis 目前支持的唯一作为嵌入式执行引擎的语言解释器,它旨在简单易学。 然而,选择 Lua 作为一种语言仍然给许多 Redis 用户带来了挑战。
Redis 函数功能不对实现的语言做出任何假设。 作为函数定义一部分的执行引擎处理运行它。 从理论上讲,引擎可以用任何语言执行函数,只要它遵守几个规则(例如终止执行函数的能力)。
目前,如上所述,Redis 附带了一个嵌入式 Lua 5.1 引擎。 有计划在未来支持更多的引擎。 Redis 函数可以使用 Lua 的所有可用功能来临时脚本, 唯一的例外是 Redis Lua脚本调试器。
函数还通过启用代码共享来简化开发。 每个函数都属于一个库,任何给定的库都可以由多个函数组成。 库的内容是不可变的,并且不允许有选择地更新其函数。 取而代之的是,库作为一个整体进行更新,其所有功能都在一个操作中。 这允许从同一库中的其他函数调用函数,或者使用库内部方法中的通用代码在函数之间共享代码,这些方法也可以采用语言本机参数。
如上所述,函数旨在更好地支持通过逻辑架构维护数据实体视图的一致视图的用例。 因此,函数与数据本身一起存储。 函数还保留到 AOF 文件中,并从主函数复制到副本,因此它们与数据本身一样持久。 当 Redis 用作临时缓存时,需要其他机制(如下所述)来使函数更持久。
与 Redis 中的所有其他操作一样,函数的执行是原子的。 函数的执行会在整个时间内阻止所有服务器活动,这与事务的语义类似。 这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。 已执行函数的阻塞语义始终适用于所有连接的客户端。 由于运行函数会阻塞 Redis 服务器,因此函数需要快速完成执行,因此应避免使用长时间运行的函数。
以下代码片段演示了一个简单的库,该库注册了一个名为 knockknock 的函数,并返回字符串回复:
- redis.register_function(
- 'knockknock',
- function() return 'Who\'s there?' end
- )
在我们转到以下示例之前,了解 Redis 对键名称参数和非键名称参数的区分至关重要。
虽然 Redis 中的键名称只是字符串,但与任何其他字符串值不同,这些值表示数据库中的键。 密钥名称是 Redis 中的一个基本概念,也是操作 Redis 集群的基础。
重要:为确保在独立部署和集群部署中正确执行 Redis 函数,必须显式提供函数访问的所有键名称作为输入键参数。
任何不是键名称的函数输入都是常规输入参数。
现在,让我们假设我们的应用程序将其部分数据存储在 Redis Hashes 中。 我们想要一种类似 HSET 的方式来设置和更新所述哈希中的字段,并将上次修改时间存储在名为 的新字段中。 我们可以实现一个函数来完成所有这些工作。_last_modified_
我们的函数将调用 TIME 来获取服务器的时钟读数,并使用新字段的值和修改的时间戳更新目标哈希值。 我们将实现的函数接受以下输入参数:哈希的键名和要更新的字段值对。
适用于 Redis 函数的 Lua API 使这些输入可作为函数回调的第一个和第二个参数进行访问。 回调的第一个参数是一个 Lua 表,其中填充了函数的所有键名输入。 同样,回调的第二个参数由所有常规参数组成。
以下是我们的函数及其库注册的可能实现:
- local function my_hset(keys, args)
- local hash = keys[1]
- local time = redis.call('TIME')[1]
- return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
- end
-
- redis.register_function('my_hset', my_hset)
可以向库中添加更多函数,以利于我们的应用程序。 在访问哈希数据时,我们添加到哈希的其他元数据字段不应包含在响应中。 另一方面,我们确实希望提供获取给定哈希键的修改时间戳的方法。
我们将向库添加两个新函数来实现这些目标:
my_hgetall
_last_modified_
my_hlastmodified
- local function my_hset(keys, args)
- local hash = keys[1]
- local time = redis.call('TIME')[1]
- return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
- end
-
- local function my_hgetall(keys, args)
- redis.setresp(3)
- local hash = keys[1]
- local res = redis.call('HGETALL', hash)
- res['map']['_last_modified_'] = nil
- return res
- end
-
- local function my_hlastmodified(keys, args)
- local hash = keys[1]
- return redis.call('HGET', hash, '_last_modified_')
- end
-
- redis.register_function('my_hset', my_hset)
- redis.register_function('my_hgetall', my_hgetall)
- redis.register_function('my_hlastmodified', my_hlastmodified)
除了将函数捆绑到数据库管理的软件工件中之外,库还促进了代码共享。 我们可以将一个从其他函数调用的错误处理帮助程序函数添加到我们的库中。 帮助程序函数验证输入键表是否具有单个键。 成功后,它会返回 ,否则会返回错误回复。check_keys()
nil
- local function check_keys(keys)
- local error = nil
- local nkeys = table.getn(keys)
- if nkeys == 0 then
- error = 'Hash key name not provided'
- elseif nkeys > 1 then
- error = 'Only one key name is allowed'
- end
-
- if error ~= nil then
- redis.log(redis.LOG_WARNING, error);
- return redis.error_reply(error)
- end
- return nil
- end
-
- local function my_hset(keys, args)
- local error = check_keys(keys)
- if error ~= nil then
- return error
- end
-
- local hash = keys[1]
- local time = redis.call('TIME')[1]
- return redis.call('HSET', hash, '_last_modified_', time, unpack(args))
- end
-
- local function my_hgetall(keys, args)
- local error = check_keys(keys)
- if error ~= nil then
- return error
- end
-
- redis.setresp(3)
- local hash = keys[1]
- local res = redis.call('HGETALL', hash)
- res['map']['_last_modified_'] = nil
- return res
- end
-
- local function my_hlastmodified(keys, args)
- local error = check_keys(keys)
- if error ~= nil then
- return error
- end
-
- local hash = keys[1]
- return redis.call('HGET', keys[1], '_last_modified_')
- end
-
- redis.register_function('my_hset', my_hset)
- redis.register_function('my_hgetall', my_hgetall)
- redis.register_function('my_hlastmodified', my_hlastmodified)
Redis 允许用户在服务器上上传和执行 Lua 脚本。 脚本可以采用编程控制结构,并在执行访问数据库时使用大多数命令。 由于脚本在服务器中执行,因此从脚本读取和写入数据非常高效。
Redis 保证脚本的原子执行。 在执行脚本时,所有服务器活动都会在其整个运行时被阻止。 这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。
脚本提供了几个属性,这些属性在许多情况下都很有价值。 这些包括:
Lua 允许您在 Redis 中运行部分应用程序逻辑。 此类脚本可以跨多个键执行条件更新,可能以原子方式组合几种不同的数据类型。
尽管服务器执行它们,但 Eval 脚本被视为客户端应用程序的一部分,这就是它们未命名、版本控制或持久化的原因。 因此,如果缺少所有脚本,应用程序可能需要随时重新加载(在服务器重新启动、故障转移到副本等之后)。 从版本 7.0 开始, 提供了一种可编程性的替代方法,它允许使用额外的编程逻辑来扩展服务器本身。
应用程序可以发送这两个完全不同但同时完全相同的脚本:
- redis> EVAL "return 'Hello'" 0
- "Hello"
- redis> EVAL "return 'Scripting!'" 0
- "Scripting!"
尽管 Redis 未阻止此操作模式,但由于脚本缓存考虑,它是一种反模式(更多内容见下文)。 您可以参数化它们并传递执行它们所需的任何参数,而不是让应用程序生成相同脚本的细微变体。
以下示例演示了如何通过参数化实现与上述相同的效果:
- redis> EVAL "return ARGV[1]" 0 Hello
- "Hello"
- redis> EVAL "return ARGV[1]" 0 Parameterization!
- "Parameterization!"
可以通过 redis.call()
或 redis.pcall()
从 Lua 脚本调用 Redis 命令。
两者几乎相同。 两者都执行 Redis 命令及其提供的参数(如果这些参数表示格式正确的命令)。 但是,这两个函数之间的区别在于处理运行时错误(例如语法错误)的方式。 调用函数引发的错误将直接返回给执行该函数的客户端。 相反,调用函数时遇到的错误将返回到脚本的执行上下文,以便进行可能的处理。
- > EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
- OK
每当我们调用 EVAL 时,我们还会在请求中包含脚本的源代码。 重复调用 EVAL 来执行同一组参数化脚本,既浪费了网络带宽,又在 Redis 中产生了一些开销。 当然,节省网络和计算资源是关键,因此,Redis 为脚本提供了缓存机制。
使用EVAL执行的每个脚本都存储在服务器保留的专用缓存中。 缓存的内容按脚本的 SHA1 摘要总和进行组织,因此脚本的 SHA1 摘要总和在缓存中唯一标识它。 您可以通过运行 EVAL 并在之后调用 INFO 来验证此行为。 您会注意到used_memory_scripts_eval 和 number_of_cached_scripts 指标会随着执行的每个新脚本而增长。
如上所述,动态生成的脚本是一种反模式。 在应用程序运行时生成脚本可能会(并且可能会)耗尽主机的内存资源来缓存它们。 相反,脚本应该尽可能通用,并通过其参数提供自定义执行。
通过调用 SCRIPT LOAD 命令并提供其源代码,将脚本加载到服务器的缓存中。 服务器不执行脚本,而只是编译并将其加载到服务器的缓存中。 加载后,可以使用从服务器返回的 SHA1 摘要执行缓存脚本。
- redis> SCRIPT LOAD "return 'Immabe a cached script'"
- "c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f"
- redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0
- "Immabe a cached script"
从版本 3.2 开始,Redis 包含一个完整的 Lua 调试器,可以 用于使编写复杂 Redis 脚本的任务变得更加简单。
代号为 LDB 的 Redis Lua 调试器具有以下重要功能:
redis-cli
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。