首页
Preview

Redis7新特性之函数

Redis函数

Redis函数是用于管理在服务器上执行的代码的API。这个功能在Redis 7中可用,取代了在以前的Redis版本中使用的EVAL命令。

序言(或者说,Eval脚本有什么问题?)

在Redis的早期版本中,脚本仅通过EVAL命令提供,该命令允许将Lua脚本发送给服务器执行。 Eval脚本的核心用例是在Redis内部执行应用程序逻辑的一部分,以高效且原子性地执行。 这样的脚本可以在多个键之间执行条件更新,可能还可以结合多种不同的数据类型。

使用EVAL要求应用程序每次都发送完整的脚本进行执行。 由于这会导致网络和脚本编译开销,Redis提供了EVALSHA命令的优化。通过首先调用SCRIPT LOAD获取脚本的SHA1,应用程序可以后续只使用摘要来调用脚本。

根据设计,Redis只缓存已加载的脚本。 这意味着脚本缓存可能会在任何时候丢失,例如在调用SCRIPT FLUSH之后、重新启动服务器之后或故障转移到副本之后。 如果缺少任何脚本,应用程序在运行时负责重新加载脚本。 底层的假设是脚本是应用程序的一部分,而不是由Redis服务器维护。

这种方法适用于许多轻量级的脚本使用情况,但是一旦应用程序变得复杂并且更多地依赖于脚本,就会引入几个困难,即:

  1. 所有客户端应用程序实例都必须维护所有脚本的副本。这意味着必须有一些机制将脚本更新应用于应用程序的所有实例。
  2. 事务的上下文中调用缓存脚本会增加事务失败的概率,因为可能会缺少脚本。由于更有可能失败,使用缓存脚本作为工作流的构建块就不太吸引人了。
  3. SHA1摘要毫无意义,使得系统的调试非常困难(例如,在MONITOR会话中)。
  4. 如果使用不当,EVAL会促使一个反模式,即客户端应用程序直接将脚本渲染为原样,而不是负责地使用!KEYSARGV Lua API
  5. 由于它们是临时的,一个脚本不能调用另一个脚本。这使得在脚本之间共享和重用代码几乎不可能,除非在客户端预处理(参见第一点)。

为了满足这些需求,同时避免对已建立和受欢迎的临时脚本进行破坏性更改,Redis v7.0引入了Redis函数。

Redis函数是什么?

Redis函数是从临时脚本演变而来的一个进步。

函数提供与脚本相同的核心功能,但是它们是数据库的一部分,是作为数据库的一个整体进行管理的。 Redis将函数作为数据库的一部分进行管理,并通过数据持久性和复制来确保其可用性。 因为函数是数据库的一部分,所以在运行时不需要加载它们,也不需要冒风险中止事务。 使用函数的应用程序只依赖于它们的API,而不是依赖于数据库中嵌入的脚本逻辑。

而临时脚本被认为是应用程序的一部分,函数扩展了数据库服务器本身,以用户提供的逻辑为基础。 它们可以用于公开由核心Redis命令组成的更丰富的API,类似于模块,只需开发一次,启动时加载,然后由各种应用程序/客户端重复使用。 每个函数都有一个唯一的用户定义名称,这使得调用和跟踪其执行变得更加容易。

Redis函数的设计还试图区分用于编写函数的编程语言和服务器对其进行管理的语言。 目前,Redis仅配备了一个内置的Lua 5.1引擎。 未来计划支持更多的引擎。 Redis函数可以使用Lua的所有可用功能来执行临时脚本,唯一的例外是Redis Lua脚本调试器

函数还通过启用代码共享简化了开发。 每个函数都属于一个库,任何给定的库可以包含多个函数。 库的内容是不可变的,不允许选择性地更新其函数。 相反,库作为一个整体进行更新,所有函数一起进行操作。 这允许在同一库中的其他函数中调用函数,或者通过在库内部方法中使用共同的代码来在函数之间共享代码,该方法还可以接受语言本地参数。

函数旨在更好地支持通过逻辑模式维护数据实体的一致视图的用例,如上所述。 因此,函数与数据本身一起存储。 函数也会被持久化到AOF文件,并从主节点复制到副本,因此它们与数据本身一样持久。 当Redis用作临时缓存时,需要使用其他机制(下面描述)使函数更持久。

与Redis中的所有其他操作一样,函数的执行是原子的。 函数的执行在整个时间内都会阻塞所有服务器活动,类似于事务的语义。 这些语义意味着脚本的所有影响要么尚未发生,要么已经发生。 在任何时候,运行函数都会阻塞所有连接的客户端。 由于运行函数会阻塞Redis服务器,因此函数的执行速度应该很快,因此应避免使用长时间运行的函数。

加载库和函数

让我们通过一些具体的例子和Lua代码片段来了解Redis函数。

如果您对Lua一般以及在Redis中的使用不熟悉,您可能会从介绍Eval脚本Lua API页面中的一些示例中受益,以更好地理解该语言。

每个Redis函数都属于一个加载到Redis的单个库。 使用FUNCTION LOAD命令将库加载到数据库中。 该命令以库负载作为输入, 库负载必须以Shebang语句开头,该语句提供了有关库的元数据(例如要使用的引擎和库名称)。 Shebang格式如下:

