上一篇文章《redis pipeline批量处理提高性能》中我们讲到redis pipeline模式在批量数据处理上带来了很大的性能提升,我们先来回顾一下pipeline的原理,redis client与server之间采用的是请求应答的模式,如下所示:

Client: command1
Server: response1
Client: command2
Server: response2

在这种情况下,如果要完成10个命令,则需要20次交互才能完成。因此,即使redis处理能力很强,仍然会受到网络传输影响,导致吞吐量上不去。而在管道(pipeline)模式下,多个请求可以变成这样:

Client: command1,command2…
Server: response1,response2…

在这种情况下,完成命令只需要2次交互。这样网络传输上能够更加高效,加上redis本身强劲的处理能力,给数据处理带来极大的性能提升。但实际上遇到的问题是,项目上所用到的是Redis集群,初始化的时候使用的类是JedisCluster而不是Jedis。去查了JedisCluster的文档,并没有发现提供有像Jedis一样的获取Pipeline对象的 pipelined()方法。

为什么RedisCluster无法使用pipeline?

我们知道,Redis 集群的键空间被分割为 16384 个槽(slot),集群的最大节点数量也是 16384 个。每个主节点都负责处理 16384 个哈希槽的其中一部分。具体的redis命令,会根据key计算出一个槽位(slot),然后根据槽位去特定的节点redis上执行操作。如下所示:

master1(slave1): 0~5460
master2(slave2):5461~10922
master3(slave3):10923~16383

集群有三个master节点组成,其中master1分配了 0~5460的槽位,master2分配了 5461~10922的槽位,master3分配了 10923~16383的槽位。

一次pipeline会批量执行多个命令,那么每个命令都需要根据“key”运算一个槽位(JedisClusterCRC16.getSlot(key)),然后根据槽位去特定的机器执行命令,也就是说一次pipeline操作会使用多个节点的redis连接,而目前JedisCluster是无法支持的。

如何基于JedisCluster扩展pipeline?

设计思路

1.首先要根据key计算出此次pipeline会使用到的节点对应的连接(也就是jedis对象,通常每个节点对应一个Pool)。

2.相同槽位的key,使用同一个jedis.pipeline去执行命令。

3.合并此次pipeline所有的response返回。

4.连接释放返回到池中。

也就是将一个JedisCluster下的pipeline分解为每个单节点下独立的jedisPipeline操作,最后合并response返回。具体实现就是通过JedisClusterCRC16.getSlot(key)计算key的slot值,通过每个节点的slot分布,就知道了哪些key应该在哪些节点上。再获取这个节点的JedisPool就可以使用pipeline进行读写了。

实现上面的过程可以有很多种方式,本文将介绍一种也许是代码量最少的一种解决方案。

解决方案

上面提到的过程,其实在JedisClusterInfoCache对象中都已经帮助开发人员实现了,但是这个对象在JedisClusterConnectionHandler中为protected并没有对外开放,而且通过JedisCluster的API也无法拿到JedisClusterConnectionHandler对象。所以通过下面两个类将这些对象暴露出来,这样使用getJedisPoolFromSlot就可以知道每个key对应的JedisPool了。

Maven依赖

<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

JedisClusterPipeline

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster; import java.util.Set; public class JedisClusterPipeline extends JedisCluster {
public JedisClusterPipeline(Set<HostAndPort> jedisClusterNode, int connectionTimeout, int soTimeout, int maxAttempts, String password, final GenericObjectPoolConfig poolConfig) {
super(jedisClusterNode,connectionTimeout, soTimeout, maxAttempts, password, poolConfig);
super.connectionHandler = new JedisSlotAdvancedConnectionHandler(jedisClusterNode, poolConfig,
connectionTimeout, soTimeout ,password);
} public JedisSlotAdvancedConnectionHandler getConnectionHandler() {
return (JedisSlotAdvancedConnectionHandler)this.connectionHandler;
} /**
* 刷新集群信息,当集群信息发生变更时调用
* @param
* @return
*/
public void refreshCluster() {
connectionHandler.renewSlotCache();
}
}

JedisSlotAdvancedConnectionHandler

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisSlotBasedConnectionHandler;
import redis.clients.jedis.exceptions.JedisNoReachableClusterNodeException; import java.util.Set; public class JedisSlotAdvancedConnectionHandler extends JedisSlotBasedConnectionHandler { public JedisSlotAdvancedConnectionHandler(Set<HostAndPort> nodes, GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout,String password) {
super(nodes, poolConfig, connectionTimeout, soTimeout, password);
} public JedisPool getJedisPoolFromSlot(int slot) {
JedisPool connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
// It can't guaranteed to get valid connection because of node
// assignment
return connectionPool;
} else {
renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state
connectionPool = cache.getSlotPool(slot);
if (connectionPool != null) {
return connectionPool;
} else {
throw new JedisNoReachableClusterNodeException("No reachable node in cluster for slot " + slot);
}
}
}
}

