记一次使用Redis-lua脚本的经历

425 阅读6分钟

背景


当前项目需要对一些统计类的数据进行算法统一,优化,按照产品给定的指标,进行统一计算,将数据进行原子化计算并存储,方便后续维护,在用时进行拼装组合,因为需要获取大量的缓存数据,所以我们决定在查询时使用Lua脚本来完成取值操作。

技术调研


  • 优点

    • 原子性:Lua脚本在Redis中以原子方式执行。对于需要保持数据一致性的场景非常重要
    • 网络开销减少:将多个get操作合并为一个请求,较少了网络往返(RTT)的开销。适用于高频率、大批量的读取操作。在高并发的情况下,网络延迟会显著影响性能,使用Lua脚本能大幅提高效率。
    • 逻辑处理:如果在获取键值的同时需要对数据进行处理(比如过滤、聚合等),Lua脚本是在服务器端直接完成,避免了将数据传回客户端再处理。
  • 缺点

    • 调试困难:因为是在Redis服务器端进行,所以不容易观察执行状态和获取错误信息。
    • 性能瓶颈:因为Lua脚本是原子性操作,如果执行时间过长,会阻塞其他命令,导致性能下降。
  • 前提

    使用Lua脚本的前提是在Redis中开启了Lua脚本的支持,默认是开启的,可以通过以下方式测试是否开启

    • 查看Redis版本

      Lua脚本支持自Redis 2.6版本开始就已经内置了。因此,确保你的Redis版本是2.6或更高版本。在Redis CLI中运行以下命令可以查看版本:

      INFO server
      

      在返回的信息中,查找redis_version字段,确保版本为2.6或更高。

    • 运行简单的Lua脚本来测试例如

      EVAL "return 'Hello, Lua!'" 0
      

      如果返回了"Hello, Lua!",则说明Lua脚本执行正常

    • 检查配置文件

      尽管Lua脚本支持是默认启用的,但在某些特定的环境中(如某些云服务或容器化环境),可能会有配置限制。可以查看Redis的配置文件(redis.conf)中是否有以下配置:

      lua-time-limit <milliseconds>
      

      这个配置项用于设置Lua脚本的超时时间,通常默认值是5000毫秒(5秒)。如果没有此配置,说明使用的是默认设置。

在Redis中,使用Lua脚本时,EVAL命令的语法介绍:


语法

EVAL script numkeys key [key ...] arg [arg ...]
  • script: 要执行的Lua脚本。
  • numkeys: 表示后续传入的键(KEYS)的数量。
  • key [key ...]: 后续传入的具体键。如果numkeys为0,则表示没有键。
  • arg [arg ...]: 传入给Lua脚本的额外参数。

关于 numkeys 和传递的键

  • 如果你将numkeys设置为0,那么后面不应该有任何键传入。这意味着你在执行脚本时没有提供任何键供Lua脚本访问。
  • 如果你的Lua脚本需要使用KEYS数组中的键,你必须确保numkeys的值与后续传入的键的数量一致。

示例

以下是一些使用EVAL命令的例子:

1. 没有键的情况

如果你没有要操作的键,应该这样调用:

EVAL "return 'Hello, Lua!'" 0

这里的0表示没有键传递给脚本,因此脚本中不能访问KEYS数组。

2. 有一个键的情况

如果你需要一个键,比如获取某个键的值,你应该这样调用:

EVAL "return redis.call('GET', KEYS[1])" 1 test:10002:age

这里的1表示你传入了一个键test:10002:age,脚本中可以通过KEYS[1]来访问它。

具体案例


完整的Lua脚本

local cursor = "0"
local pattern = KEYS[1]  -- 从参数中获取匹配模式
local values = {}

repeat
    local res = redis.call('SCAN', cursor, 'MATCH', pattern, 'COUNT', 1000)  -- 每次扫描1000个键
    cursor = res[1]  -- 更新游标

    for _, key in ipairs(res[2]) do  -- 遍历匹配的键
        local value = redis.call('GET', key)  -- 获取每个键的值
        if value and value ~= "" then  -- 检查值是否存在且不为空串
            table.insert(values, value)  -- 将值添加到结果集
        end
    end
until cursor == "0"  -- 如果游标为0,表示扫描完成

return values  -- 返回所有值的集合

使用Redis_cli进行测试语句如下,需要将双引号改成单引号

eval "local cursor = '0' local pattern = KEYS[1] local values = {}  repeat local res = redis.call('SCAN', cursor, 'MATCH', pattern, 'COUNT', 1000) cursor = res[1]  for _, key in ipairs(res[2]) do local value = redis.call('GET', key)   if value  and value ~= '' then table.insert(values, value) end end until cursor == '0' return values" 1 test:*:*

SCAN命令的解释

redis/db1> eval "return redis.call('SCAN', 0, 'MATCH', KEYS[1], 'COUNT', 1000)" 1 test:*:*
1) 2957
2) 1) test:10001:age
   2) test:10002:name
   3) test:10003:sex
   4) test:10001:name
  • SCAN命令的解释

    • 游标(Cursor)

      • SCAN 是一个增量迭代的命令,它通过游标来跟踪当前扫描的位置。每次调用 SCAN 时都会返回一个新的游标值,供下次调用使用。
      • 如果返回的游标为 0,这表示扫描已经完成。
    • 匹配模式(MATCH)

      • MATCH 后面可以跟一个模式(例如通配符模式),用于过滤返回的键。只有符合该模式的键会被返回。
    • 计数(COUNT)

      • COUNT 是一个提示值,告诉 Redis 每次返回时尽量返回的键的数量,也是一次扫描中扫描key的个数。在这个例子中,COUNT 1000 表示 Redis 尝试返回最多 1000 个键。
      • 注意:这个值并不是保证返回的数量,实际返回的键的数量可能会少于这个值,因为 SCAN 命令在内部是增量迭代的,所以它并不一定会在每次调用中返回指定的数量。
  • COUNT的指定

    • 数据规模:如果你的Redis数据库中存储了大量的键,可以考虑将COUNT设置得高一些,以减少扫描的次数。例如,如果你有成千上万的键,可以尝试1000或更多。
    • 性能:较大的COUNT值可能会导致一次扫描消耗更多的时间和内存,因此要根据实际情况进行测试。如果Redis服务器在处理SCAN命令时出现延迟或性能下降,可以尝试减小COUNT值。
    • 负载:如果Redis服务器的负载较高,可能需要将COUNT设置得小一些,以避免一次性取回大量数据,造成服务器负担过重。