Redis lua 执行性能优化
注意以下仅基于个人经验总结注意以下仅基于个人经验总结注意以下仅基于个人经验总结Redis中可在 lua 中实现多命令的原子化执行一般情况下性能够用但是脚本较为复杂且还需更高的性能则需要对 lua 脚本进行优化以下是我的 lua 性能优化经验代码以 C# .NET8 为例Redis 版本为 8.2.0系统为 Window10CPU 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:localkeyKEYS[1]-- 本地余额缓存localbalanceCache{}localresult{}localuserIds{}localuserIdsExistsMap{}-- 收集去重userIdfori1,#ARGV,2dolocaluserIdARGV[i]ifnotuserIdsExistsMap[userId]thenuserIdsExistsMap[userId]trueuserIds[#userIds1]userIdendend-- 从redis取当前数据缓存在本地localvaluesredis.call(HMGET,key,unpack(userIds))fori1,#valuesdolocaluserIduserIds[i]localbalancevalues[i]-- 缓存余额到本地若redis中有值无值代表非法参数跳过ifbalancethenbalanceCache[userId]tonumber(balance)endend-- 计费localj1;fori1,#ARGV,2dolocaluserIdARGV[i]localamountARGV[i1]localoldBalancebalanceCache[userId]ifnotoldBalancethenresult[j]falseelselocalnewBalanceoldBalanceamountifnewBalance0thenbalanceCache[userId]newBalance result[j]trueelseresult[j]falseendendjj1end-- 批量写回localargs{}foruserId,balanceinpairs(balanceCache)doargs[#args1]userId args[#args1]balanceendif#args0thenredis.call(HMSET,key,unpack(args))endreturnresult在内部缓存后一次lua执行实际上只需要执行两次redis.call极大节省开销如果不使用上例手段优化执行该示例的余额更新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找到对应的调用方回调结果调用方提交后异步等待即可