使用 Redis 协议批量写入数据。

前言

批量加载就是指一次性把大量的现有数据加载到 Redis 中。理想情况下,你会想要快速高效地执行这个操作。本文讲述了在 Redis 中批量加载数据的几种策略。

使用 Redis 协议

使用普通的 Redis 客户端来执行批量加载可不是一个好主意 —— 简单地重复执行插入命令是一个非常慢的过程。它之所以慢,是因为你必须要等待上一个命令执行结束才能够开始下一个命令,中间消耗了很多无效的交互时间。

管道也不是不能用,但是若要批量加载大量记录,你就必须在读取服务端答复时,不断地写新的命令,以此来保证插入尽可能快。

只有一小部分的客户端支持非阻塞 I/O,而且有些客户端不能高效解析服务端的答复,这就意味着它们不能够最大化吞吐量。

鉴于以上种种原因,其他方式都被排除。若要批量插入数据到 Redis 中,只剩下了一种最好的方式:生成一个包含 Redis 协议的纯文本文件,保留内容的原始格式,然后调用相应的命令来插入需要的数据。

举个例子,如果我想要生成一个很大的数据集,这个数据集包含了几百万的键值对(keyN -> valueN),该怎么做呢?嗯,我会先创建一个文件,文件内容是下面这些遵循 Redis 协议格式的命令:

SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN

一旦文件创建成功,接下来的操作就是把它喂给 Redis,越快越好。在以前,我们通常是 netcat 来喂数据的,就像下面这样:

(cat data.txt; sleep 10) | nc localhost 6379 > /dev/null

不过,就批量导入而言,这个方式不大可靠,因为 netcat 无法知道数据何时传输完毕,也不能够检查传输是否出错。

在 Redis 2.6 及之后的版本中,redis-cli 工具支持一个叫做**管道模式pipe mode**的新模式,它是专门为执行批量加载而设计的。

使用这个管道模式后,你需要运行的命令就像下面这样:

cat data.txt | redis-cli --pipe

这会产生一个类似下面的输出:

All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000000

此外,redis-cli 工具还会确保只把 Redis 实例产生的错误重定向到标准输出。

生成 Redis 协议

Redis 协议非常易于生成和解析,你可以在 这里 查看相关文档。不过,如果你只是为了实现批量加载的话,就不需要理解协议的每一个细节了,你只需要知道下面这几个命令就行:

*<args><cr><lf>
$<len><cr><lf>
<arg0><cr><lf>
<arg1><cr><lf>
...
<argN><cr><lf>

其中,<cr> 的意思是 \r(对应的 ASCII 码为 13),lf 的意思是 \n(对应的 ASCII 码为 10)。

例如,SET key value 这个命令可以用下面的协议表示:

*3<cr><lf>
$3<cr><lf>
SET<cr><lf>
$3<cr><lf>
key<cr><lf>
$5<cr><lf>
value<cr><lf>

或者你也可以使用双引号包裹它们,用单个字符串来表示:

"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"

为了批量加载而生成的文件就是由符合上面的格式的命令组成的,一条接着一条。

下面的这个 Ruby 函数可以用来生成有效的协议:

def gen_redis_proto(*cmd)
    proto = ""
    proto << "*"+cmd.length.to_s+"\r\n"
    cmd.each{|arg|
        proto << "$"+arg.to_s.bytesize.to_s+"\r\n"
        proto << arg.to_s+"\r\n"
    }
    proto
end

puts gen_redis_proto("SET","mykey","Hello World!").inspect

有了上面的函数,你就可以很轻松地生成上面例子中的键值对了。你可以这样做:

(0...1000).each{|n|
    STDOUT.write(gen_redis_proto("SET","Key#{n}","Value#{n}"))
}

我们可以直接运行这个程序,然后把它的输出通过管道喂给 redis-cli。这样一来,第一个批量加载会话就执行完毕了。

$ ruby proto.rb | redis-cli --pipe
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 1000

管道模式的工作原理

redis-cli 的管道模式真神奇!它的神奇之处就在于,既能达到和 netcat 一样快速,又能同时理解 server 的上一个回复。

而这是通过下面的方式实现的:

  • redis-cli --pipe 试图尽可能快地发送数据到 server。
  • 同时,它也在尽可能地读取数据,并试图解析。
  • 一旦标准输入中没有更多数据的可读取,它就会发送一个特殊的 ECHO 命令,附带一个 20 字节大小的随机字符串。这样一来,我们就可以确定这是最新发送的命令,也可以确定我们能够在收到同样的 20 字节时就开始匹配回复响应。当收到了匹配的回复,它就可以成功退出了。

有了这个技巧,我们不需要解析发送到 server 的协议来知道将发送多少条命令,只需要简单解析响应就好了。

然而,当解析响应时,我们使用了一个计数器来记录解析完成数量。这样,我们才能最终告诉用户此次批量加载的会话中发送的命令数量。

后语

读了本文,你是否学会了 Redis 批量加载数据的最佳技巧呢?在下一篇文章中,我将介绍 Redis 的另一个用例 —— 分布式锁。请保持关注喔!

参考


本文使用 CC BY-SA 4.0 国际协议 进行许可,欢迎 遵照协议规定 转载。
作者:六开箱
链接:https://lkxed.github.io/posts/redis-bulk-loading/