Redis的Pipeline和事务(进阶篇)

目录

1、Pipeline 

2、事务

2-1、Redis的事务原理

2-2、Redis的watch命令

2-3、Pipeline和事务的区别


1、Pipeline 

前面我们已经说过,Redis客户端执行一条命令分为如下4个部分

1)发送命令

2)命令排队

3)命令执行

4)返回结果

 

  • 其中1和4花费的时间称为Round Trip Time (RTT,往返时间),也就是数据在网络上传输的时间。
  • Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。
  • 但大部分命令是不支持批量操作的,例如要执行n次 hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。
  • Redis的客户端和服务端可能部署在不同的机器上。例如客户端在本地,Redis服务器在阿里云的广州,两地直线距离约为800公里
  • 那么1次RTT时间 = 800 x 2 / ( 300000 × 2/3 ) = 8毫秒,(光在真空中传输速度为每秒30万公里,这里假设光纤为光速的2/3 )。而Redis命令真正执行的时间通常在微秒(1000微妙=1毫秒)级别,所以才会有Redis 性能瓶颈是网络这样的说法。
  • Pipeline(流水线)机制能改善上面这类问题,它能将一组 Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端,没有使用Pipeline执行了n条命令,整个过程需要n次RTT。

 使用Pipeline 执行了n次命令,整个过程需要1次RTT。

 Pipeline并不是什么新的技术或机制,很多技术上都使用过。而且RTT在不同网络环境下会有不同,例如同机房和同机器会比较快,跨机房跨地区会比较慢。

redis-cli的--pipe选项实际上就是使用Pipeline机制,但绝对部分情况下,我们使用Java语言的Redis客户端中的Pipeline会更多一点。

代码演示:

// set方法,为了验证方便,所以 keys 与 values 的值都是提前设置好对应关系的
public void plSet(List<String> keys, List<String> values) {
    if(keys.size()!=values.size()) {
        throw new RuntimeException("key和value个数不匹配!");
    }
    Jedis jedis = null;
    try {
        jedis = jedisPool.getResource();

        // pipe是将所有的命令组装成pipeline
        Pipeline pipelined = jedis.pipelined();

        // 遍历key值,并传入对应val
        for(int i=0;i<keys.size();i++){
            pipelined.set(keys.get(i),values.get(i));
        }
        pipelined.sync();
    } catch (Exception e) {
        throw new RuntimeException("执行Pipeline设值失败!",e);
    } finally {
        jedis.close();
    }
}

// get方法
public List<Object> plGet(List<String> keys) {
    Jedis jedis = null;
    try {
        jedis = jedisPool.getResource();

        // pipe是将所有的命令组装成pipeline
        Pipeline pipelined = jedis.pipelined();

        // 不是仅仅是get方法,set方法还要很多很多方法pipeline都提供了支持
        for(String key:keys){
            pipelined.get(key);
        }
        return pipelined.syncAndReturnAll();//这里只会向redis发送一次
    } catch (Exception e) {
        throw new RuntimeException("执行Pipeline获取失败!",e);
    } finally {
        jedis.close();
    }
}
// 验证对比 非Pipeline和Pipeline 执行结果
@Test
public void testPipeline() {
    long setStart = System.currentTimeMillis();
    for (int i = 0; i < TEST_COUNT; i++) { //单个的操作
        redisString.set("testStringM:key_" + i, String.valueOf(i));
    }
    long setEnd = System.currentTimeMillis();
    System.out.println("非pipeline操作"+TEST_COUNT+"次字符串数据类型set写入,耗时:" + (setEnd - setStart) + "毫秒");

    List<String> keys = new ArrayList<>(TEST_COUNT);
    List<String> values= new ArrayList<>(TEST_COUNT);
    for (int i = 0; i < keys.size(); i++) {
        keys.add("testpipelineM:key_"+i);
        values.add(String.valueOf(i));
    }
    long pipelineStart = System.currentTimeMillis();
    redisPipeline.plSet(keys,values);
    long pipelineEnd = System.currentTimeMillis();
    System.out.println("pipeline操作"+TEST_COUNT+"次字符串数据类型set写入,耗时:" + (pipelineEnd - pipelineStart) + "毫秒");
}

运行结果

差距有100多倍,可以得到如下两个结论:

  1. Pipeline执行速度一般比逐条执行要快。
  2. 客户端和服务端的网络延时越大,Pipeline的效果越明显。