#!<引擎名称> name=<库名称>

让我们尝试加载一个空库:

redis> FUNCTION LOAD "#!lua name=mylib\n"
(error) ERR No functions registered

这个错误是预期的,因为加载的库中没有函数。每个库需要至少包含一个已注册的函数才能成功加载。 已注册的函数具有名称,并充当库的入口点。 当目标执行引擎处理FUNCTION LOAD命令时,它将注册库的函数。

Lua引擎在加载时编译和评估库源代码,并通过调用redis.register_function()API注册函数。

以下代码片段演示了一个简单的库,注册了一个名为_knockknock_的函数,返回一个字符串回复:

#!lua name=mylib
redis.register_function(
  'knockknock',
  function() return 'Who\'s there?' end
)

在上面的示例中,我们向Lua的redis.register_function()API提供了两个关于函数的参数:它的注册名称和一个回调函数。

我们可以加载我们的库并使用FCALL调用已注册的函数:

redis> FUNCTION LOAD "#!lua name=mylib\nredis.register_function('knockknock', function() return 'Who\\'s there?' end)"
mylib
redis> FCALL knockknock 0
"Who's there?"

请注意,FUNCTION LOAD命令返回已加载的库的名称,此名称稍后可以用于FUNCTION LISTFUNCTION DELETE

我们向FCALL提供了两个参数:函数的注册名称和数值0。这个数值表示其后跟随的键名的数量(与EVALEVALSHA的工作方式相同)。

我们将立即解释键名和其他参数如何在函数中可用。由于这个简单的示例不涉及键,我们现在只使用0。

输入键和常规参数

在我们移动到下一个示例之前,了解Redis在键名参数和非键名参数之间所做的区分非常重要。

在Redis中,键名只是字符串,与其他任何字符串值不同,它们表示数据库中的键。 键的名称是Redis的一个基本概念,是操作Redis Cluster的基础。

重要: 为了确保Redis函数的正确执行,在独立和集群部署中,函数访问的所有键名参数都必须明确提供为输入键参数。

函数中不是键名的任何输入都是常规输入参数。

现在,假设我们的应用程序将一些数据存储在Redis Hash中。 我们希望有一种类似于HSET的方法来设置和更新该Hash中的字段,并在名为_last_modified_的新字段中存储最后修改时间。 我们可以实现一个函数来完成所有这些操作。

我们的函数将调用TIME来获取服务器的时钟读数,并使用新字段值和修改的时间戳更新目标Hash。 我们将实现的函数接受以下输入参数:Hash的键名和要更新的字段-值对。

Redis函数的Lua API使得这些输入作为函数回调的第一个和第二个参数可访问。 回调的第一个参数是一个Lua表,其中包含函数的所有键名输入。 类似地,回调的第二个参数由所有常规参数组成。

以下是我们函数的一个可能实现和其库的注册:

#!lua name=mylib

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)

如果我们创建一个名为_mylib.lua_的新文件,其中包含库的定义,我们可以这样加载它(不删除源代码中有用的空白):

$ cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE

我们在调用FUNCTION LOAD时添加了REPLACE修饰符,以告诉Redis我们要覆盖现有的库定义。 否则,我们会收到Redis的错误,提示库已经存在。

现在,已加载库的更新代码已加载到Redis中,我们可以继续调用我们的函数:

redis> FCALL my_hset 1 myhash myfield "some value" another_field "another value"
(integer) 3
redis> HGETALL myhash
1) "_last_modified_"
2) "1640772721"
3) "myfield"
4) "some value"
5) "another_field"
6) "another value"

在这种情况下,我们使用1为键名参数的数量调用了FCALL。 这意味着函数的第一个输入参数是一个键名(因此包含在回调的keys表中)。 在该第一个参数之后,所有后续的输入参数都被视为常规参数,并构成传递给回调的args表,作为其第二个参数。

扩展库

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

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

  1. my_hgetall Redis函数将返回给定Hash键的所有字段及其对应的值,不包括元数据(即_last_modified_字段)。
  2. my_hlastmodified Redis函数将返回给定Hash键的修改时间戳。

库的源代码可能如下所示:

#!lua name=mylib

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)

虽然上面的内容应该很简单,但请注意,my_hgetall还调用了redis.setresp(3)。 这意味着函数在调用redis.call()之后期望RESP3响应,与默认的RESP2协议不同,它提供了字典(关联数组)响应。 这样做允许函数从回复中删除(或设置为nil,如Lua表的情况)特定的字段,在我们的例子中是_last_modified_字段。

假设您将库的实现保存在_mylib.lua_文件中,您可以使用以下命令将其替换:

$ cat mylib.lua | redis-cli -x FUNCTION LOAD REPLACE

一旦加载完成,您可以使用FCALL调用库的函数:

redis> FCALL my_hgetall 1 myhash
1) "myfield"
2) "some value"
3) "another_field"
4) "another value"
redis> FCALL my_hlastmodified 1 myhash
"1640772721"

您还可以使用FUNCTION LIST命令获取库的详细信息:

