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);
        });
    }
}
相关推荐
松涛和鸣12 小时前
72、IMX6ULL驱动实战:设备树(DTS/DTB)+ GPIO子系统+Platform总线
linux·服务器·arm开发·数据库·单片机
likangbinlxa12 小时前
【Oracle11g SQL详解】UPDATE 和 DELETE 操作的正确使用
数据库·sql
r i c k13 小时前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦13 小时前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL14 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·14 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德14 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫14 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i14 小时前
完全卸载MariaDB
数据库·mariadb
期待のcode14 小时前
Redis的主从复制与集群
运维·服务器·redis