本文由 简悦 SimpRead 转码, 原文地址 www.toutiao.com
Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储。大多数开发人员可能听说过 redis 可以运行 Lua 脚本,但是可能不知道 r
Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储。大多数开发人员可能听说过 redis 可以运行 Lua 脚本,但是可能不知道 redis 在什么情况下需要使用到 Lua 脚本。
- 可以遵循这个链接中的方法在操作系统上安装 Redis
- 如果你对 redis 命令不熟悉,查看《Redis 命令引用》
简而言之:Lua 脚本带来性能的提升。
- 很多应用的服务任务包含多步 redis 操作以及使用多个 redis 命令,这时你可以使用 Redis 结合 Lua 脚本,会为你的应用带来更好的性能。
- 另外包含在一个 Lua 脚本里面的 redis 命令具备原子性,当你面对高并发场景下的 redis 数据库操作时,可以有效避免多线程操作产生脏数据。
说了那么多,Lua 不会怎么办?不要慌!Lua 其实很简单,如果你曾经学习过任何一门编程语言,学习 Lua 都非常简单。下面给大家举几个例子学习一下:
Lua 脚本通过各种语言的 redis 客户端都可以调用,我们就简单一点使用 redis-cli 看下面的 redis 命令行:
|
|
EVAL 命令行后面跟着的是 Lua 脚本:“redis.call(‘set’, KEYS[1], ARGV[1])”, 放到编程语言里面就是一段字符串,跟在 Lua 脚本字符串后面的三个参数依次是:
- redis Lua 脚本所需要的 KEYS 的数量 ,只有一个 KEYS[1],所以紧跟脚本之后的参数值是 1
- Lua 脚本需要的参数 KEYS[1] 的参数值,在我们的例子中值为 key:name
- Lua 脚本需要的参数 ARGV[1] 的参数值,在我们的例子中值为 value
Lua 脚本中包括两组参数:KEYS[] 和 ARGV[] ,两个数组下标从 1 开始。一个值得去遵守的最佳实践是:把 redis 操作所需的 key 通过 KEYS 进行参数传递,其他的 Lua 脚本所需的参数通过 ARGV 进行传递。
上面的脚本执行完成之后,我们使用下面的 Lua 脚本来进行验证,如果该脚本的返回值是”value”,与我们之前设置的 key:name 的值相同,则表示我们的 Lua 脚本被正确执行了。
|
|
我们的第一个 Lua 脚本只包含一条语句,调用 redis.call
|
|
所以在 Lua 脚本里面可以通过 redis.call 执行 redis 命令,call 方法的第一个参数就是 redis 命令的名称,因为我们调用的是 redis 的 set 命令,所以需要传递 key 和 value 两个参数。
我们第二个脚本不只是执行了一个脚本,因为执行 get 命令还返回了执行结果。注意脚本中有一个 return 关键字。
|
|
当然如果只是上面的这么简单的 Lua 脚本,还不如直接使用命令行更方便。我们实际使用到的 Lua 脚本会比上面的复杂,上面的 Lua 脚本只是一个 Hello World。
我曾使用 Lua 脚本从一个 hash map 里面按照一定的顺序获取若干 key 对应的值。对应的顺序在一个 zset 排序集合中进行保存,数据设置及排序可以通过下面的完成。
|
|
如果不知道 hmset 和 zadd 命令的作用,可以参考 hmset 和 zadd
执行下面的 Lua 脚本
|
|
你将看到如下的输出结果
|
|
- 通过 zrange 取出 order 集合里面的数据,即:[key:3 , key:1 , key:2]
- 然后通过 unpack 函数将 [key:3 , key:1 ,key:2] 转成 key:3 key:1 key:2
- 最后执行 hmget hkeys key:3 key:1 key:2,所以得到上面的输出结果
Redis 可以对 Lua 脚本进行预加载,可以通过 script load 命令把 Lua 脚本预加载到 redis 里面。
|
|
预加载完成之后,你会看到下面的一段输出
|
|
这是一个具有唯一性的 hash 字符串,这个 hash 就代表着我们刚刚预加载的 Lua 脚本,我们可以通过 EVALSHA 命令执行该脚本。如:
|
|
执行的结果与下面的是一致的。
|
|
有些开发人员有的时候可能会将 JSON 数据保存在 Redis 里面,我们先不说这样做是不是一种好的方式,我们只来谈一下如何通过 Lua 脚本修改 JSON 数据。
正常情况下,你需要修改一个 JSON Object,你需要把它从 redis 里面查询回来,解析它,修改 key 值,然后再将它序列化保存到 redis 里面。这样做有几个问题:
- 高并发场景下无法保证原子性,另一个线程可以在当前线程获取和设置 Object 操作之间更改这个 JSON 数据。在这种情况下,将丢失更新。
- 性能问题。如果您经常进行这样的更改并且 JSON 数据相当大,这可能会成为应用的性能瓶颈。因为你经常性地进行取数据,存数据。
通过在 Lua 中实现上面逻辑,因为 redis 的 Lua 脚本是在服务端执行的,一方面可以保证操作的原子性,解决高并发丢失更新的问题,另一方面节省网络传输同时提升性能。
下面我们向 redis 里面保存一个测试 JSON 字符串:obj
|
|
现在,让我们运行我们的脚本:
|
|
- local obj = redis.call(“get”,KEYS[1]); 其中 KEYS[1]=obj,所以返回值 obj= ‘{“a”:“foo”,“b”:“bar”}’
- local obj2 = string.gsub(obj,"(" .. ARGV[1] .. “":)([^,}]+)”,"%1" .. ARGV[2]);, .. 是 Lua 脚本的字符串连接符号;我们使用 RegEx 模式来匹配密钥并替换其值,如果对表达式不熟悉,自行补课;"%1" 表示第一个被匹配的子串,"%1" .. ARGV[2] 等于 “b”:“bar2”, 并使用 gsub 进行替换。
- 最后将结果返回,obj 的 JSON 对象的结果如下:
|
|
我建议只有在你能证明它能带来更好的性能时才使用 Lua 脚本。如果你只是想要保证 redis 操作原子性,那么可以使用 transactions 事务。不一定非要使用 Lua 脚本。
此外 redis Lua 脚本不应太长。因为当脚本运行时相当于为被操作对象加锁,其他操作都在等待它完成。如果 Lua 脚本需要相当长的时间执行,则可能会导致瓶颈而不是提高性能。Lua 脚本在达到超时后停止(默认情况下为 5 秒)。