结合上一期 Redis(一) Redis简介(Redis(一) Redis简介-CSDN博客)

目录

Redis 可编程性

运行脚本

只读脚本

只读脚本历史记录 

最长执行时间

Redis 函数

加载库和函数

输入键和常规参数

扩展库 

重用库中的代码

Lua 脚本 

脚本参数化

从脚本与 Redis 交互 

脚本缓存

在 Redis 中调试 Lua 脚本


Redis 可编程性

使用 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 EVAL脚本
  • Redis 函数

在运行脚本或函数时,Redis 保证其原子执行。 脚本的执行会在整个时间内阻止所有服务器活动,这与事务的语义类似。 这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。 已执行脚本的阻塞语义始终适用于所有连接的客户端。

请注意,这种阻塞方法的潜在缺点是执行慢速脚本不是一个好主意。 创建快速脚本并不难,因为脚本的开销非常低。 但是,如果您打算在应用程序中使用慢速脚本,请注意,所有其他客户端都会被阻止,并且在运行时无法执行任何命令。

只读脚本

只读脚本是仅执行不修改 Redis 中任何键的命令的脚本。 可以通过向脚本添加标志或使用只读脚本命令变体之一(EVAL_ROEVALSHA_RO 或 FCALL_RO )执行脚本来执行只读脚本。 它们具有以下属性:no-writes

  • 它们始终可以在副本上执行。
  • 它们始终可以通过 SCRIPT KILL 命令杀死。
  • 当 redis 超过内存限制时,它们永远不会因 OOM 错误而失败。
  • 它们在写入暂停期间不会被阻止,例如在协调故障转移期间发生的暂停。
  • 它们不能执行任何可能修改数据集的命令。
  • 目前,PUBLISH 、SPUBLISH 和 PFCOUNT 也被视为脚本中的写入命令,因为它们可以尝试将命令传播到副本和 AOF 文件。

除了所有只读脚本提供的好处外,只读脚本命令还具有以下优点:

  • 它们可用于将 ACL 用户配置为只能执行只读脚本。
  • 对于希望使用副本进行读取扩展的应用程序,许多客户端还更好地支持将只读脚本命令路由到副本。

只读脚本历史记录 

Redis 7.0 中引入了只读脚本和只读脚本命令

  • 在 Redis 7.0.1  PUBLISH 之前, SPUBLISH  和 PFCOUNT  不被视为脚本中的写入命令
  • 在 Redis 7.0.1 之前,该标志并不意味着no-writesallow-oom
  • 在 Redis 7.0.1 之前,该标志不允许脚本在写入暂停期间运行。no-writes

最长执行时间

脚本受最大执行时间的约束(默认设置为 5 秒)。 此默认超时时间很大,因为脚本通常在不到一毫秒的时间内运行。 该限制已到位,用于处理在开发过程中创建的意外无限循环。

可以以毫秒精度修改脚本可以执行的最长时间, 通过或使用 CONFIG SET 命令。 影响最大执行时间的配置参数称为 。redis.confbusy-reply-threshold

当脚本达到超时阈值时,Redis 不会自动终止该脚本。 这样做将违反 Redis 与脚本引擎之间的约定,该约定确保脚本是原子的。 中断脚本的执行可能会使数据集留下半写更改。

因此,当脚本的执行时间超过配置的超时时,会发生以下情况:

  • Redis 会记录脚本运行时间过长的情况。
  • 它再次开始接受来自其他客户端的命令,但会回复所有发送正常命令的客户端的 BUSY 错误。此状态下唯一允许的命令是 SCRIPT KILLFUNCTION KILL 和 。SHUTDOWN NOSAVE
  • 可以使用 SCRIPT KILL 和 FUNCTION KILL 命令终止仅执行只读命令的脚本。这些命令不违反脚本语义,因为脚本尚未将数据写入数据集。
  • 如果脚本已经执行了一次写入操作,则唯一允许的命令是停止服务器而不将当前数据集保存在磁盘上(基本上,服务器已中止)。SHUTDOWN NOSAVE

Redis 函数

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)

扩展库 

可以向库中添加更多函数,以利于我们的应用程序。 在访问哈希数据时,我们添加到哈希的其他元数据字段不应包含在响应中。 另一方面,我们确实希望提供获取给定哈希键的修改时间戳的方法。

我们将向库添加两个新函数来实现这些目标:

  1. Redis 函数将从给定的哈希键名称返回所有字段及其各自的值,不包括元数据(即字段)。my_hgetall_last_modified_
  2. Redis 函数将返回给定哈希键名称的修改时间戳。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)

Lua 脚本 

Redis 允许用户在服务器上上传和执行 Lua 脚本。 脚本可以采用编程控制结构,并在执行访问数据库时使用大多数命令。 由于脚本在服务器中执行,因此从脚本读取和写入数据非常高效。

Redis 保证脚本的原子执行。 在执行脚本时,所有服务器活动都会在其整个运行时被阻止。 这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。

脚本提供了几个属性,这些属性在许多情况下都很有价值。 这些包括:

  • 通过在数据所在的位置执行逻辑来提供局部性。数据局部性可降低整体延迟并节省网络资源。
  • 确保脚本原子执行的阻塞语义。
  • 启用 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 交互 

可以通过 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"

在 Redis 中调试 Lua 脚本

从版本 3.2 开始,Redis 包含一个完整的 Lua 调试器,可以 用于使编写复杂 Redis 脚本的任务变得更加简单。

代号为 LDB 的 Redis Lua 调试器具有以下重要功能:

  • 它使用服务器-客户端模型,因此它是一个远程调试器。 Redis 服务器充当调试服务器,而默认客户端为 . 但是,可以按照服务器实现的简单协议来开发其他客户端。redis-cli
  • 默认情况下,每个新的调试会话都是一个分叉会话。 这意味着在调试 Redis Lua 脚本时,服务器不会阻塞,并且可用于开发或并行执行多个调试会话。 这也意味着在脚本调试会话完成后会回滚更改,因此可以使用与上一个调试会话完全相同的 Redis 数据集再次重新启动新的调试会话。
  • 可按需提供备用同步(非分叉)调试模型,以便可以保留对数据集的更改。 在此模式下,服务器会在调试会话处于活动状态时阻止。
  • 支持分步执行。
  • 支持静态和动态断点。
  • 支持将调试后的脚本记录到调试器控制台中。
  • 检查 Lua 变量。
  • 跟踪脚本执行的 Redis 命令。
  • Redis 和 Lua 值的漂亮打印。
  • 无限循环和长执行检测,模拟断点。
04-21 03:13