最详细的JedisCluster实现Pipeline的文章

Redis Cluster Java客户端Pipeline实现方案
本文围绕Redis Cluster的Java客户端Pipeline展开。因Redis 6.0发布,从Codis迁移到Redis Cluster,而JedisCluster无Pipeline模式。介绍了lettuce和Redisson实现Pipeline的情况,分析了JedisCluster不支持的原因,最终自定义ConnectionHandler和ClusterPipeline实现了集群版Pipeline,并进行了性能测试。

前言

2020年4月30日,Redis 6.0.0正式发布,标志着redis从此告别单线程。在此之前,在大数据生产环境中使用的是一个30个节点的Codis集群,SparkStreaming以此作为缓存,QPS高峰大概在2000w/s。

因为Codis不再更新迭代,于是在Redis 6.0.6版本发布的时候搭建了Redis Cluster,新的应用将不再使用Codis。之前连接Codis使用的Java客户端是Jedis,通过Pipeline方式批次执行命令,以此来提高效率。而Redis Cluster的客户端JedisCluster没有提供Pipeline方式,只能单条执行命令,于是开始考虑其他的Java客户端。

这里备选了两个客户端:lettuceRedisson

pipeline原理

这里先说一下Jedis的pipeline的原理。通过pipeline对redis的所有操作命令,都会先放到一个List中,当pipeline直接执行或者通过jedis.close()调用sync()的时候,所有的命令都会一次性地发送到客户端,并且每个操作命令返回一个response,通过get来获取操作结果。

lettuce

lettuce提供了async异步方式来实现pipeline的功能,来测试一下是否可按批次处理命令。

测试代码:

public static void main(String[] args) throws Exception {
        RedisURI uri = RedisURI.builder()
                .withHost("47.102.xxx.xxx")
                .withPassword("Redis6.0.6".toCharArray())
                .withPort(10001)
                .build();
        RedisClusterClient client = RedisClusterClient.create(uri);
        StatefulRedisClusterConnection<String, String> connect = client.connect();
        RedisAdvancedClusterAsyncCommands<String, String> async = connect.async();
        // 断点1
        async.set("key1", "v1");
        Thread.sleep(1000 * 3);
        // 断点2
        async.set("key2", "v2");
        // 断点3
        async.flushCommands();
        Thread.sleep(1000 * 3);
        connect.close();
        client.shutdown();
}

在程序中设置三个断点。如果是pipeline的话,只有执行完断点3,两条set命令才会执行。
运行结果:
运行结果
结果表明还未到flushCommands(),第一个set命令已经执行。到这你可能就会以为lettuce其实还是逐条命令执行,只是开启了异步请求模式。其实不然,在lettuce异步操作中,默认开启了命令自动刷新功能,所以给你的假象还是逐条执行,在此需要禁用自动刷新来开启pipeline功能。

在set()之前加上一行代码:

async.setAutoFlushCommands(false);

运行结果:
运行结果

Redisson

redisson提供了batch来实现pipeline的功能。

测试代码:

 Config config = new Config();
 config.useClusterServers()
       .addNodeAddress("redis://47.102.219.86:10001")
       .setPassword("Redis@6.0.6");
 RedissonClient redisson = Redisson.create(config);
 RBatch batch = redisson.createBatch();
 String key = "test";
 for (int i = 1; i < 3; i++) {
      batch.getMap(key + i).putAsync(String.valueOf(i), String.valueOf(i));
	}
 // 打上断点
 batch.execute();
 redisson.shutdown();

这里我们在execute()处打上断点,debug运行程序。
运行结果:
执行结果
结果表明Redisson会将命令放在一个batch中,当执行execute()时,会将命令一次性发送到redis执行。虽然Redisson实现了pipeline的功能,但是我最后还是放弃了它。原因很简单,它的方法不像jedis和lettuce一样简单明了,和redis的操作命令相差太多,导致使用起来比较繁琐。

Jedis Cluster Pipeline

原因

开头也提到了,Jedis对Redis Cluster提供了JedisCluster客户端,但是没有Pipeline模式,那么JedisCluster为什么不支持Pipeline?

在redis中一共有16384个Slot,每个节点负责一部分Slot,当对Key进行操作时,redis会通过CRC16计算出key对应的Slot,将Key映射到Slot所在节点上执行操作。

因为不同Key映射的节点不同,所以JedisCluster需要持有Redis Cluster每个节点的连接才能执行操作,而Pipeline是面向于一个redis连接的执行模式,所以JedisCluster无法支持Pipeline。

