为什么 redis 中的 lua 脚本如此慢?有什么解决方法吗?

我正在评估在 redis 中使用 lua 脚本,它们似乎有点慢。我进行如下基准测试:

  1. 对于一个非 lua 版本,我简单地执行了 SET key_i val_i 1M 次
  2. 对于一个lua 版本,我做了同样的事情,但是在一个脚本中:EVAL "SET KEYS[1] ARGV[1]" 1 key_i val_i

在我的笔记本电脑上测试,lua 版本大约比非 lua 版本慢 3 倍。我知道 lua 是一种脚本语言,而不是编译语言等等,但这似乎是很大的性能开销-这是正常的吗?

假设这确实是正常的,是否有任何解决办法?是否有一种方法来使用更快的语言(例如 redis 是用 C 编写的)实现脚本,以实现更好的性能?

编辑:我正在使用此处的 go 代码进行测试:https://gist.github.com/ortutay/6c4a02dee0325a608941

点赞
用户734069
用户734069

问题不在于 Lua 或 Redis,而在于您的期望。您正在将脚本编译1百万次。没有理由期望这会很快。

Redis 中 EVAL 的目的不是执行单个命令;您可以自行执行。目的是在 Redis 自身中进行复杂逻辑,而不是在本地客户端上执行。也就是说,您实际上在单个 EVAL 脚本中执行了 1 百万个设置操作的整个系列,这将由 Redis 服务器本身执行。

我不太了解 Go,所以无法编写调用它的语法。但我知道 Lua 脚本会是什么样子:

for i = 1, ARGV[1] do
  local key = "key:" .. tostring(i)
  redis.call('SET', key, i)
end

将其放入 Go 字符串中,然后将其传递给适当的调用,不带键参数,并且单个非键参数是循环次数。

2016-03-22 01:49:31
用户12671031
用户12671031

所以现在有一种解决方法,使用 John Sully 创建的模块。它适用于 Redis 和 KeyDB,允许您使用 V8 JIT 引擎,该引擎比 Lua 脚本运行更快速且可以运行复杂脚本。https://github.com/JohnSully/ModJS

2020-06-10 23:21:24
用户462398
用户462398

我偶然发现了这个帖子,也对基准测试结果很感兴趣。我写了一个简单的 Ruby 脚本来比较它们。脚本使用不同选项在同一个键上执行简单的“SET / GET”操作。

require "redis"

def elapsed_time(name, &block)
  start = Time.now
  block.call
  puts "#{name} - elapsed time: #{(Time.now-start).round(3)}s"
end

iterations = 100000
redis_key = "test"

redis = Redis.new

elapsed_time "Scenario 1: From client" do
  iterations.times { |i|
    redis.set(redis_key, i.to_s)
    redis.get(redis_key)
  }
end

eval_script1 = <<-LUA
redis.call("SET", "#{redis_key}", ARGV[1])
return redis.call("GET", "#{redis_key}")
LUA

elapsed_time "Scenario 2: Using EVAL" do
  iterations.times { |i|
    redis.eval(eval_script1, [redis_key], [i.to_s])
  }
end

elapsed_time "Scenario 3: Using EVALSHA" do
  sha1 = redis.script "LOAD", eval_script1
  iterations.times { |i|
    redis.evalsha(sha1, [redis_key], [i.to_s])
  }
end

eval_script2 = <<-LUA
for i = 1,#{iterations} do
  redis.call("SET", "#{redis_key}", tostring(i))
  redis.call("GET", "#{redis_key}")
end
LUA

elapsed_time "Scenario 4: Inside EVALSHA" do
  sha1 = redis.script "LOAD", eval_script2
  redis.evalsha(sha1, [redis_key], [])
end

eval_script3 = <<-LUA
for i = 1,2*#{iterations} do
  redis.call("SET", "#{redis_key}", tostring(i))
  redis.call("GET", "#{redis_key}")
end
LUA

elapsed_time "Scenario 5: Inside EVALSHA with 2x the operations" do
  sha1 = redis.script "LOAD", eval_script3
  redis.evalsha(sha1, [redis_key], [])
end

我在我的 MacBook Pro 上得到了以下结果

Scenario 1: From client - elapsed time: 11.498s
Scenario 2: Using EVAL - elapsed time: 6.616s
Scenario 3: Using EVALSHA - elapsed time: 6.518s
Scenario 4: Inside EVALSHA - elapsed time: 0.241s
Scenario 5: Inside EVALSHA with 2x the operations - elapsed time: 0.5s

总的来说:

  • 方案 1 与方案 2 显示主要贡献者是往返时间,因为方案 1 需要向 Redis 提交 2 个请求,而方案 2 只需 1 个,并且方案 1 的执行时间约为方案 2 的 2 倍。
  • 方案 2 与方案 3 表明 EVALSHA 确实提供了一些好处,我相信这些好处会随着脚本复杂度的增加而增加。
  • 方案 4 与方案 5 显示调用脚本的开销几乎可以忽略不计,因为我们将操作数量增加了一倍,执行时间增加了约 2 倍。
2021-10-07 15:05:36