编写测试类,向redis集群写入10000条数据,分别测试调用普通JedisCluster模式和调用上面实现的JedisCluster Pipeline模式的性能对比,测试类如下:

import redis.clients.jedis.*;
import redis.clients.util.JedisClusterCRC16;
import java.io.UnsupportedEncodingException;
import java.util.*; public class PipelineTest {
public static void main(String[] args) throws UnsupportedEncodingException {
PipelineTest client = new PipelineTest();
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("node1",20249));
nodes.add(new HostAndPort("node2",20508));
nodes.add(new HostAndPort("node3",20484));
String redisPassword = "123456";
//测试
client.jedisCluster(nodes,redisPassword);
client.clusterPipeline(nodes,redisPassword);
}
//普通JedisCluster 批量写入测试
public void jedisCluster(Set<HostAndPort> nodes,String redisPassword) throws UnsupportedEncodingException {
JedisCluster jc = new JedisCluster(nodes, 2000, 2000,100,redisPassword, new JedisPoolConfig());
List<String> setKyes = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
setKyes.add("single"+i);
}
long start = System.currentTimeMillis();
for(int j = 0;j < setKyes.size();j++){
jc.setex(setKyes.get(j),100,"value"+j);
}
System.out.println("JedisCluster total time:"+(System.currentTimeMillis() - start));
}
//JedisCluster Pipeline 批量写入测试
public void clusterPipeline(Set<HostAndPort> nodes,String redisPassword) {
JedisClusterPipeline jedisClusterPipeline = new JedisClusterPipeline(nodes, 2000, 2000,10,redisPassword, new JedisPoolConfig());
JedisSlotAdvancedConnectionHandler jedisSlotAdvancedConnectionHandler = jedisClusterPipeline.getConnectionHandler();
Map<JedisPool, List<String>> poolKeys = new HashMap<>();
List<String> setKyes = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
setKyes.add("pipeline"+i);
}
long start = System.currentTimeMillis();
//查询出 key 所在slot ,通过 slot 获取 JedisPool ,将key 按 JedisPool 分组
jedisClusterPipeline.refreshCluster();
for(int j = 0;j < setKyes.size();j++){
String key = setKyes.get(j);
int slot = JedisClusterCRC16.getSlot(key);
JedisPool jedisPool = jedisSlotAdvancedConnectionHandler.getJedisPoolFromSlot(slot);
if (poolKeys.keySet().contains(jedisPool)){
List<String> keys = poolKeys.get(jedisPool);
keys.add(key);
}else {
List<String> keys = new ArrayList<>();
keys.add(key);
poolKeys.put(jedisPool, keys);
}
}
//调用Jedis pipeline进行单点批量写入
for (JedisPool jedisPool : poolKeys.keySet()) {
Jedis jedis = jedisPool.getResource();
Pipeline pipeline = jedis.pipelined();
List<String> keys = poolKeys.get(jedisPool);
for(int i=0;i<keys.size();i++){
pipeline.setex(keys.get(i),100, "value" + i);
}
pipeline.sync();//同步提交
jedis.close();
}
System.out.println("JedisCluster Pipeline total time:"+(System.currentTimeMillis() - start));
}
}

测试结果如下:

JedisCluster total time:29147
JedisCluster Pipeline total time:190

结论:对于批量操作,JedisCluster Pipeline有明显的性能提升。

总结

本文旨在介绍一种在Redis集群模式下提供Pipeline批量操作的功能。基本思路就是根据redis cluster对数据哈希取模的算法,先计算数据存放的slot位置, 然后根据不同的节点将数据分成多批,对不同批的数据进行单点pipeline处理。

但是需要注意的是,由于集群模式存在节点的动态添加删除,且client不能实时感知(只有在执行命令时才可能知道集群发生变更),因此,该实现不保证一定成功,建议在批量操作之前调用 refreshCluster() 方法重新获取集群信息。应用需要保证不论成功还是失败都会调用close() 方法,否则可能会造成泄露。如果失败需要应用自己去重试,因此每个批次执行的命令数量需要控制,防止失败后重试的数量过多。

基于以上说明,建议在集群环境较稳定(增减节点不会过于频繁)的情况下使用,且允许失败或有对应的重试策略。