那么我们自己有没有办法利用JedisCluster去封装一个具有Pipeline模式的客户端?

思路

刚刚提到,JedisCluster会持有Redis Cluster所有节点的连接。那么,如果我们可以获取到所有节点的连接,对每个节点的连接都开启Pipeline。首先计算出每个Key所在的Slot,再找到Slot对应节点,就可以将Key放到对应节点连接的Pipeline上,这样不就实现了集群版的Pipeline了么!

我们要做的工作就是找到对应关系,将每个Key分配到对应的节点连接中。

秉着不重复造轮子的观点,我们先看看JedisCluster是如何执行命令的?

JedisCluster

先写样例,并在get()处打断点。
JedisCluster
run()

CRC16

进入run(),可以看到JedisClusterCRC16提供了getSlot()方法,可以计算出Key所在的Slot
run()

run()里面调用了runWithRetries(),这是核心方法之一,Step into

// 据方法调用参数删除了部分代码
private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
    Jedis connection = null;
    try {
    	// false
    	if (tryRandomNode) {
      		connection = connectionHandler.getConnection();
    	} else {
	    	// 重点:从方法名看,是根据slot来获取jedis连接!!
      		connection = connectionHandler.getConnectionFromSlot(slot);
    	}
      	return execute(connection);
    } catch (JedisNoReachableClusterNodeException jnrcne) {
      throw jnrcne;
    } catch (JedisConnectionException jce) {
      // 释放连接
      releaseConnection(connection);
      connection = null;
      if (attempts <= 1) {
      	// 刷新slots
        this.connectionHandler.renewSlotCache();
      }
      return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
    } 
  }

从runWithRetries()可以看到,JedisCluster通过调用getConnectionFromSlot(slot)来获取jedis连接,这里实现了Slot和Jedis的关系

那么connectionHandler为什么可以提供redis连接?

connectionHandler

查看connectionHandler变量信息

connectionHandler
可以看到它有一个JedisClusterInfoCache类型的成员变量cache,cache有两个HashMap类型的成员变量nodes和slots,nodes保存节点和JedisPool的映射关系,slots保存16384个slot和JedisPool的映射关系,这里slot和节点实现了映射关系

接着看一下getConnectionFromSlot()

getConnectionFromSlot()
getSlotPool()
可以看出,cache调用getSlotPool(),从成员变量slots中通过slot取到了相应节点的JedisPool。

简单的画一下流程图:

流程图

至此,所有轮子都已经具备,开始造车。
Pipeline

实现Pipeline

我们只要获取到connectionHandler变量,就可以使用它的成员变量cache来获取Jedis。

connectionHandler是JedisCluster的成员变量,在其父类BinaryJedisCluster中找到了此变量。
BinaryJedisCluster
cache是connectionHandler的成员变量,在其父类JedisClusterConnectionHandler找到了此变量。
JedisClusterConnectionHandler
connectionHandler和cache都是protected变量,外部类无法直接访问,所以需要定义子类访问变量。

自定义ConnectionHandler

目的:使用cache保存的Cluster信息,用其来获取JedisPool。

public class JedisSlotConnectionHandlerImp extends JedisSlotBasedConnectionHandler implements Serializable {
    public JedisSlotConnectionHandlerImp(Set<HostAndPort> nodes, GenericObjectPoolConfig poolConfig, int connectionTimeout, int soTimeout, String password) {
        super(nodes, poolConfig, connectionTimeout, soTimeout, password);
    }

    // 自定义通过slot获取JedisPool的方法
    // 为了保证后面一个JedisPool只取一个Jedis
    public JedisPool getJedisPoolFromSlot(int slot) {
        JedisPool jedisPool = cache.getSlotPool(slot);
        if (jedisPool != null) {
            return jedisPool;
        } else {
            renewSlotCache();
            jedisPool = cache.getSlotPool(slot);
            if (jedisPool != null) {
                return jedisPool;
            } else {
                throw new JedisNoReachableClusterNodeException("No reachable node in cluster for slot " + slot);
            }
        }
    }
}

自定义ClusterPipeline

目的:使用connectionHandler来建立key、slot以及JedisPool之间关系映射