Pipeline虽然好用,但是每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成,比如可以将Pipeline的总发送大小控制在内核输入输出缓冲区大小之内或者控制在单个TCP 报文最大值1460字节之内。

内核的输入输出缓冲区大小一般是4K-8K,不同操作系统会不同(当然也可以配置修改)

最大传输单元(Maximum Transmission Unit,MTU),这个在以太网中最大值是1500字节。那为什么单个TCP 报文最大值是1460,因为因为还要扣减20个字节的IP头和20个字节的TCP头,所以是1460

同时Pipeline只能操作一个Redis实例,但是即使在分布式Redis场景中,也可以作为批量操作的重要优化手段。

2、事务

  • 简单地说,事务表示一组动作,要么全部执行,要么全部不执行。
  • 例如在社交网站上用户A关注了用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。
  • Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi 命令代表事务开始,exec命令代表事务结束。另外discard命令是回滚。

A客户端:

 B客户端:

在事务没有提交的时,是查不到数据的

只有在事务提交后,才可以查到数据

可以看到sadd命令此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中的一个缓存队列(所以discard也只是丢弃这个缓存队列中的未执行命令,并不会回滚已经操作过的数据,这一点要和关系型数据库的Rollback操作区分开)。

只有当exec执行后,用户A关注用户B的行为才算完成,如下所示exec返回的两个结果对应sadd命令。

但是要注意Redis的事务功能很弱。在事务回滚机制上,Redis只能对基本的语法错误进行判断。

如果事务中的命令出现错误,Redis 的处理机制也不尽相同。

1、语法命令错误,例如:下面操作错将set写成了sett,属于语法错误,会造成整个事务无法执行,事务内的操作都没有执行

2、运行时错误,例如:事务内第一个命令简单的设置一个string类型,第二个对这个key进行sadd命令,这种就是运行时命令错误,因为语法是正确的,可以看到Redis并不支持回滚功能,第一个set命令已经执行成功,开发人员需要自己修复这类问题。

2-1、Redis的事务原理

事务是Redis实现在服务器端的行为,用户执行 multi 命令时,服务器会将对应这个用户的客户端对象设置为一个特殊的状态,在这个状态下后续用户执行的查询命令不会被真的执行,而是被服务器缓存起来,直到用户执行 exec 命令为止,服务器会将这个用户对应的客户端对象中缓存的命令按照提交的顺序依次执行。

2-2、Redis的watch命令

有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis 提供了watch命令来解决这类问题。

A客户端设置 test 为123 

此时B客户端更新了 test 为 666 

 

A客户端继续执行命令

 

可以看到 A客户端 在执行 multi 之前执行了 watch 命令,B客户端A客户端 执行 exec 之前修改了 key 值,造成 A客户端 事务没有执行(exec结果为nil),所以使用 watch 命令,可以在 A客户端 在修改或添加此 key 时,发现有没有其他客户端也在使用该 key,从而影响 exec 是否会执行成功

2-3、Pipeline和事务的区别

PipeLine看起来和事务很类似,感觉都是一批批处理,但两者还是有很大的区别:

  • pipeline是客户端的行为,对于服务器来说是透明的,可以认为服务器无法区分客户端发送来的查询命令是以普通命令的形式还是以pipeline的形式发送到服务器的;
  • 事务则是实现在服务器端的行为,用户执行 multi 命令时,服务器会将对应这个用户的客户端对象设置为一个特殊的状态,在这个状态下后续用户执行的查询命令不会被真的执行,而是被服务器缓存起来,直到用户执行 exec 命令为止,服务器会将这个用户对应的客户端对象中缓存的命令按照提交的顺序依次执行。
  • 应用pipeline可以提服务器的吞吐能力,并提高Redis处理查询请求的能力。

但是这里存在一个问题,当通过pipeline提交的查询命令数据较少,可以被内核缓冲区所容纳时,Redis可以保证这些命令执行的原子性。然而一旦数据量过大,超过了内核缓冲区的接收大小,那么命令的执行将会被打断,原子性也就无法得到保证。因此pipeline只是一种提升服务器吞吐能力的机制,如果想要命令以事务的方式原子性的被执行,还是需要事务机制,或者使用更高级的脚本功能以及模块功能。

  • 可以将事务和pipeline结合起来使用,减少事务的命令在网络上的传输时间,将多次网络IO缩减为一次网络IO。

Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了Redis 的“keep it simple”的特性,下篇文章会介绍Lua脚本同样可以实现事务的相关功能,但是功能要强大很多。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值