一种简单实现Redis集群Pipeline功能的方法及性能测试的更多相关文章

  1. Linux 下Redis集群安装部署及使用详解(在线和离线两种安装+相关错误解决方案)

    一.应用场景介绍 本文主要是介绍Redis集群在Linux环境下的安装讲解,其中主要包括在联网的Linux环境和脱机的Linux环境下是如何安装的.因为大多数时候,公司的生产环境是在内网环境下,无外网 ...

  2. 深入剖析Redis系列: Redis集群模式搭建与原理详解

    前言 在 Redis 3.0 之前,使用 哨兵(sentinel)机制来监控各个节点之间的状态.Redis Cluster 是 Redis 的 分布式解决方案,在 3.0 版本正式推出,有效地解决了 ...

  3. Linux离线安装redis集群

    一.应用场景介绍 本文主要是介绍Redis集群在Linux环境下的安装讲解,联网环境安装较为简单,这里只说脱机的Linux环境下是如何安装的.因为大多数时候,公司的生产环境是在内网环境下,无外网,服务 ...

  4. 03 . Redis集群

    Redis集群方案 Redis Cluster 集群模式通常具有 高可用.可扩展性.分布式.容错等特性.Redis分布式方案一般有两种 客户端分区方案 客户端 就已经决定数据会被 存储到哪个 redi ...

  5. [个人翻译]Redis 集群教程(中)

    上一篇:http://www.cnblogs.com/li-peng/p/6143709.html 官方原文地址:https://redis.io/topics/cluster-tutorial  水 ...

  6. (转)理想化的 Redis 集群

    一个豁达的关键是正确乐观的面对失败的系统.不需要过多的担心,需要一种去说那又怎样的能力.因此架构的设计是如此的重要.许多优秀的系统没有进一步成长的能力,我们应该做的是去使用其他的系统去共同分担工作. ...

  7. 04.redis集群+SSM整合使用

    redis集群+SSM整合使用 首先是创建redis-cluster文件夹: 因为redis最少需要6个节点(三主三从),为了更好的理解,我这里创建了两台虚拟机(192.168.0.109 192.1 ...

  8. redis集群部署及常用的操作命令(上)

    简单说下自己测试搭建简单的redis集群的大体步骤: 1.首先你的有6个redis(官方说最少6个,3master,3slave),可以先在一台机器上搭建,搭建到多台上应该只需要改变启动命令即可(可能 ...

  9. redis集群部署及常用的操作命令_01

    简单说下自己测试搭建简单的redis集群的大体步骤: 1.首先你的有6个redis(官方说最少6个,3master,3slave),可以先在一台机器上搭建,搭建到多台上应该只需要改变启动命令即可(可能 ...

随机推荐

  1. SOCKET CAN的理解

    转载请注明出处:http://blog.csdn.net/Righthek 谢谢! CAN总线原理 由于Socket CAN涉及到CAN总线协议.套接字.Linux网络设备驱动等.因此,为了能够全面地 ...

  2. WinForm GroupBox控件重绘外观

    private void groupBoxFun_Paint(PaintEventArgs e, GroupBox groupBox){ e.Graphics.Clear(groupBox.BackC ...

  3. 《Java练习题》进阶练习题(五)

    编程合集: https://www.cnblogs.com/jssj/p/12002760.html 前言:不仅仅要实现,更要提升性能,精益求精,用尽量少的时间复杂度和空间复杂度解决问题. [程序88 ...

  4. 《Java基础知识》Java类的定义及其实例化

    类必须先定义才能使用.类是创建对象的模板,创建对象也叫类的实例化. 下面通过一个简单的例子来理解Java中类的定义: public class Dog { String name; int age; ...

  5. css应用视觉设计

    应用视觉设计:创建一个 CSS 线性渐变 HTML元素的背景色并不局限于单色.css还提供了颜色过渡,也就是渐变.可以通过background里面的linear-gradient()来实现线性渐变,下 ...

  6. 批量SSH key-gen无密码登陆认证脚本

    SSH key-gen无密码登录认证脚本 使用为了让linux之间使用ssh不需要密码,可以采用了数字签名RSA或者DSA来完成.主要使用ssh-key-gen实现. 通过 ssh-key-gen 来 ...

  7. Vue基础系列(五)——Vue中的指令(中)

    写在前面的话: 文章是个人学习过程中的总结,为方便以后回头在学习. 文章中会参考官方文档和其他的一些文章,示例均为亲自编写和实践,若有写的不对的地方欢迎大家和我一起交流. VUE基础系列目录 < ...

  8. sqlserver数据库批量插入-SqlBulkCopy

    当想在数据库中插入大量数据时,使用insert 不仅效率低,而且会导致一系列的数据库性能问题 当使用insert语句进行插入数据时.我使用了两种方式: 每次插入数据时,都只插入一条数据库,这个会导致每 ...

  9. Linux 按 Ctrl + S 卡死的解决办法

    ctrl + s 的作用是暂停屏幕输出 ctrl + q 恢复屏幕输出即可 恢复之后会出现在暂停期间输入的字符

  10. mongodb-API

    mongodb-API 连接mongo(该操作一般在初始化时就执行) 出现 由于目标计算机积极拒绝,无法连接的错误时 查看是否进行虚拟机的端口转发 将 /etc/ 目录下的mongodb.conf 文 ...