注意:以下仅基于个人经验总结
注意:以下仅基于个人经验总结
注意:以下仅基于个人经验总结
Redis中可在 lua 中实现多命令的原子化执行,一般情况下性能够用,但是脚本较为复杂且还需更高的性能则需要对 lua 脚本进行优化,以下是我的 lua 性能优化经验,代码以 C# .NET8 为例,Redis 版本为 8.2.0,系统为 Window10,CPU 10 代 i3 主频 3.7 GHZ
其实主要就三点:
- 使用 Pipeline 串多个 lua 批量执行减少网络传输
- lua 内部也尽量进行批量执行
- 单次 lua 内部对中间数据进行缓存
1. 使用 Pipeline 优化
这个是比较常用的优化手段,使用 Pipeline 可以一次打包减少网络往返传输,Pipeline 批次大小需要自己实测调整到一个比较合适的范围就行,还有利用 Redis 的 lua hash 缓存功能避免传输整个脚本进一步减少网络消耗,不过如果你的 Redis 和程序部署在同一台服务器上的话其实没有什么变化。
如果使用多个连接的话还可以开启redis的io多线程功能,有一定的io优化,对lua执行无帮助,执行仍然是单线程
2. lua内部批量执行
这里主要是命令上的优化,例如 set 命令,可以改为 mset 批量操作,get 也改成 mget 批量取值
3. 单次 lua 内部对中间数据进行缓存
这里需要说明的一点是 redis.call 的调用开销很大,即使是在 lua 内部执行 redis.call 命令操作 Redis,如果 call 命令调用多了也会严重拉低执行性能,性能与 redis.call 次数负相关,个人经验是lua脚本内部不超过 5个 call 命令比较好,最好是三个以内。
以下为测试程序和结果:
只要不调用 redis.call 命令,纯 lua 执行还是非常快的基本上可以忽略不记,所以可以把一些数据缓存在 lua 内存中,注意 Redis 的 lua 环境是沙箱,每次执行 lua 脚本都是重新初始化一个 lua 环境,所以 缓存仅限于当前执行时有效。
可以将lua执行的参数也进行批量收集,然后传递给 lua 脚本,所有参数在同一个脚本里面进行执行,这样就可以将一些中间数据直接在当前 lua 内进行缓存减少 redis.call 的调用提升执行性能。
如一个简单的计费功能(假设余额充足):
需要两个步骤:
- 调用
redis.call命令取出当前值 - 扣减,如果扣减后余额大于0则调用
redis.call更新余额并返回1,否则返回0表示失败(lua中返回false解析出来是null)
这里就不可避免的有两次redis.call调用,如果有一百万次扣费,就会有两百万次的redis.call调用
如果改成lua参数批量化执行将会变成这样(示例简化版本):
参数:
KEYS[1] = balanceKey
ARGV:
userId1 amount1
userId2 amount2
...
lua:
lua
local key = KEYS[1]
-- 本地余额缓存
local balanceCache = {}
local result = {}
for i = 1, #ARGV, 2 do
local userId = ARGV[i]
local amount = tonumber(ARGV[i + 1])
-- 如果缓存没有,才读取 Redis
local balance = balanceCache[userId]
if not balance then
balance = tonumber(redis.call("HGET", key, userId) or "0")
balanceCache[userId] = balance
end
-- 扣费
if balance < amount then
result[#result + 1] = false
else
balance = balance - amount
balanceCache[userId] = balance
result[#result + 1] = true
end
end
-- 批量写回
local args = {}
for userId, balance in pairs(balanceCache) do
args[#args + 1] = userId
args[#args + 1] = balance
end
if #args > 0 then
redis.call("HMSET", key, unpack(args))
end
return result
如果不使用上例手段优化,执行该示例的余额更新lua性能大约是3w/s,使用了优化后性能约20w/s
(数值只是大概值写了很久了忘了当时具体数值)
虽然看着不优化也有3w/s,但是这是只有这一个lua的消耗,如果还要其他高并发的占用性能的脚本,把3w均分下来其实性能很拉跨
另外对于 一些复杂 lua 来说,在使用参数批量收集时再使用集群似乎不是非常好的选择个人感觉限制较大,在执行前会根据你的redis key进行分片分配到某个实例上,但是lua内部可能有key是动态拼接,参数中的key也可能不会分配到同一个实例,虽然可以通过使用 {} 形式将key指定为分片依据,但是一些复杂的lua脚本内部可能还使用了不少别的key,这些key可能不在当前实例,或者最终可能仍然打到同一个实例上,仅个人观点
4. 程序方面的优化
我做了两层批处理收集
第一层是批处理收集lua参数,收集到指定数量(或达到指定最长收集时间)的参数后再提交给pipeline,这收集的一批参数就是执行一次lua的参数。
第二层就是Pipeline,提交Pipeline的执行前的时候也有一个批处理收集,收集到指定数量后(或达到指定最长收集时间)再一次性pipeline处理
另外,批处理收集时还可以根据数据特点和redis key的分布情况进行分组收集,例如将具有相同用户id的参数收集,然后作为一次lua执行,这样能更好的利用lua内缓存以减少 redis.call 的调用次数从而提升性能。
至于结果回调是程序内部对每次执行维护了一个唯一id,通过该id找到对应的调用方回调结果,调用方提交后异步等待即可