黑马程序员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 slot2. 集群批处理解决方案
方案一:串行命令(不推荐)
逐个执行命令,性能最差,但实现简单:
// 串行执行,性能最差
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语言字符串存在以下问题:
- 获取长度时间复杂度高:需要遍历整个字符串,时间复杂度为O(N)
- 非二进制安全:以’\0’作为结束标识,如果字符串中包含’\0’会被误认为结束
- 不可修改:字符串字面量保存在常量池中,无法修改
SDS结构详解
SDS本质上是一个结构体,包含以下几个部分:
struct SDS {
uint8_t len; // 已使用字节数
uint8_t alloc; // 总申请字节数(不含结束标识)
uint8_t flags; // 头信息类型标识
char buf[]; // 存储实际数据的字符数组
};SDS的优势
- 常数时间获取长度:长度信息直接保存在结构体中,O(1)时间复杂度
- 二进制安全:通过长度字段确定字符串边界,不依赖结束标识
- 动态扩展:支持动态扩容,避免频繁内存重分配
- 内存预分配:采用预分配策略,减少内存分配次数
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. 最佳实践总结
批处理优化
- 在集群环境下,优先使用并行Slot方案
- 如使用Spring Boot,直接使用RedisTemplate的批处理方法
- 避免单次传输过多命令,建议1000-5000个为一批
- 对于性能要求极高的场景,可考虑Hash Tag方案
SDS理解
- 理解SDS的设计原理,有助于更好地使用Redis
- 利用SDS的二进制安全特性,可存储任意二进制数据
- 注意SDS的内存预分配策略,有助于理解Redis的内存使用
总结
Redis集群模式下的批处理优化是提升性能的重要手段,需要根据具体场景选择合适的方案。SDS作为Redis的基础数据结构,相比C语言字符串具有明显优势,特别是二进制安全和常数时间获取长度的特性。
在实际应用中,建议使用Spring Boot提供的RedisTemplate,它已内置了集群批处理的优化方案。同时要理解Redis底层数据结构的设计思想,有助于更好地进行性能优化和问题排查。