Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。
这意味着通常情况下一个请求会遵循以下步骤:
- 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
- 服务端处理命令,并将结果返回给客户端。
因此,例如下面是4个命令序列执行情况:
- Client: INCR X
- Server: 1
- Client: INCR X
- Server: 2
- Client: INCR X
- Server: 3
- Client: INCR X
- Server: 4
客户端和服务器通过网络进行连接。这个连接可以很快(loopback接口)或很慢(建立了一个多次跳转的网络连接)。无论网络延如何延时,数据包总是能从客户端到达服务器,并从服务器返回数据回复客户端。
这个时间被称之为 RTT (Round Trip Time - 往返时间). 当客户端需要在一个批处理中执行多次请求时很容易看到这是如何影响性能的(例如添加许多元素到同一个list,或者用很多Keys填充数据库)。例如,如果RTT时间是250毫秒(在一个很慢的连接下),即使服务器每秒能处理100k的请求数,我们每秒最多也只能处理4个请求。
如果采用loopback接口,RTT就短得多(比如我的主机ping 127.0.0.1只需要44毫秒),但它任然是一笔很多的开销在一次批量写入操作中。
幸运的是有一种方法可以改善这种情况。
Redis 管道(Pipelining)
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
下面使用Java对Redis进行批量操作,分别看下使用和不使用Pipeline下,性能如何,笔者已set get为例,hmset和hmget类似。
首先创建一个maven项目,pom.xml文件中引入jedis的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
批量写入数据
package pipeline;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
public class BatchOperSet {
private static final String HOST = "192.168.191.65";
private static final int PORT = 6379;
// 批量插入数据到Redis,正常使用
public static void batchSetNotUsePipeline() throws Exception {
Jedis jedis = new Jedis(HOST, PORT);
String keyPrefix = "normal";
long begin = System.currentTimeMillis();
for (int i = 1; i < 10000; i++) {
String key = keyPrefix + "_" + i;
String value = String.valueOf(i);
jedis.set(key, value);
}
jedis.close();
long end = System.currentTimeMillis();
System.out.println("not use pipeline batch set total time:" + (end - begin));
}
// 批量插入数据到Redis,使用Pipeline
public static void batchSetUsePipeline() throws Exception {
Jedis jedis = new Jedis(HOST, PORT);
Pipeline pipelined = jedis.pipelined();
String keyPrefix = "pipeline";
long begin = System.currentTimeMillis();
for (int i = 1; i < 10000; i++) {
String key = keyPrefix + "_" + i;
String value = String.valueOf(i);
pipelined.set(key, value);
}
pipelined.sync();
jedis.close();
long end = System.currentTimeMillis();
System.out.println("use pipeline batch set total time:" + (end - begin));
}
public static void main(String[] args) {
try {
batchSetNotUsePipeline();
batchSetUsePipeline();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
not use pipeline batch set total time:2679
use pipeline batch set total time:39
批量获取数据
package pipeline;
import java.util.HashMap;
import java.util.Map;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
public class BatchOperGet {
private static final String HOST = "192.168.191.65";
private static final int PORT = 6379;
// 批量从Redis中获取数据,正常使用
public static Map<String, String> batchGetNotUsePipeline() throws Exception {
Map<String, String> map = new HashMap<String, String>();
Jedis jedis = new Jedis(HOST, PORT);
String keyPrefix = "normal";
long begin = System.currentTimeMillis();
for (int i = 1; i < 10000; i++) {
String key = keyPrefix + "_" + i;
String value = jedis.get(key);
map.put(key, value);
}
jedis.close();
long end = System.currentTimeMillis();
System.out.println("not use pipeline batch get total time:" + (end - begin));
return map;
}
// 批量从Redis中获取数据,使用Pipeline
public static Map<String, String> batchGetUsePipeline() throws Exception {
Map<String, String> map = new HashMap<String, String>();
Jedis jedis = new Jedis(HOST, PORT);
Pipeline pipelined = jedis.pipelined();
String keyPrefix = "pipeline";
// 使用pipeline方式批量获取数据,只能获取到value值,对应的key获取不到,我们通过一个中间map来获取key
HashMap<String, Response<String>> intrmMap = new HashMap<String, Response<String>>();
long begin = System.currentTimeMillis();
for (int i = 1; i < 10000; i++) {
String key = keyPrefix + "_" + i;
intrmMap.put(key, pipelined.get(key));
}
pipelined.sync();
jedis.close();
for (Map.Entry<String, Response<String>> entry :intrmMap.entrySet()) {
Response<String> sResponse = (Response<String>)entry.getValue();
String key = new String(entry.getKey());
String value = sResponse.get();
map.put(key, value);
}
long end = System.currentTimeMillis();
System.out.println("use pipeline batch get total time:" + (end - begin));
return map;
}
public static void main(String[] args) {
try {
batchGetNotUsePipeline();
batchGetUsePipeline();
// Map<String, String> normalMap = batchGetNotUsePipeline();
// for(Map.Entry<String, String> entry : normalMap.entrySet()) {
// System.out.println(entry.getKey() + "=" + entry.getValue());
// }
// Map<String, String> pipelineMap = batchGetUsePipeline();
// for(Map.Entry<String, String> entry : pipelineMap.entrySet()) {
// System.out.println(entry.getKey() + "=" + entry.getValue());
// }
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
not use pipeline batch get total time:2990
use pipeline batch get total time:41
总结:
需要对Redis进行批量操作时,建议使用Redis 管道(Pipeline),这样可以大幅提升性能
问题记录
连接 Redis 报错
redis.clients.jedis.exceptions.JedisConnectionException: java.net.ConnectException: Connection refused: connect
at redis.clients.jedis.Connection.connect(Connection.java:207)
at redis.clients.jedis.BinaryClient.connect(BinaryClient.java:93)
at redis.clients.jedis.Connection.sendCommand(Connection.java:126)
at redis.clients.jedis.BinaryClient.set(BinaryClient.java:110)
at redis.clients.jedis.Client.set(Client.java:47)
at redis.clients.jedis.Jedis.set(Jedis.java:120)
at pipeline.BatchOperByPipeline.batchSetNotUsePipeline(BatchOperByPipeline.java:28)
at pipeline.BatchOperByPipeline.main(BatchOperByPipeline.java:13)
Caused by: java.net.ConnectException: Connection refused: connect
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:345)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at redis.clients.jedis.Connection.connect(Connection.java:184)
... 7 more
解决:保证windows下可以telnet ip 6307
(1).redis.conf中将属性protected-mode设置为no,默认为yes,redis开启了保护模式
(2).注释掉redis.conf文件中的如下一行,默认redis只能在本机访问
# bind 127.0.0.1
(3).重新启动Redis服务,连接成功!