Redis 不仅可以通过 redis-cli 完成惊人的事情,还可以与您选择的语言进行更多交互。但在某些情况下,客户端/服务器架构无法高效或安全地实现某些行为——逻辑需要在数据库层运行。这就是 Lua 发挥作用的地方。Lua 作为脚本语言内置于 Redis 中。通过 Lua,您可以在 Redis 中原子地执行代码,而无需客户端与服务器之间的传输开销。
一个这样的任务示例是向哈希字段追加值。虽然 Redis 可以轻松地使用 APPEND 命令向字符串键追加值,但没有专门的命令用于向哈希字段追加值。虽然您可以尝试通过客户端获取字段的值,将新字符串追加到该值并重置哈希字段来实现,但这是一个糟糕的主意。由于这不是原子操作,因此在您向哈希字段追加值时,另一个客户端可能会介入并更改它,然后原始客户端会覆盖新的更新。
客户端 #1 | 客户端 #2 | |
1 | ><a href="https://redis.ac.cn/commands/hget" target="_blank" rel="noopener">HGET myhash myfield “hello” | |
2 | [客户端将“ world” 追加到“hello”] | ><a href="https://redis.ac.cn/commands/hset" target="_blank" rel="noopener">HSET myhash myfield goodbye 0 |
3 | > HSET myhash myfield “hello world” 0 | |
4 | > HGET myhash myfield “hello world” |
如您在第 2 行所见,对“goodbye”的更新丢失了。我们可以使用 Lua 助手来解决这个问题,并消除将值移动到客户端的发送/接收开销。
在任何文本编辑器中,我们需要创建 Lua 脚本。我们将其命名为 ‘happend.lua’
local original = redis.call('HGET',KEYS[1],ARGV[1]) return redis.call('HSET',KEYS[1], ARGV[1], original .. ARGV[2])
在第一行中,我们创建了一个名为 original 的局部变量,它将存储作为第一个传递的键存储的哈希键的当前值,字段是第一个非键参数。重要的是要理解 Lua 脚本在执行时会区分键和非键参数。
在第二行中,我们在同一个键和字段上调用 HSET,然后将原始值与第二个非键参数进行连接。这个值会被返回给 Redis,因此我们将保留 HSET 的原始返回值。
虽然您可以使用 EVAL 命令直接执行 Lua 脚本,但这可能会变得相当混乱和低效。Redis 有一个内置的脚本缓存,可以预加载脚本,然后通过脚本的 SHA1 哈希摘要来引用它。要从命令行加载脚本,我们将使用 cat 和 redis-cli。
$ redis-cli -a yourRedisPassword SCRIPT LOAD "$(cat ./happend.lua)" "d30c7f6d0c23fcfe6d4630b11f4e3db4cb2db099"
(注意:如果您的脚本即使只有一个字符不同,您也会得到一个完全不同的哈希字符串)
现在,在 redis-cli 中,我们可以使用 EVALSHA 来调用我们的脚本并执行追加操作
> HSET mynewhash greeting "Hello" (integer) 1 > EVALSHA d30c7f6d0c23fcfe6d4630b11f4e3db4cb2db099 1 mynewhash greeting " world" (integer) 0 > HGET mynewhash greeting "Hello world"
让我们分解 EVALSHA 命令。第一个参数是我们使用 SCRIPT LOAD 创建的脚本的 SHA1 摘要。第二个参数是键的数量。在这种情况下,我们操作单个键,所以它是 1。第三个参数是我们正在操作的键。第四个参数是我们想要操作的字段,最后第五个参数是我们追加到字段的值。
由于追加操作是在 Lua 脚本内部发生的,因此上述场景不会发生。Lua 脚本以完全同步和原子方式执行。
虽然 Lua 可以成为一个非常有用的问题解决方案,但应该谨慎使用。脚本确实会阻塞服务器,并可能导致数据库无响应。在分片场景中,脚本会尽量将所有操作保持在单个服务器上,以避免跨分片错误。