Redis --- 使用 Pipeline 实现批处理操作

在正常情况下,我们每次发送 Redis 命令时,客户端会等待 Redis 服务器的响应,直到接收到结果后,才会发送下一个命令。这种方式虽然保证了操作的顺序性,但在执行大量命令时会产生很大的网络延迟。

通过 Pipeline 技术,我们的客户端可以将多个命令同时发送给 Redis 服务器,并且不需要等待每个命令的返回结果,直到所有命令都被执行完毕,客户端再一起获取返回值。这样能减少每个命令的等待时间,大幅提高执行效率。

Redis Pipeline 是一种优化 Redis 操作的机制,通过将多个命令打包发送到 Redis 服务器,减少客户端与服务器之间的网络往返时间(RTT),从而显著提升性能。

在默认情况下,Redis 客户端与服务器之间的通信是请求-响应模式,即:

  1. 客户端发送一个命令到服务器。

  2. 服务器执行命令并返回结果。

  3. 客户端等待响应后再发送下一个命令。

这种模式在命令数量较少时没有问题,但在需要执行大量命令时,网络往返时间(RTT)会成为性能瓶颈。所以我们需要实现下面目的:

  • 将多个命令打包发送到服务器。

  • 服务器依次执行这些命令,并将结果一次性返回给客户端。

  • 减少网络开销,提升性能。

以下是一个简单的 Java 示例,展示了如何使用 Jedis(Redis 的一个 Java 客户端)执行 Pipeline:

注意:批处理时不建议一次携带太多命令,并且Pipeline的多个命令之间不具备原子性。

java 复制代码
// 创建 Jedis 实例
Jedis jedis = new Jedis("localhost", 6379);

// 使用 pipelining 方式批量执行命令
Pipeline pipeline = jedis.pipelined();

// 批量操作:使用 pipeline 来缓存命令
for (int i = 0; i < 1000; i++) {
    pipeline.set("key" + i, "value" + i);
}

// 同步执行所有命令
pipeline.sync();
  • pipelined() 方法: 创建一个 Pipeline 对象,它缓存所有要执行的命令。
  • 批量设置命令: 通过 pipeline.set() 将多个 SET 命令放入管道中,但命令并不会立即执行。
  • sync() 方法: 通过调用 sync() 方法,客户端将会把所有缓存的命令一次性发送给 Redis,并等待它们完成执行。

但是这些都是在单机模式下的批处理,那对于集群来说该如何使用呢?

向MSet或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如何Redis是一个集群,那批处理命令的多个key必须落在同一个插槽中,否则就会导致执行失败。

一般推荐使用并行插槽来解决,如果使用hash_tag,可能会出现大量的key分配同一插槽导致数据倾斜,而并行插槽不会。

那么这里我们模拟一下并行插槽实现:

将多个键值对按照Redis集群的槽位进行分组,然后分别使用jedisCluster.mset()方法按组设置键值对。

java 复制代码
public class JedisClusterTest {

    // 声明一个JedisCluster对象,用于与Redis集群进行交互
    private JedisCluster jedisCluster;

    // 在每个测试方法执行之前,初始化JedisCluster连接
    @BeforeEach
    void setUp() {
        // 配置Jedis连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(8);
        poolConfig.setMaxIdle(8);
        poolConfig.setMinIdle(0);
        poolConfig.setMaxWaitMillis(1000);

        // 创建一个HashSet,用于存储Redis集群的节点信息
        HashSet<HostAndPort> nodes = new HashSet<>();
        // 添加Redis集群的节点信息(IP和端口)
        nodes.add(new HostAndPort("192.168.150.101", 7001));
        nodes.add(new HostAndPort("192.168.150.101", 7002));
        nodes.add(new HostAndPort("192.168.150.101", 7003));
        nodes.add(new HostAndPort("192.168.150.101", 8001));
        nodes.add(new HostAndPort("192.168.150.101", 8002));
        nodes.add(new HostAndPort("192.168.150.101", 8003));

        // 使用配置的连接池和节点信息初始化JedisCluster对象
        jedisCluster = new JedisCluster(nodes, poolConfig);
    }

    // 测试方法:使用mset命令一次性设置多个键值对
    @Test
    void testMSet() {
        // 使用JedisCluster的mset方法,一次性设置多个键值对
        // 但是jedisCluster默认是无法解决批处理问题的,需要我们手动解决
        jedisCluster.mset("name", "Jack", "age", "21", "sex", "male");
    }

