高级篇最后一点,讲讲实践过程中的一些建议
文章目录
3.Redis最佳实践
3.1Redis键值设计
3.1.1key的设计
Redis的key虽然可以自定义,但最好遵循下面几个最佳实践约定:
- 遵循基本格式:
[业务名称]:[数据名]:[id] - 长度不超过44字节
- 不包括特殊字符

优点:
- 可读性强
- 避免key冲突
- 方便管理
- 更节省内存:key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小
OBJECT ENCODING key命令的作用是查看指定键(Key)在内存中具体的底层实现方式(编码格式)
3.1.2BigKey问题
BigKey通常以Key中成员的数量来综合判定,eg:
- Key本身的数据量过大:一个String类型的Key,它是值为5MB
- Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10000个
- Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1000个但这些成员的Value值总大小为100MB
MEMORY USAGE key查看指定键的占用字节大小
推荐值:
- 单个key的value小于10KB
- 对于集合类型的key,建议元素数量小于1000
BigKey的危害
-
网络阻塞
对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢。
-
数据倾斜
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡。
-
Redis阻塞
对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞。
-
CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用。
如何发现BigKey
-
redis-cli --bigkeys
利用redis-cli提供的--bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key
统计只能看到第一名,不够完整
-
scan扫描
自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)
MEMORY USAGE的计算逻辑非常重,在 BigKey 场景下极易导致 Redis 主线程阻塞(卡顿)javaimport com.zhengge.jedis.util.JedisConnectionFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.ScanResult; import java.util.HashMap; import java.util.List; import java.util.Map; public class JedisTest { private Jedis jedis; @BeforeEach void setUp() { // 1.建立连接 // jedis = new Jedis("192.168.150.101", 6379); jedis = JedisConnectionFactory.getJedis(); // 2.设置密码 jedis.auth("123321"); // 3.选择库 jedis.select(0); } final static int STR_MAX_LEN = 10 * 1024; final static int HASH_MAX_LEN = 500; @Test void testScan() { int maxLen = 0; long len = 0; String cursor = "0"; do { // 扫描并获取一部分key ScanResult<String> result = jedis.scan(cursor); // 记录cursor cursor = result.getCursor(); List<String> list = result.getResult(); if (list == null || list.isEmpty()) { break; } // 遍历 for (String key : list) { // 判断key的类型 String type = jedis.type(key); switch (type) { case "string": len = jedis.strlen(key); maxLen = STR_MAX_LEN; break; case "hash": len = jedis.hlen(key); maxLen = HASH_MAX_LEN; break; case "list": len = jedis.llen(key); maxLen = HASH_MAX_LEN; break; case "set": len = jedis.scard(key); maxLen = HASH_MAX_LEN; break; case "zset": len = jedis.zcard(key); maxLen = HASH_MAX_LEN; break; default: break; } if (len >= maxLen) { System.out.printf("Found big key : %s, type: %s, length or size: %d %n", key, type, len); } } } while (!cursor.equals("0")); } @AfterEach void tearDown() { if (jedis != null) { jedis.close(); } } }特性 Jedis StringRedisTemplate 定位 底层客户端,Redis 驱动 Spring 的高级封装模板 层级 基础层 抽象层,基于 Jedis/Lettuce 连接管理 需手动管理连接池,实例线程不安全 自动管理连接池,模板本身线程安全 数据序列化 原生数据,无自动序列化 默认使用字符串序列化,存取都是 String 使用场景 非 Spring 项目、极致性能场景 Spring 项目、主要操作字符串数据 -
第三方工具
利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况
-
网络监控
自定义工具,监控进出Redis的网络数据,超出预警值时主动告警
如何删除BigKey
BigKey内存占用较多,在删除它时也需要小号很长时间,导致Redis主线程阻塞,引发一系列问题
-
Redis3.0及以下版本
如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey

-
Redis4.0以后
Redis4.0后提供了异步删除的命令:
unlink
把BigKey删除打散后,还需要选择合适的数据结构把它们的数据存进去
3.1.3选择合适的数据结构

例二:假如有hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?

存在的问题:
- hash的entry数量超过500时,会使用哈希表而不是ZipList,内存占用较多
- 可以通过hash-max-ziplist-entries配置entry上限,但是如果entry过多就会导致BigKey问题
方案1:拆分为string类型

存在的问题:
- string结构底层没有太多的内存优化,内存占用较多
- 想要批量获取这些数据比较麻烦
方案2:拆分成小的hash,将id/100作为key,将id%100作为field,每100个元素为一个hash

java
package com.zhengge.test;
import com.project.jedis.util.JedisConnectionFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.ScanResult;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JedisTest {
private Jedis jedis;
@BeforeEach
void setUp() {
// 1.建立连接
// jedis = new Jedis("192.168.150.101", 6379);
jedis = JedisConnectionFactory.getJedis();
// 2.设置密码
jedis.auth("123321");
// 3.选择库
jedis.select(0);
}
@Test
void testSetBigKey() {
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 650; i++) {
map.put("hello_" + i, "world!");
}
jedis.hmset("m2", map);
}
@Test
void testBigHash() {
Map<String, String> map = new HashMap<>();
for (int i = 1; i <= 100000; i++) {
map.put("key_" + i, "value_" + i);
}
jedis.hmset("test:big:hash", map);
}
@Test
void testBigString() {
for (int i = 1; i <= 100000; i++) {
jedis.set("test:str:key_" + i, "value_" + i);
}
}
@Test
void testSmallHash() {
int hashSize = 100;
Map<String, String> map = new HashMap<>(hashSize);
for (int i = 1; i <= 100000; i++) {
int k = (i - 1) / hashSize;
int v = i % hashSize;
map.put("key_" + v, "value_" + v);
if (v == 0) {
jedis.hmset("test:small:hash_" + k, map);
}
}
}
@AfterEach
void tearDown() {
if (jedis != null) {
jedis.close();
}
}
}
总结:

3.2批处理优化
3.2.1MSET和Pipeline
大量数据的导入方式有:一次运一点分多次运输,一次运很多几次就运完
单个命令的执行流程: 一次命令的响应时间 = 1次往返的网络传输耗时 + 1次Redis执行命令耗时

单个命令的执行耗时非常短

N条命令一个一个传输,在传输上就非常耗时
N条命令批量执行:

怎么批量执行?
MSET
Redis提供了很多Mxxx这样的命令,可以实现批量插入数据
利用mset批量插入10万条数据
java
@Test
void testMxx() {
String[] arr = new String[2000];
int j;
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
j = (i % 1000) << 1;
arr[j] = "test:key_" + i;
arr[j + 1] = "value_" + i;
if (j == 0) {
jedis.mset(arr);
}
}
}
但是一次传输太多也会把带宽占满,导致网络阻塞
Pipeline
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有复杂的数据类型要处理,建议使用Pipeline
java
@Test
void testPipeline() {
// 创建管道
Pipeline pipeline = jedis.pipelined();
long b = System.currentTimeMillis();
for (int i = 1; i <= 100000; i++) {
// 放入命令到管道
pipeline.set("test:key_" + i, "value_" + i);
if (i % 1000 == 0) {
// 每放入1000条命令,批量执行
pipeline.sync();
}
}
long e = System.currentTimeMillis();
System.out.println("time: " + (e - b));
}
为啥MSET比Pipeline执行速度快?
是因为MSET是Redis自带的且具有原子性,而在执行Pipeline时还会有别的命令插队
特性 MSET Pipeline 本质 Redis服务端的原生原子命令 客户端的网络优化技术 执行方式 一次解析,内部批量处理 打包发送,服务端逐个执行 原子性 有(执行期间无"插队") 无(执行期间可能"插队") 灵活性 低(只能用于String类型的SET) 高(可混合SET, GET, LPUSH等任意命令)
总结:

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

