Redis学习一:管道技术

Redis学习一:管道技术

  • 简介
  • Redis管道技术
    • 总结
  • Redis发布订阅
  • Redis事物
  • 面试中常见的问题

简介

我们在项目中大量使用redis,却很少停下脚步细细研究它到底是个什么东西。有人说它是nosql的一种,有人说它是缓存。redis官网中说到Redis是一个开源的基于内存的数据结构存储器,可用作数据库、缓存、消息代理(Message Broker)。提供了字符串、hash表、list、set、有序集合等非常丰富的数据结构。Redis具有内置的版本复制、Lua脚本、LRU回收、事物和多层次的磁盘持久化,还通过Redis Sentinel 实现了高可用性、通过redis cluster实现自动分区。
总的来说,Redis是一个基于内存实现的key-value形式的存储结构。接下来,小方想从管道技术、发布订阅、事物这几个点入手学习。

Redis管道技术

redis中使用了管道技术(pipeline)来加速查询。
Redis是一个使用客户端/服务端模式的TCP服务,这也就是说一个请求要经历如下的过程:
(1)客户端发送查询请求到服务端,通常以阻塞的方式等待从socket中读取服务端的响应。
(2)服务端处理查询的命令,发送响应给客户端。
假如客户端要执行如下的四条命令,

  • Client: INCR X
  • Server: 1
  • Client: INCR X
  • Server: 2
  • Client: INCR X
  • Server: 3
  • Client: INCR X
  • Server: 4

如果像http协议一样每发一个请求都必须等待响应再发下一条,上面的例子至少需要8个tcp包。实际上对于redis来说没有这个必要,使用管道技术,客户端可以在未收到响应的时候继续发送请求,可以将多个请求一起发送,服务端将最终的结果返回。
使用管道技术,上面的例子可以优化为

  • Client: INCR X
  • Client: INCR X
  • Client: INCR X
  • Client: INCR X
  • Server: 1
  • Server: 2
  • Server: 3
  • Server: 4

使用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)这行代码并不是获取响应,而是将响应放入缓存,并返回响应的实例。
Redis学习一:管道技术_第1张图片
既然返回响应的方式不一样了,那么编码方式也不一样了。

		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 unformatted = this.client.getAll();
            Iterator var2 = unformatted.iterator();

            while(var2.hasNext()) {
                Object o = var2.next();
                this.generateResponse(o);
            }
        }

    }
 
  

总结

毕竟缓存响应使用的是ArrayList或者LinkedList,存储空间有限,不宜一次性发送过多的命令。个人认为,管道技术适合于不需要关注中间结果的频繁操作,在发送完命令之后,最后一次性读取结果,仅操作单个key无需使用管道。

下次更新:

Redis发布订阅

Redis事物

面试中常见的问题

  1. 为什么单线程的redis会认为比多线程的数据库操作快?
  2. 高并发环境中如何解决资源竞争问题
  3. redis适用的场景,优缺点
  4. redis与memcached
  5. redis的事物
  6. redis的缓存失效策略和主键失效机制

你可能感兴趣的:(缓存,redis,redis,pipeline,管道技术)