有些时候,Redis实例需要装载大量用户在短时间内产生的数据,数以百万计的keys需要被快速的创建。我们称之为大量数据插入(mass insertion),本文档的目标就是提供如下信息:Redis如何尽可能快的处理数据。
本文参考网上方案特意整理总结如下:
使用正常模式的Redis 客户端执行大量数据插入不是一个好主意:因为一个个的插入会有大量的时间浪费在每一个命令往返时间上。使用管道(pipelining)是一种可行的办法,但是在大量插入数据的同时又需要执行其他新命令时,这时读取数据的同时需要确保请可能快的的写入数据。
只有一小部分的客户端支持非阻塞输入/输出(non-blocking I/O),并且并不是所有客户端能以最大限度的提高吞吐量的高效的方式来分析答复。
例如,如果我们需要生成一个10亿的`keyN -> ValueN’的大数据集,我们会创建一个如下的redis命令集的文件:
SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN
从Redis 2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。
使用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协议文档请参考Redis协议说明。 但是为了生成大量数据插入的目标,你需要了解每一个细节协议,每个命令会用如下方式表示:
*
$
...
这里的是”\r”(或者是ASCII的13)、是”\n”(或者是ASCII的10)。
例如:命令SET key value协议格式如下:
*3
$3
SET
$3
key
$5
value
或表示为引用字符串:
"*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"
你需要将大量插入数据的命令按照上面的方式一个接一个的生成到文件。
dis-难点是保证recli在pipe mode模式下执行和netcat一样快的同时,如何能理解服务器发送的最后一个回复。
这是通过以下方式获得:
redis-cli –pipe试着尽可能快的发送数据到服务器。
读取数据的同时,解析它。
一旦没有更多的数据输入,它就会发送一个特殊的ECHO命令,后面跟着20个随机的字符。我们相信可以通过匹配回复相同的20个字符是同一个命令的行为。
一旦这个特殊命令发出,收到的答复就开始匹配这20个字符,当匹配时,就可以成功退出了。
同时,在分析回复的时候,我们会采用计数器的方法计数,以便在最后能够告诉我们大量插入数据的数据量。
1.3.1准备数据文件,格式是文本文件,名称是:redis_commands.txt。
我在Windows环境下生成了一个txt文件,一条数据一行,代码如下:
SET Key0 Value0
SET Key1 Value1
SET Key2 Value2
SET Key3 Value3
SET Key4 Value4
SET Key5 Value5
SET Key6 Value6
SET Key7 Value7
SET Key8 Value8
SET Key9 Value9
SET Key10 Value10
...
SET KeyN ValueN
public class getStringTest {
/**
* 格式化成输入字符串
*/
private String getString(String... args) {
StringBuilder sb = new StringBuilder();
sb.append("*").append(args.length).append("\r\n");
for (String arg : args) {
sb.append("$").append(arg.length()).append("\r\n");
sb.append(arg).append("\r\n");
}
return sb.toString();
}
@Test
public void initFile2() {
Long startTime = System.currentTimeMillis();
String file = "d:\\d.txt";
BufferedWriter w = null;
StringBuilder sb = new StringBuilder();
try {
w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "utf-8"));
for(int i=0 ;i < 10000000;i++){
//for (int i = 1; i <= 100; i++) {
if (i / 3 == 0) {
w.flush();
}
sb.setLength(0);
sb.append(this.getString("set", "u" + i, "name" + i));
//sb.append(this.getString("hmset", "usr" + i, "userid", "usr" + i, "username", "usrname" + i));
w.append(sb.toString());
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
w.flush();
w.close();
} catch (IOException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("耗时: "+(endTime - startTime)/1000+" s。");
}
}
我生成了1000万的数据,因为这个文本文件我是在Windows环境下生成的,所以需要格式转换。
1.3.2如果使用Windows环境下生成的文件,需要进行格式转换,如果是在Linux环境下生成的文件就不需要格式转换,如果文本文件比较大,执行转换时间会有几秒,等待即可。
执行格式转换
[root@linux ~]# unix2dos redis_commands.txt
unix2dos:converting file redis_commands.txt to DOS format ...
以上代码进行格式转换完毕
需要说明一点,unix2dos这个命令需要先安装,如果没有安装,会提示:command not found。
执行以下命令安装:
[root@linux ~]# yum install unix2dos
1.3.3进行数据批量插入
[root@linux ~]# cat d.txt | redis-cli -h 134.177.11.22 -p 6379 [-a "password"] -n 0 --pipe
All data transferred.Waiting for the last reply...
Last reply received from server.
errors:0,replies:10000000
我们可以采用Jedis的父类中的pipelined()方法获取管道,它可以实现一次性发送多条命令并一次性返回结果,这样就大量的减少了客户端与Redis的通信次数,可以有效的提高程序效率(但是,因为Redis要一次性返回所有结果,它会把这些结果都缓存起来,因此命令越多,缓存消耗的内存也会越大,具体还要视情况而定).此外Pipeline的原理是队列(先进先出),这样也保证了数据的顺序性。
public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("127.0.0.1", 6474);
Pipeline p = jedis.pipelined();
p.setex("key_a", 120, "11111");
p.setex("key_b", 120, "2222");
p.sync();
if (jedis != null && jedis.isConnected()) {
jedis.close();
}
}
public void saveDataToRedis(Map<String, String> map) {
redisTemplate.executePipelined(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
map.forEach((key, value) -> connection.set(redisTemplate.getKeySerializer().serialize(key), redisTemplate.getValueSerializer().serialize(value)));
return null;
}
});
}
参考文章
https://blog.csdn.net/sod5211314/article/details/86088469
https://blog.csdn.net/AsWinn/article/details/84784725
https://blog.csdn.net/supersub000/article/details/80100016
https://blog.csdn.net/L_Test_passed/article/details/89239581