zex - kv设计 & 批处理 & 主从or集群 & 配置 · Redis
kv设计
key定义
- 业务:字段:id
- 长度不超过44字节 -> 底层embstr
- 无特殊字符
key是string类型, 底层编码包括 int , embstr , raw, 其中embstr在小于44字节使用, 采用连续内存, 占用小
BigKey
- key本身大, 5MB的key
- key对应的成员多, ZSet
- key对应的成员大, Hash->Nval
危害
- 读请求导致带宽占满 -> 网络阻塞
- BigKey所在的Redis实例内存使用率远超过平均 -> 数据倾斜
- 对元素多的Hash、zset等做运算很耗时, 主线程被阻塞 -> Redis阻塞
- 对
BigKey数据反/序列化导致CPU使用率飙升 -> CPU压力
数据倾斜
Cluster是靠slot来负载均衡, 理论上每个节点处理的请求分布应该均匀 但是BigKey会导致单个slot落到某个节点导致内存暴涨, 集群整体性能不均衡
find BigKey
- redis-cli的
--bigkeys, 返回统计信息和每个数据的Top1的bigkey - 第三方工具: Redis-RDB-Tools分析RDB快照文件
- 通过代码扫描所有key, 利用strlen、hlen判断key的长度
代码块收起展开
String cursor = "0";
int count = 1000; // 每次扫描1000个key
long threshold = 1024 * 1024; // 1MB
ScanParams scanParams = new ScanParams().count(count);
int total = 0;
do {
// 2. 扫描一批 key
ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
cursor = scanResult.getCursor();
for (String key : scanResult.getResult()) {
total++;
try {
// 3. 获取 key 的内存占用
Long size = jedis.memoryUsage(key);
if (size != null && size > threshold) {
System.out.printf("⚠️ BigKey found: %-30s size=%.2f KB%n",
key, size / 1024.0);
}
} catch (Exception e) {
System.out.println("Error checking key: " + key);
}
}
} while (!cursor.equals("0"));删除BigKey
redis 3.0以下
遍历集合类型的BigKey元素, 再逐个删除val, 最后删除BigKey
4.0以后
用异步删除的unlink命令, 进行差分和选择合适的数据结构
批处理
Redis执行非常快, 但是往返redis命令不够快, 因此批量传送多条 redis命令可以实现效率上的优化
单节点的批处理
MSet —> 批量&原子性
不要在次批处理中传输太多命令,否则单次命令占带宽过多,会导致络阻塞
代码块收起展开
//String[] strs是命令数组
jedis.mset(strs)- 能操作的数据类型有限
pipeline —> 非原子性
- 能操作更复杂的数据类型
- 命令之间不具有原子性
代码块收起展开
Pipeline pp = jedis.pipelined();
for(int i=1;i<=100;i++){
pp.set(key,val);
if(i%10==0){
pp.sync();
}
}集群的批处理
由于redis是去中心化的, 所以如果批量处理的key中有落在不同的slot的,
就无法同时调度多个master去执行

并行slot
Redis 集群批处理策略对比
| 维度 | 串行命令 | 串行 slot | 并行 slot | hash_tag |
|---|---|---|---|---|
| 实现思路 | for 循环遍历,逐个执行命令 | 客户端计算每个 key 的 slot, 按 slot 分组, 每组用 pipeline 批处理 串行执行各组 | 客户端计算每个 key 的 slot,按 slot 分组,每组用 pipeline 批处理 并行执行各组 | 给所有 key 设置相同 hash_tag,使其落在同一 slot |
| 执行方式 | 串行 | 按 slot 串行 | 按 slot 并行 | 单 slot 执行 |
| 耗时 | N 次网络耗时 + N 次命令耗时 | m 次网络耗时 + N 次命令耗时(m=slot 数) | 1 次网络耗时 + N 次命令耗时 | 1 次网络耗时 + N 次命令耗时 |
| 优点 | 实现简单 | 相比串行更快 | 性能最好(网络开销最小) | 性能高 + 实现简单 |
| 缺点 | 非常慢 | slot 多时仍慢 | 实现复杂 | 容易出现数据倾斜 |
代码块收起展开
// 1. 根据 slot 分组
Map<Integer, List<String>> slotGroups = keys.stream()
.collect(Collectors.groupingBy(JedisClusterCRC16::getSlot));
// 2. 串行执行(按槽顺序)
List<String> resultsSerial = new ArrayList<>();
for (List<String> group : slotGroups.values()) {
resultsSerial.addAll(jedis.mget(group.toArray(new String[0])));
}
System.out.println("串行结果: " + resultsSerial);
// 3. 并行执行(每个槽一个线程)
ExecutorService executor = Executors.newFixedThreadPool(slotGroups.size());
List<Future<List<String>>> futures = new ArrayList<>();
for (List<String> group : slotGroups.values()) {
futures.add(
executor.submit(() -> jedis.mget(group.toArray(new String[0])) )
);
}直接用multiset,会将插槽自动分区, 不用担心
主从or集群
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的可用特性满足业务需求的情况下,尽量不搭建Redis集群
集群
- 高可用能自动故障恢复
- 相互ping要携带 slot & 集群状态, 节点一多通信带宽就炸了
- slot -> 数据倾斜
- 为了数据完整性, 一个master挂了就会停用集群---> 关掉
require-full-coverage
配置优化
服务端
持久化配置
- 建议关闭RDB持久化功能,使用AOF持久化
- 利用脚本定期在slave节点做RDB,实现数据备份设置合理的rewrite阈值,避免频繁的bgrewrite
- 禁止在rewrite期间做aof,避免因AOF引起的阻塞, 将增量数据放在aof缓冲区
部署有关建议:
- Redis实例的物理机要预留够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
- 不要与CPU密集型应用部署在一起, fork时cpu占用大
- 不要与高硬盘负载应用一起部署。例如:数据库、消息队列
慢查询
- slowlog len:查询慢查询日志长度
- slowlog get[n]:读取n条慢查询日志
- slowlog reset:清空慢查询列表
服务器优化
Redis在root用户下是可以把数据写入磁盘的 (SAVE BGSAVE) 此时如果攻击者能修改 Redis 的dir 和 db file name 参数 就能让Redis把某个key的内容写进系统任意文件
内存配置
内存缓冲区常见有三种:
复制缓冲区
- 主从复制的
repl_backlog_buf - 特点:
- 环状缓冲区
- 问题:
代码块收起展开
如果太小 → 可能导致频繁的全量复制 → 影响性能AOF缓冲区
- 用途:
- AOF 刷盘前的缓冲区域
- AOF 执行 rewrite 的缓冲区
- 特点: 无法设置容量上限
- 说明(原图标注): AOF 缓冲区无上限,但 AOF 执行很快,缓冲区内存波动不高
客户端缓冲区
输入缓冲区:
- 用于接收客户端请求
- 特点: 最大 1GB,不能设置
输出缓冲区:
- 用于发送响应给客户端
- 特点: 可以设置限制(重点)
类型:
- normal
- replica
- pubsub 设置格式
代码块收起展开
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>