redis> FUNCTION LIST
1) 1) "library_name"
   2) "mylib"
   3) "engine"
   4) "LUA"
   5) "functions"
   6) 1) 1) "name"
         2) "my_hset"
         3) "description"
         4) (nil)
      2) 1) "name"
         2) "my_hgetall"
         3) "description"
         4) (nil)
      3) 1) "name"
         2) "my_hlastmodified"
         3) "description"
         4) (nil)

您可以看到更新库的功能非常简单。

在库中重用代码

除了将函数捆绑到一起作为数据库管理的软件工件之外,库还促进了代码共享。 我们可以向库中添加一个错误处理辅助函数,供其他函数调用。 辅助函数check_keys()验证输入的_keys_表是否具有一个键。 成功时返回nil,否则返回一个错误回复

更新的库的源代码如下:

#!lua name=mylib

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允许使用FCALL_RO对只读副本执行my_hgetallmy_hlastmodified

redis> FCALL_RO my_hgetall 1 myhash
1) "myfield"
2) "some value"
3) "another_field"
4) "another value"
redis> FCALL_RO my_hlastmodified 1 myhash
"1640772721"

您还可以使用FUNCTION LIST命令获取库的详细信息:

redis> FUNCTION LIST
1) 1) "library_name"
   2) "mylib"
   3) "engine"
   4) "LUA"
   5) "functions"
   6) 1) 1) "name"
         2) "my_hset"
         3) "description"
         4) (nil)
      2) 1) "name"
         2) "my_hgetall"
         3) "description"
         4) (nil)
      3) 1) "name"
         2) "my_hlastmodified"
         3) "description"
         4) (nil)

您可以看到更新库的功能非常简单。

集群中的函数

如上所述,Redis会自动处理已加载函数到副本的复制。 在Redis Cluster中,还需要将函数加载到所有集群节点。这不是由Redis Cluster自动处理的,需要由集群管理员处理(例如模块加载、配置设置等)。

由于函数的一个目标是与客户端应用程序分开存在,这不应该是Redis客户端库的职责。相反,可以使用redis-cli --cluster-only-masters --cluster call host:port FUNCTION LOAD ...在所有主节点上执行加载命令。

此外,需要注意的是redis-cli --cluster add-node会自动处理将已加载的函数从现有节点传播到新节点。

函数和临时Redis实例

在某些情况下,可能需要启动一个新的Redis服务器,并预先加载一组函数。常见的原因可能包括:

  • 在新环境中启动Redis
  • 重新启动使用函数的临时(仅缓存)Redis

在这种情况下,我们需要确保在Redis接受传入的用户连接和命令之前,预加载的函数可用。

为此,可以使用redis-cli --functions-rdb从现有服务器中提取函数。这将生成一个RDB文件,可以在Redis启动时加载。

函数标志

在执行函数时,Redis需要一些关于其行为的信息,以便正确执行资源使用策略并维护数据一致性。

例如,Redis需要知道在只读副本上使用FCALL_RO执行某个函数时,该函数将如何行为。

默认情况下,Redis假设所有函数都可以执行任意读写操作。函数标志使得可以在注册函数时声明更具体的函数行为。让我们看看这是如何工作的。

在上面的示例中,我们定义了两个仅读取数据的函数。我们可以尝试使用FCALL_RO来执行它们。

redis > FCALL_RO my_hgetall 1 myhash
(error) ERR Can not execute a function with write flag using fcall_ro.

Redis返回此错误,因为理论上,函数可以在数据库上执行读取和写入操作。 为了安全起见,默认情况下,Redis假设函数既可以读取又可以写入,因此阻止了其执行。 在以下情况下,服务器将返回此错误:

  1. 在只读副本上使用FCALL执行函数。
  2. 使用FCALL_RO执行函数。
  3. 检测到磁盘错误(Redis无法持久化,因此拒绝写入)。

在这些情况下,您可以向函数的注册中添加no-writes标志,禁用保护机制并允许其运行。 要注册带有标志的函数,请使用redis.register_function命名参数变体。

库的更新注册代码片段如下所示:

redis.register_function('my_hset', my_hset)
redis.register_function{
  function_name='my_hgetall',
  callback=my_hgetall,
  flags={ 'no-writes' }
}
redis.register_function{
  function_name='my_hlastmodified',
  callback=my_hlastmodified,
  flags={ 'no-writes' }
}

一旦我们替换了库,Redis允许在只读副本上使用FCALL_RO运行my_hgetallmy_hlastmodified

redis> FCALL_RO my_hgetall 1 myhash
1) "myfield"
2) "some value"
3) "another_field"
4) "another value"
redis> FCALL_RO my_hlastmodified 1 myhash
"1640772721"

有关完整文档标志,请参阅脚本标志

译自:https://redis.io/docs/manual/programmability/functions-intro/

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
weilai1
好未来作为一家科技驱动的教育企业,始终坚持“爱和科技让教育更美好”的使命。一直以来,好未来技术团队致力于教育科技技术的研究与创新。这里是好未来技术团队的对外窗口,每周推送精选技术文章、人才招聘等信息。

评论(0)

添加评论