背景
当前项目需要对一些统计类的数据进行算法统一,优化,按照产品给定的指标,进行统一计算,将数据进行原子化计算并存储,方便后续维护,在用时进行拼装组合,因为需要获取大量的缓存数据,所以我们决定在查询时使用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设置得小一些,以避免一次性取回大量数据,造成服务器负担过重。
- 数据规模:如果你的Redis数据库中存储了大量的键,可以考虑将