public class JedisClusterPipeline extends JedisCluster implements Serializable {
    // 覆盖父类中的connectionHandler
    protected JedisSlotConnectionHandlerImp connectionHandler;
    public JedisClusterPipeline(HashSet node, int connectionTimeout, int soTimeout, int maxAttempts, String password, GenericObjectPoolConfig poolConfig) {
        super(node, connectionTimeout, soTimeout, maxAttempts, password, poolConfig);
        connectionHandler = new JedisSlotConnectionHandlerImp(node, poolConfig, connectionTimeout, soTimeout, password);
    }
	// 通过key转换成slot,再获取JedisPool
    public JedisPool getJedisPoolFromSlot(String key) {
        return connectionHandler.getJedisPoolFromSlot(JedisClusterCRC16.getSlot(key));
    }
}

使用

使用自定义的JedisClusterPipeline,需要自己实现set、get、hget等方法来覆盖父类JedisCluster对应的方法。最初的目的是应用于Spark将维度信息存入Redis Cluster,当时是用scala面向RDD的partition实现了集群版的hmset()方法。

这里临时用Java实现一下Pipeline的set()方法。

实现set()

public class JedisClusterPipelineCommand {
    /**
     * 自定义的pipeline模式set方法
     * @param key 存放的key
     * @param value 存放的value
     * @param clusterPipeline 用来获取JedisPool
     * @param pipelines 建立JedisPool和pipeline映射,保证一个JedisPool只开启一个pipeline
     * @param jedisMap 建立pipeline和Jedis映射,用来释放Jedis
     * @param nums 记录每个pipeline放入key的条数
     * @param threshold pipeline进行sync的阈值
     */
    public static void setByPipeline(String key, String value, JedisClusterPipeline clusterPipeline, ConcurrentHashMap<JedisPool, Pipeline> pipelines, ConcurrentHashMap<Pipeline, Jedis> jedisMap,  ConcurrentHashMap<Pipeline, Integer> nums, int threshold) {
        JedisPool jedisPool = clusterPipeline.getJedisPoolFromSlot(key);
        // 查看对应节点是否已经开启了pipeline
        Pipeline pipeline = pipelines.get(jedisPool);
        if (pipeline == null) {
            Jedis jedis = jedisPool.getResource();
            pipeline = jedis.pipelined();
            // 构建映射关系,保证每个节点只有一个jedis来开启pipeline
            jedisMap.put(pipeline, jedis);
            pipelines.put(jedisPool, pipeline);
            nums.put(pipeline, 0);
        }else {
            int num = nums.get(pipeline);
            nums.put(pipeline, num + 1);
            if (num % threshold == 0) {
                pipeline.sync();
            }
        }
        pipeline.set(key, value);
    }

    /**
     * 释放jedis并强制pipeline sync
     */
    public static void releaseConnection(ConcurrentHashMap<Pipeline, Jedis> jedisMap) {
        for (Jedis jedis : jedisMap.values()) {
            jedis.close();
        }
    }
}

执行类

    public static void main(String[] args) throws Exception {
        JedisPoolConfig config = new JedisPoolConfig();
        HashSet jedisClusterNodes = new java.util.HashSet<HostAndPort>();
        jedisClusterNodes.add(new HostAndPort("47.102.xxx.xx", 10001));
        JedisClusterPipeline jedisClusterPipeline = new JedisClusterPipeline(jedisClusterNodes, 1000, 1000, 10, "Redis6", config);
        ConcurrentHashMap<JedisPool, Pipeline> pipelines = new ConcurrentHashMap<>();
        ConcurrentHashMap<Pipeline, Jedis> jedisMap = new ConcurrentHashMap<>();
        ConcurrentHashMap<Pipeline, Integer> nums = new ConcurrentHashMap<>();
        for (int i = 0; i < 1000; i++) {
            JedisClusterPipelineCommand.setByPipeline("k" + i, "v" + i, jedisClusterPipeline, pipelines, jedisMap, nums, 100 );
        }
        JedisClusterPipelineCommand.releaseConnection(jedisMap);
    }

执行结果
执行结果

性能测试

本机环境1000条数据

  • pipeline模式:2.32s

  • JedisCluster:68.6s

Spark on Yarn 128w条 Hash

  • 1Core 1G Pipeline:18s

本机环境测试结果受限于网络和主机配置,仅供比较参考。

结语

最后选择自己实现pipeline,首先是因为比较了解pipeline的原理,说白了就是用习惯了。其次是在本机测试letttuce时,出现了一些意料之外的问题,目前还在探索中。下一步的工作就是慢慢的将Pipeline其他的方法实现,逐步优化,用于生产。

在这里插入图片描述