    // 测试方法:使用mset命令按槽位分组设置多个键值对
    @Test
    void testMSet2() {
        // 创建一个HashMap,用于存储多个键值对
        Map<String, String> map = new HashMap<>(3);
        map.put("name", "Jack");
        map.put("age", "21");
        map.put("sex", "Male");

        // 将map中的键值对按照Redis集群的槽位进行分组
        Map<Integer, List<Map.Entry<String, String>>> result = map.entrySet()
                .stream()
                .collect(Collectors.groupingBy(
                        // 使用ClusterSlotHashUtil计算每个键对应的槽位
                        entry -> ClusterSlotHashUtil.calculateSlot(entry.getKey()))
                );

        // 遍历按哈希槽分组后的结果
        for (List<Map.Entry<String, String>> list : result.values()) {
            // 创建一个数组用于批量设置Redis的键值对
            String[] arr = new String[list.size() * 2];  // 每个键值对包含两个元素
            int j = 0;  // 索引变量,用于在数组中定位位置
            for (int i = 0; i < list.size(); i++) {
                j = i << 1;  // 通过位移计算数组中的位置
                Map.Entry<String, String> e = list.get(i);  // 获取当前的键值对
                arr[j] = e.getKey();  // 将键放入数组中
                arr[j + 1] = e.getValue();  // 将值放入数组中
            }
            // 批量设置Redis集群中的键值对
            jedisCluster.mset(arr);
        }
    }

    // 在每个测试方法执行之后,关闭JedisCluster连接
    @AfterEach
    void tearDown() {
        // 如果JedisCluster对象不为空,则关闭连接
        if (jedisCluster != null) {
            jedisCluster.close();
        }
    }
}

而在Redis集群环境下,如果需要批量获取多个键的值,可以使用multiGet方法。multiGetRedisTemplate提供的一个方法,用于一次性获取多个键的值。然而,需要注意的是,multiGet在集群环境下要求所有键必须位于同一个槽位(slot),否则会抛出异常。

java 复制代码
@Service
public class RedisService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 跨槽位批量获取多个键的值
     */
    public Map<String, Object> batchGetCrossSlot(List<String> keys) {
        // 按槽位分组
        Map<Integer, List<String>> slotKeyMap = keys.stream()
                .collect(Collectors.groupingBy(ClusterSlotHashUtil::calculateSlot));

        // 存储最终结果
        Map<String, Object> result = new HashMap<>();

        // 对每个槽位的键分别调用multiGet
        for (Map.Entry<Integer, List<String>> entry : slotKeyMap.entrySet()) {
            List<String> slotKeys = entry.getValue();
            List<Object> slotValues = redisTemplate.opsForValue().multiGet(slotKeys);

            // 将结果存入Map
            for (int i = 0; i < slotKeys.size(); i++) {
                result.put(slotKeys.get(i), slotValues.get(i));
            }
        }

        return result;
    }

    /**
     * 测试跨槽位批量获取方法
     */
    public void testBatchGetCrossSlot() {
        List<String> keys = Arrays.asList("name", "age", "sex");
        Map<String, Object> values = batchGetCrossSlot(keys);

        // 打印结果
        values.forEach((key, value) -> {
            System.out.println("Key: " + key + ", Value: " + value);
        });
    }
}
相关推荐
2301_8025023344 分钟前
哈工大计算机系统2025大作业——Hello的程序人生
数据库·程序人生·课程设计
Alan3164 小时前
Qt 中,设置事件过滤器(Event Filter)的方式
java·开发语言·数据库
R_AirMan5 小时前
结合源码分析Redis的内存回收和内存淘汰机制,LRU和LFU是如何进行计算的?
redis·lfu·lru·内存回收·内存淘汰
TDengine (老段)5 小时前
TDengine 集群容错与灾备
大数据·运维·数据库·oracle·时序数据库·tdengine·涛思数据
Lao A(zhou liang)的菜园6 小时前
高效DBA的日常运维主题沙龙
运维·数据库·dba
迪迦不喝可乐7 小时前
mysql知识点
数据库·mysql
不太可爱的大白7 小时前
MySQL 事务的 ACID 四大特性及其实现原理
数据库·mysql
观测云8 小时前
HikariCP 可观测性最佳实践
数据库
文牧之8 小时前
PostgreSQL的扩展 dblink
运维·数据库·postgresql
趁你还年轻_9 小时前
Redis-旁路缓存策略详解
数据库·redis·缓存