黑马程序员Redis教程学习笔记(第8部分):集群模式下的批处理优化与SDS数据结构

2026-04-02
5
-
- 分钟
|

黑马程序员Redis教程学习笔记(第8部分):集群模式下的批处理优化与SDS数据结构

课程概述

本篇笔记涵盖黑马程序员Redis教程中关于集群模式下的批处理优化策略和Redis底层数据结构SDS(简单动态字符串)的重要内容。主要包括集群环境下MSET和Pipeline的使用限制、四种批处理解决方案以及Redis的SDS数据结构设计原理。

1. 集群模式下的批处理问题

问题描述

在Redis集群模式下,MSET、Pipeline等批处理命令要求多个Key必须落在同一个槽中,否则会导致执行失败。

问题根源

  • Redis集群基于哈希槽(16384个槽)实现数据分片
  • 当批处理的多个Key计算出的哈希槽不一致时,这些Key会被分配到不同的节点
  • 批处理需要跨多个节点执行,违背了批处理初衷
  • 无法保证原子性,可能导致数据不一致

错误示例

# 在集群模式下执行MSET,如果Key分布在不同槽中会报错
MSET name:jacks age:21 gender:male
# 报错:CROSSSLOT Keys in request don't hash to the same slot

2. 集群批处理解决方案

方案一:串行命令(不推荐)

逐个执行命令,性能最差,但实现简单:

// 串行执行,性能最差
for(String key : keys) {
    jedis.set(key, getValue(key));
}

方案二:串行Slot(推荐用于简单场景)

  • 计算每个Key的槽值
  • 将槽值相同的命令归为一组
  • 逐组执行批处理
// 按槽值分组
Map<Integer, List<Map.Entry<String, String>>> slotGroups = new HashMap<>();
for(Map.Entry<String, String> entry : data.entrySet()) {
    int slot = JedisClusterCRC16.getSlot(entry.getKey());
    slotGroups.computeIfAbsent(slot, k -> new ArrayList<>()).add(entry);
}

// 串行执行各组
for(List<Map.Entry<String, String>> group : slotGroups.values()) {
    String[] array = new String[group.size() * 2];
    int idx = 0;
    for(Map.Entry<String, String> entry : group) {
        array[idx++] = entry.getKey();
        array[idx++] = entry.getValue();
    }
    cluster.mset(array);
}

方案三:并行Slot(推荐)

  • 同样按槽值分组
  • 但使用多线程并行执行各组命令
  • 性能优于串行Slot
// 并行执行各组
slotGroups.values().parallelStream().forEach(group -> {
    String[] array = new String[group.size() * 2];
    int idx = 0;
    for(Map.Entry<String, String> entry : group) {
        array[idx++] = entry.getKey();
        array[idx++] = entry.getValue();
    }
    cluster.mset(array);
});

方案四:Hash Tag(性能最优但可能导致数据倾斜)

在Key中使用大括号{}包含相同内容,确保相关Key落在同一槽中:

{user1001}:name
{user1001}:age  
{user1001}:email

这种方式可以正常使用MSET和Pipeline,但可能导致数据倾斜问题。

3. Redis的SDS(简单动态字符串)数据结构

为什么需要SDS

Redis虽然是用C语言实现的,但没有直接使用C语言的字符串,因为C语言字符串存在以下问题:

  1. 获取长度时间复杂度高:需要遍历整个字符串,时间复杂度为O(N)
  2. 非二进制安全:以’\0’作为结束标识,如果字符串中包含’\0’会被误认为结束
  3. 不可修改:字符串字面量保存在常量池中,无法修改

SDS结构详解

SDS本质上是一个结构体,包含以下几个部分:

struct SDS {
    uint8_t len;      // 已使用字节数
    uint8_t alloc;    // 总申请字节数(不含结束标识)
    uint8_t flags;    // 头信息类型标识
    char buf[];       // 存储实际数据的字符数组
};

SDS的优势

  1. 常数时间获取长度:长度信息直接保存在结构体中,O(1)时间复杂度
  2. 二进制安全:通过长度字段确定字符串边界,不依赖结束标识
  3. 动态扩展:支持动态扩容,避免频繁内存重分配
  4. 内存预分配:采用预分配策略,减少内存分配次数

SDS内存预分配策略

  • 小于1MB:扩容后长度的2倍 + 1(预留结束标识空间)
  • 超过1MB:扩容后长度 + 1MB + 1(预留结束标识空间)

多种SDS类型

Redis定义了多种SDS结构以适应不同长度的字符串: - sdshdr5:5位,已废弃 - sdshdr8:8位,最大2^8-1字节 - sdshdr16:16位,最大2^16-1字节 - sdshdr32:32位,最大2^32-1字节 - sdshdr64:64位,最大2^64-1字节

根据字符串长度自动选择合适的SDS类型,既节省内存又保证性能。

4. 实际应用中的优化建议

1. 选择合适的批处理方案

  • 串行命令:性能最差,仅用于极少量数据
  • 串行Slot:实现简单,性能较好
  • 并行Slot:性能优秀,推荐使用
  • Hash Tag:性能最优,但可能导致数据倾斜

2. Spring Boot中的自动优化

Spring Boot的RedisTemplate会自动处理集群下的批处理: - 自动计算Key的槽值 - 按槽值分组 - 使用异步方式执行各组命令 - 实现了并行Slot方案

3. 避免Big Key问题

在设计Key时要考虑: - Key长度控制在44字节以内 - 合理设计Key结构,避免过长 - 避免单个Key存储过多数据

4. 数据结构选择

对于存储对象,有三种方式: 1. JSON字符串方式:实现简单,但灵活性差 2. 字段打散方式:灵活但占用空间大 3. 哈希结构方式(推荐):灵活性好,内存占用小

5. 性能对比分析

方案 网络往返次数 性能 实现复杂度 推荐程度
串行命令 N次 最差 简单 不推荐
串行Slot 槽数量次 较好 中等 一般推荐
并行Slot 槽数量次 优秀 中等 强烈推荐
Hash Tag 1次 最优 简单 有条件推荐

6. 最佳实践总结

批处理优化

  1. 在集群环境下,优先使用并行Slot方案
  2. 如使用Spring Boot,直接使用RedisTemplate的批处理方法
  3. 避免单次传输过多命令,建议1000-5000个为一批
  4. 对于性能要求极高的场景,可考虑Hash Tag方案

SDS理解

  1. 理解SDS的设计原理,有助于更好地使用Redis
  2. 利用SDS的二进制安全特性,可存储任意二进制数据
  3. 注意SDS的内存预分配策略,有助于理解Redis的内存使用

总结

Redis集群模式下的批处理优化是提升性能的重要手段,需要根据具体场景选择合适的方案。SDS作为Redis的基础数据结构,相比C语言字符串具有明显优势,特别是二进制安全和常数时间获取长度的特性。

在实际应用中,建议使用Spring Boot提供的RedisTemplate,它已内置了集群批处理的优化方案。同时要理解Redis底层数据结构的设计思想,有助于更好地进行性能优化和问题排查。

评论交流

文章目录