<think> 首先,用户要求提供关于JedisCluster的相关信息,如使用方法、特性、配置等。用户引用了几个站内引用,我需要参考这些内容。 回顾引用内容: - 引用[1]:展示了JedisCluster的初始化代码。 - 引用[2]:描述了JedisCluster作为高级客户端的结构和功能。 - 引用[3]:展示了JedisCluster的初始化代码,并提到它封装了连接池。 - 引用[4]:解释了JedisCluster的线程安全性和初始化过程,提到了节点和槽位的对应关系。 - 引用[5]:讨论了JedisCluster在批量插入时的性能问题,比较了普通方式和Pipeline方式。 用户的问题:"我想获取关于JedisCluster的相关信息,如使用方法、特性、配置等" 系统级指令: - 所有行内数学表达式用$...$,但这里可能没有数学内容。 - 独立公式用$$...$$,但可能不适用。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾自然地添加引用标识,如[^1]。 回答结构: - 结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 内容组织: 1. **介绍JedisCluster**:从引用中看,JedisClusterRedis集群的高级客户端,封装了集群操作。 2. **特性**:基于引用: - 线程安全:通过JedisPool实现(引用[4])。 - 封装集群操作:计算槽位和节点对应关系(引用[3])。 - 实现多个接口:提供单键、多键和脚本命令(引用[2])。 3. **使用方法**: - 初始化:需要提供节点集、超时等参数(引用[1]、[3]、[4])。 - 代码示例:从引用中提取。 4. **配置**: - 使用GenericObjectPoolConfig配置连接池(引用[3]、[4])。 - 参数如连接超时、重试次数等(引用[4])。 5. **性能考虑**: - 批量操作时,使用Pipeline提高效率(引用[5])。 生成相关问题: - 在回答末尾,添加相关问题。 回答结构: - 开头介绍JedisCluster。 - 分点讲解:特性、使用方法、配置。 - 代码示例。 - 性能优化。 - 相关问题。 添加引用标识: - 在相关句子末尾自然地添加,如[^1]、[^2]等。 确保中文回答。 终回答大纲: 1. **JedisCluster简介**:简述什么是JedisCluster,基于引用。 2. **特性**: - 高级客户端,封装集群操作[^2]。 - 线程安全,通过JedisPool实现[^4]。 - 支持多种命令接口[^2]。 3. **使用方法**: - 初始化步骤。 - 代码示例(参考引用[1]或[3])。 4. **配置选项**: - 连接池配置(GenericObjectPoolConfig)[^3]。 - 超时、重试次数等参数[^4]。 5. **性能优化**: - 使用Pipeline处理批量操作以提高效率[^5]。 6. **相关问题**:生成几个相关问题。 相关问题建议: 1. 如何优化JedisCluster的性能? 2. JedisClusterRedis集群中的工作原理是什么? 3. 如何处理JedisCluster中的节点故障? 现在,编写回答。 </think> ### JedisCluster相关信息 JedisCluster是Jedis客户端库中专门为Redis集群设计的高级包装类,它封装了集群操作的复杂性,提供了便捷的接口访问分布式Redis环境。基于您提供的引用信息,我将从特性、使用方法、配置等方面逐步解析JedisCluster的核心内容,确保内容真实可靠。 #### 1. **特性** JedisCluster具备以下关键特性,这些特性使其成为高效访问Redis集群的工具: - **高级客户端封装**:JedisCluster继承自BinaryJedisCluster,并实现了多个命令接口(如JedisCommands、MultiKeyJedisClusterCommands),支持单键操作、多键操作和Lua脚本执行,简化了开发者调用集群命令的流程[^2]。 - **线程安全设计**:由于Jedis本身非线程安全,JedisCluster内部使用JedisPool(连接池)来管理连接实例,确保多线程环境下的安全性。每个节点对应一个JedisPool实例,资源复用避免了线程冲突[^4]。 - **自动槽位映射**:JedisCluster在初始化时自动计算Redis集群的槽位(slot)与节点的对应关系,无需手动处理键的分布。底层通过JedisClusterInfoCache类缓存这些映射信息,实现高效的键值路由[^3][^4]。 - **集群感知能力**:它通过连接集群中的任意节点获取整个集群的元数据(如节点列表、槽位分配),从而动态适应集群拓扑变化,支持故障转移[^2][^3]。 - **性能优化支持**:支持Pipeline批量操作,显著提升写入或读取效率(如引用[5]显示,Pipeline方式比普通方式快数百倍)。 #### 2. **使用方法** JedisCluster的核心使用方法围绕初始化对象和命令调用展开。以下是一个典型代码示例,基于引用[1]、[3]和[4],演示如何初始化和执行基本操作: ```java import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.util.GenericObjectPoolConfig; import java.util.HashSet; import java.util.Set; public class JedisClusterExample { public static void main(String[] args) { // 步骤1: 初始化节点集(至少一个节点) Set<HostAndPort> clusterNodes = new HashSet<>(); clusterNodes.add(new HostAndPort("127.0.0.1", 7379)); // 替换为实际节点IP和端口 // 步骤2: 配置连接池(可选,推荐优化性能) GenericObjectPoolConfig<Jedis> poolConfig = new GenericObjectPoolConfig<>(); poolConfig.setMaxTotal(200); // 设置大连接数 poolConfig.setMaxIdle(50); // 设置空闲连接数 // 步骤3: 创建JedisCluster实例 JedisCluster jedisCluster = new JedisCluster( clusterNodes, 2000, // 连接超时时间(毫秒) 2000, // 读写超时时间(毫秒) 5, // 大重试次数 "password", // 集群密码(若有) poolConfig // 连接池配置 ); // 步骤4: 执行命令(示例:写入和读取) jedisCluster.set("key", "value"); // 写入操作 String result = jedisCluster.get("key"); // 读取操作 System.out.println(result); // 输出: value // 步骤5: 关闭资源(建议在应用退出时调用) jedisCluster.close(); } } ``` **关键点说明**: - **初始化参数**: - `Set<HostAndPort>`:集群节点集,至少提供一个节点地址(如Redis服务器的IP和端口),JedisCluster会自动发现其他节点[^1][^3]。 - 超时参数:连接超时和读写超时(默认2000毫秒),用于避免网络阻塞。 - 重试次数:操作失败时的重试上限(默认5次),处理临时故障[^4]。 - 密码和连接池配置:支持认证和资源优化[^3][^4]。 - **命令执行**:直接调用`set()`、`get()`等方法,JedisCluster内部处理键的槽位计算和路由到正确节点。 - **资源管理**:使用后调用`close()`释放连接池资源,防止内存泄漏[^3]。 #### 3. **配置选项** JedisCluster的配置主要通过`GenericObjectPoolConfig`和构造函数参数实现,核心配置项包括: - **连接池配置**: - `setMaxTotal(int)`: 设置大连接数,避免资源耗尽(推荐根据负载调整,如200-500)[^3][^4]。 - `setMaxIdle(int)`: 设置大空闲连接数,提升性能(如50)。 - `setMinIdle(int)`: 设置小空闲连接数,减少初始化延迟。 这些配置直接影响并发性能和稳定性。 - **集群参数**: - `timeout`:连接和读写超时(毫秒),建议设为2000-5000以适应网络波动[^4]。 - `maxAttempts`:操作失败的大重试次数,默认5次,在高可用场景可增加[^4]。 - `password`:可选,用于认证集群节点[^3]。 - **高级配置**:通过`JedisCluster`的构造函数自定义重定向策略或SSL设置(较少使用)。 #### 4. **性能优化建议** 针对JedisCluster的性能瓶颈(如引用[5]提到的批量写入效率问题),可以采用以下优化策略: - **Pipeline批量操作**:JedisCluster支持Pipeline模式,将多个命令打包发送,减少网络往返开销。示例: ```java try (Pipeline pipeline = jedisCluster.pipelined()) { for (int i = 0; i < 1000; i++) { pipeline.set("key" + i, "value" + i); } pipeline.sync(); // 批量执行 } ``` 测试显示,Pipeline方式比单条命令写入快数百倍[^5]。 - **连接池调优**:增大`MaxTotal`以适应高并发,并监控连接池状态避免资源泄漏。 - **错误处理**:捕获`JedisCluster`异常(如`JedisClusterMaxAttemptsException`),实现重试逻辑。 - **集群监控**:集成Redis监控工具(如Prometheus),追踪槽位迁移和节点健康。 #### 总结 JedisCluster是一个强大的Redis集群客户端,通过自动槽位管理、连接池线程安全和简洁API简化了分布式Redis操作。初始化时需提供节点信息和配置参数,日常使用中优先采用Pipeline优化性能。实际应用中,参考官方文档和性能测试调整配置,能显著提升可靠性和效率[^2][^5]。 如果您有特定场景(如高并发或故障恢复),可以进一步探讨优化细节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值