-
串行命令
javaJedisCluster jedisCluster = new JedisCluster(nodes); List<String> keys = Arrays.asList("k1", "k2", "k3", "k4"); // 纯粹的 for 循环 for (String key : keys) { // 1. 客户端计算 key 在哪个节点 // 2. 建立连接(或从池取) // 3. 发送命令 // 4. 等待响应 jedisCluster.set(key, "value"); } -
串行slot
java// 1. 将 keys 按照 slot 分组 Map<Integer, List<String>> slotKeysMap = new HashMap<>(); for (String key : keys) { int slot = JedisClusterCRC16.getSlot(key); slotKeysMap.computeIfAbsent(slot, k -> new ArrayList<>()).add(key); } // 2. 串行处理每一个 Slot 组 for (Map.Entry<Integer, List<String>> entry : slotKeysMap.entrySet()) { int slot = entry.getKey(); List<String> groupKeys = entry.getValue(); // 获取该 slot 对应的节点连接 (JedisCluster 内部逻辑较复杂,这里简化演示) JedisSlotBasedConnectionHandler connectionHandler = ...; Jedis jedis = connectionHandler.getConnectionFromSlot(slot); // 对该节点的这组 key 开启 Pipeline Pipeline pipeline = jedis.pipelined(); for (String k : groupKeys) { pipeline.set(k, "value"); } pipeline.sync(); // 提交这一组的命令 jedis.close(); } -
并行slot
javaExecutorService executor = Executors.newFixedThreadPool(10); for (Map.Entry<Integer, List<String>> entry : slotKeysMap.entrySet()) { // 提交任务给线程池,并行执行 executor.submit(() -> { int slot = entry.getKey(); List<String> groupKeys = entry.getValue(); Jedis jedis = connectionHandler.getConnectionFromSlot(slot); Pipeline pipeline = jedis.pipelined(); for (String k : groupKeys) { pipeline.set(k, "value"); } pipeline.sync(); jedis.close(); }); } executor.shutdown(); -
hash_tag
javaList<String> keys = Arrays.asList( "order:{2026}:001", "order:{2026}:002", "order:{2026}:003" ); // 注意:{} 中的内容决定了 slot,所以它们都在同一个 slot // 既然都在同一个 slot,也就在同一个节点 // 直接获取该节点的连接 Jedis jedis = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot("order:{2026}")); Pipeline pipeline = jedis.pipelined(); for (String k : keys) { pipeline.set(k, "value"); } pipeline.sync(); jedis.close();- 优点:代码最简单(就像操作单机一样),性能极高(只涉及 1 次网络交互)。
- 缺点 :数据倾斜 。如果所有 Key 都带
{2026},那它们都挤在一个节点上,这个节点内存爆了,其他节点却闲着。通常只用于强关联的数据(比如一个订单下的多个商品)
spring集群环境下批处理
java
@Test
void testMSetInCluster() {
Map<String, String> map = new HashMap<>(3);
map.put("name", "Rose");
map.put("age", "21");
map.put("sex", "Female");
stringRedisTemplate.opsForValue().multiSet(map);
List<String> strings = stringRedisTemplate.opsForValue().multiGet(Arrays.asList("name", "age", "sex"));
strings.forEach(System.out::println);
}
3.3服务端优化
3.3.1持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化也要遵循以下建议:
-
用来做缓存的Redis实例尽量不要开启持久化功能
-
建议关闭RDB持久化功能,使用AOF持久化
AOF 的数据完整性远高于 RDB
-
利用脚本定期在slave节点做RDB,实现数据备份
RDB 的
bgsave虽然是在子进程做的,但主进程(父进程)需要 fork 一个子进程 -
设置合理的rewrite阙值,避免频繁的bgrewrite
重写很耗资源:重写过程同样需要
fork子进程,消耗大量 CPU 和内存带宽 -
配置no-appendfsync-on-rewrite=yes,禁止在rewrite期间做AOP,避免AOF引起的阻塞
当子进程正在重写 AOF 文件时,主进程暂时不要把新的命令同步到磁盘(fsync),只写在内存缓冲区里

部署有关建议:
- Redis实例的物理机要预留足够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
- 不要与CPU密集型应用部署在一起
- 不要与高硬盘负载应用一起部署。例如:数据库、消息队列
3.3.2慢查询问题
慢查询:在Redis执行时耗时超过超过某个阙值的命令,称为慢查询

慢查询可能会导致的问题:
- 拖慢应用响应速度
- 耗尽数据库连接资源
- 引发系统雪崩效应
- 加剧锁竞争

查看慢查询日志列表:
- slowlong len:查询慢查询日志长度
- slowlong get[n]:读取n条慢查询日志
- slowlong reset:清空慢查询列表

3.4服务器优化
3.4.1命令及安全配置
Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网是,而Redis如果没有做身份认证,会出现严重的漏洞重现方式https://cloud.tencent.com/developer/article/1039000
漏洞出现的核心原因:
- Redis未设置密码
- 利用了Redis的config set命令动态修改Redis配置
- 使用了root账号权限启动Redis
避免漏洞的方法:
- Redis 一定要设置密码
- 禁止线上使用下面命令:keys、flushall、flushdb、config set等命令。 可以利用 rename-command 禁用。
- bind:限制网卡,禁止外网网卡访问
- 开启防火墙
- 不要使用 Root 账户启动Redis
- 尽量不是有默认的端口
3.4.2内存安全和配置
当 Redis 内存不足时,可能导致 Key 频繁被删除、响应时间变长、QPS 不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。

Redis提供了一些命令,可以看到Redis目前的内存分配状态:
- info memory
- memory xxx
内存缓冲区常见的有三种:
- 复制缓冲区:主从复制的 repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过 replbacklog-size 来设置,默认1mb
- AOF缓冲区:AOF 刷盘之前的缓存区域,AOF 执行 rewrite 的缓冲区。无法设置容量上限
- 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大 1G 且不能设置。输出缓冲区可以设置

3.4集群最佳实践
集群还是主从
集群完整性问题:
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
- 集群完整性问题
- 集群带宽问题
- 数据倾斜问题
- 客户端性能问题
- 命令的集群兼容性问题
- lua和事务问题

集群带宽问题
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:
- 插槽信息
- 集群状态信息
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。
解决途径:
- 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
- 避免在单个物理机中运行太多Redis实例
- 配置合适的cluster-node-timeout值
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群