我们在项目中大量使用redis,却很少停下脚步细细研究它到底是个什么东西。有人说它是nosql的一种,有人说它是缓存。redis官网中说到Redis是一个开源的基于内存的数据结构存储器,可用作数据库、缓存、消息代理(Message Broker)。提供了字符串、hash表、list、set、有序集合等非常丰富的数据结构。Redis具有内置的版本复制、Lua脚本、LRU回收、事物和多层次的磁盘持久化,还通过Redis Sentinel 实现了高可用性、通过redis cluster实现自动分区。
总的来说,Redis是一个基于内存实现的key-value形式的存储结构。接下来,小方想从管道技术、发布订阅、事物这几个点入手学习。
redis中使用了管道技术(pipeline)来加速查询。
Redis是一个使用客户端/服务端模式的TCP服务,这也就是说一个请求要经历如下的过程:
(1)客户端发送查询请求到服务端,通常以阻塞的方式等待从socket中读取服务端的响应。
(2)服务端处理查询的命令,发送响应给客户端。
假如客户端要执行如下的四条命令,
如果像http协议一样每发一个请求都必须等待响应再发下一条,上面的例子至少需要8个tcp包。实际上对于redis来说没有这个必要,使用管道技术,客户端可以在未收到响应的时候继续发送请求,可以将多个请求一起发送,服务端将最终的结果返回。
使用管道技术,上面的例子可以优化为
使用java API测试管道技术,分别在使用管道和不使用管道的情况下进行10000次的自增操作。
public class RedisTest {
@Test
public void testRedis(){
System.out.println();
long start = System.currentTimeMillis();
usePipeline();
long end = System.currentTimeMillis();
System.out.println("usePipeline:"+(end - start));
start = System.currentTimeMillis();
withoutPipeline();
end = System.currentTimeMillis();
System.out.println("withoutPipeline:"+(end - start));
}
private static void withoutPipeline() {
try {
Jedis jedis = new Jedis("127.0.0.1", 6379);
for (int i = 0; i < 10000; i++) {
jedis.incr("test2");
}
jedis.disconnect();
} catch (Exception e) {
}
}
private static void usePipeline() {
try {
Jedis jedis = new Jedis("127.0.0.1", 6379);
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 10000; i++) {
pipeline.incr("test2");
}
pipeline.sync();
jedis.disconnect();
} catch (Exception e) {
}
}
}
测试结果:
usePipeline:4
withoutPipeline:349
差别非常明显。
作为redis的使用者,服务端如何实现无须过多的关注(其实是因为小方看不懂c),下面来看java中封装的Jedis和Pipeline。
在使用Jedis的过程中,一般情况下我们调用的都是redis.clients.jedis.Jedis这个类中的方法,以set方法为例
public String set(String key, String value) {
this.checkIsInMultiOrPipeline();
this.client.set(key, value);
return this.client.getStatusCodeReply();
}
在每次调用client的方法之前,都会进行事务和管道的检查,因为Jedis不能进行有事务的操作,带事务的连接要用redis.clients.jedis.Transaction类,同样Jedis也不能进行管道的操作,要用redis.clients.jedis.Pipeline类。也就是说Jedis中的操作都是无事务的不使用管道的???
protected void checkIsInMultiOrPipeline() {
if (this.client.isInMulti()) {
throw new JedisDataException("Cannot use Jedis when in Multi. Please use Transation or reset jedis state.");
} else if (this.pipeline != null && this.pipeline.hasPipelinedResponse()) {
throw new JedisDataException("Cannot use Jedis when in Pipeline. Please use Pipeline or reset jedis state .");
}
}
那如果要使用管道呢,调用jedis.pipelined()获取管道的实例,之后通过Pipeline操作redis。
public Pipeline pipelined() {
this.pipeline = new Pipeline();
this.pipeline.setClient(this.client);
return this.pipeline;
}
再来看Pipeline 的set方法
public Response set(String key, String value) {
this.getClient(key).set(key, value);
return this.getResponse(BuilderFactory.STRING);
}
仅仅set key-value的过程中看不出什么问题,因为两者调用的都是redis.clients.jedis.Client中的方法。那么这个时候就想两者的区别在于响应信息的读取吗?
Pipeline中也区分了两种模式,如果是事务模式(即isInMulti为true)使用ArrayList来暂存响应,不是则使用LinkedList来暂存响应。下面仅看非事务模式。
实际上,this.getResponse(BuilderFactory.STRING)这行代码并不是获取响应,而是将响应放入缓存,并返回响应的实例。
既然返回响应的方式不一样了,那么编码方式也不一样了。
Jedis jedis = new Jedis("127.0.0.1",6379);
Pipeline pipeline = jedis.pipelined();
Response response = pipeline.get("test2");
//System.out.println(response.get());//在未调用sync()之前,直接get会报JedisDataException
pipeline.sync();
System.out.println(response.get());
一定要在sync之后再读取响应,因为sync才真正将缓存的响应放到了Response中,也就是说我们可以在sync之前进行大量的操作,最后在sync一次性获取所有响应。
public void sync() {
if (this.getPipelinedResponseLength() > 0) {
List
毕竟缓存响应使用的是ArrayList或者LinkedList,存储空间有限,不宜一次性发送过多的命令。个人认为,管道技术适合于不需要关注中间结果的频繁操作,在发送完命令之后,最后一次性读取结果,仅操作单个key无需使用管道。
下次更新: