本文基于黑马2022的Redis课程高级篇编写,课程地址:黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目
课程资料:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA&pwd=eh11
本文中若未特意提及,则默认Linux环境用Ubuntu 24.04,Redis用6.2.6版本,本地主机用Windows
Redis键值设计
优雅的key结构
Redis的Key虽然可以自定义,但最好遵循下面的几个最佳实践约定:
- 遵循基本格式:[业务名称]:[数据名]:[id]
- 长度不超过44字节
- 不包含特殊字符
例如:我们的登录业务,保存用户信息,其key可以设计成如下格式:

这样设计的好处:
- 可读性强
- 避免key冲突
- 方便管理
- 更节省内存:key是string类型,底层编码包含int、embstr和raw三种。embstr在小于44字节使用,采用连续内存空间,内存占用更小。当字节数大于44字节时,会转为raw模式存储,在raw模式下,内存空间不是连续的,而是采用一个指针指向了另外一段内存空间,在这段空间里存储SDS内容,这样空间不连续,访问的时候性能也就会受到影响,还有可能产生内存碎片

拒绝BigKey
什么是BigKey
BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:
- Key本身的数据量过大:一个String类型的Key,它的值为5 MB
- Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个
- Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB
*那么如何判断元素的大小呢?*redis也给我们提供了命令

推荐值:
- 单个key的value小于10KB
- 对于集合类型的key,建议元素数量小于1000
BigKey的危害
- 网络阻塞
- 对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
- 数据倾斜
- BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
- Redis阻塞
- 对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞
- CPU压力
- 对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
如何发现BigKey
1)redis-cli –bigkeys
利用redis-cli提供的–bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key
命令:redis-cli -a 密码 --bigkeys

2)scan扫描
自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)
Redis 的 SCAN 命令用于基于游标(Cursor)进行增量式迭代。其返回结果是一个包含两个元素的数组:
- 下一次迭代的游标:
- 第一次调用时,需将游标设置为
0以开启新的迭代。 - 命令返回的第一个值即为下一次调用时需要传入的游标。
- 当命令返回的游标再次变为
0时,表示整个遍历过程已结束。
- 第一次调用时,需将游标设置为
- 当前批次的数据:
- 返回的第二个值是一个数组(List),包含本次迭代扫描到的键(Keys)或元素。
- 数组的大小受
COUNT参数控制(例如下图中设置了COUNT 2,即每次尝试返回约 2 个元素;若不设置,默认约为 10 个)。

参考代码:
|
|
3)第三方工具
利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析内存使用情况
4)网络监控
自定义工具,监控进出Redis的网络数据,超出预警值时主动告警,一般阿里云搭建的云服务器就有相关监控页面

如何删除BigKey
BigKey内存占用较多,即便时删除这样的key也需要耗费很长时间,导致Redis主线程阻塞,引发一系列问题
- redis 3.0 及以下版本
如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey。Redis也有提供专门对集合的scan:

- Redis 4.0以后
Redis在4.0后提供了异步删除的命令:unlink
恰当的数据类型
选择存储方式
比如存储一个User对象,有三种存储方式,应该选择哪种?
①方式一:json字符串

优点:实现简单粗暴
缺点:数据耦合,不够灵活
②方式二:字段打散

优点:可以灵活访问对象任意字段
缺点:占用空间大、没办法做统一控制
③方式三:hash(推荐)

优点:底层使用ziplist压缩内存,空间占用小,可以灵活访问对象的任意字段
缺点:代码相对复杂
优化hash类型key
假如有hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?

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

存在的问题:
- string结构底层没有太多内存优化,内存占用较多,现在大约会占近80M
- 想要批量获取这些数据比较麻烦
方案二:
拆分为小的hash,将 id / 100 作为key,将id % 100 作为field,这样每100个元素为一个Hash(不一定要是100,只要entry小于hash-max-ziplist-entries配置的entry上限即可)

这种方案占用的内存在25M左右
总结
- Key的最佳实践
- 固定格式:[业务名]:[数据名]:[id]
- 足够简短:不超过44字节
- 不包含特殊字符
- Value的最佳实践:
- 合理的拆分数据,拒绝BigKey
- 选择合适数据结构
- Hash结构的entry数量不要超过hash-max-ziplist-entries配置的entry上限
- 设置合理的超时时间
批处理优化
Pipeline
Redis 的 Pipeline(管道)本质上是一种批量命令优化机制。它的核心作用非常单一且明确:通过减少客户端与 Redis 服务器之间的网络往返次数(RTT, Round-Trip Time),来大幅提升批量操作的吞吐量。
我们的客户端与redis服务器是这样交互的:
单个命令的执行流程↓

N条命令的执行流程↓

redis处理指令是很快的,主要花费的时候在于网络传输。于是乎很容易想到将多条指令批量的传输给redis

MSet
Redis提供了很多Mxxx这样的命令,可以实现批量插入数据,例如:
- mset
- hmset
利用mset批量插入10万条数据,参考代码:
|
|
注意:不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞
Pipeline
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline
利用pipeline批量插入10万条数据,参考代码:
|
|
集群下的批处理
在 Redis 集群模式下,执行 MSET 或 Pipeline 等批处理操作时,存在一个关键限制:所有涉及的 Key 必须通过哈希槽(Hash Slot)计算并映射到同一个节点上。如果批处理中的多个 Key 分散在不同的节点,集群将无法在单次请求中完成路由,从而导致执行失败。
这一限制在实际开发中往往带来极大的挑战。因为在常规的批量写入场景中,业务数据通常是随机或自然分布的,很难天然地满足“全部落在同一插槽”的苛刻条件。这极易引发跨槽(Cross-slot)错误,导致批量操作无法顺利执行。
有4种解决方案:

第一种方案:串行执行,所以这种方式没有什么意义,当然,执行起来就很简单了,缺点就是耗时过久。
第二种方案:串行slot,简单来说,就是执行前,客户端先计算一下对应的key的slot,一样slot的key就放到一个组里边,不同的,就放到不同的组里边,然后对每个组执行pipeline的批处理,他就能串行执行各个组的命令,这种做法比第一种方法耗时要少,但是缺点是相对来说复杂一点,所以这种方案还需要优化一下
第三种方案:并行slot,相较于第二种方案在分组完成后串行执行,第三种方案就变成了并行执行各个命令,所以他的耗时就非常短,但是实现也更加复杂。
第四种方案:hash_tag,redis计算key的slot的时候,其实是根据key的有效部分来计算的,通过这种方式就能一次处理所有的key,这种方式耗时最短,实现也简单,但是如果通过操作key的有效部分,那么就会导致所有的key都落在一个节点上,产生数据倾斜的问题,所以推荐使用第三种方式。
Spring封装的StringRedisTemplate在集群环境下批处理就是用的第三种方案
|
|
服务端优化
持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
- 用来做缓存的Redis实例尽量不要开启持久化功能
- 推荐使用混合持久化(Hybrid Persistence)。开启
aof-use-rdb-preamble yes(Redis 4.0+支持) - 利用脚本定期在slave节点做RDB,实现数据备份
- 设置合理的rewrite阈值,避免频繁的bgrewrite
- 配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞(根据安全或性能做取舍,对性能要求高就yes,对安全要求高就no,不确定就用默认的no)
- 部署有关建议:
- Redis实例的物理机要预留足够内存,应对fork和rewrite
- 单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
- 不要与CPU密集型应用部署在一起
- 不要与高硬盘负载应用一起部署。例如:数据库、消息队列
慢查询优化
什么是慢查询
并不是很慢的查询才是慢查询,而是:在Redis执行时耗时超过某个阈值的命令,称为慢查询
慢查询的危害:由于Redis是单线程的,所以当客户端发出指令后,他们都会进入到redis底层的queue来执行,如果此时有一些慢查询的数据,就会导致大量请求阻塞,从而引起报错,所以我们需要解决慢查询问题。
慢查询的阈值可以通过配置指定:
slowlog-log-slower-than:慢查询阈值,单位是微秒。默认是10000,建议1000
慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定:
slowlog-max-len:慢查询日志(本质是一个队列)的长度。默认是128,建议1000

修改这两个配置可以使用:config set

如何查看慢查询
知道了以上内容之后,那么如何查看慢查询日志列表呢?
- slowlog len:查询慢查询日志长度
- slowlog get [n]:读取n条慢查询日志
- slowlog reset:清空慢查询列表

命令及安全配置
安全可以说是服务器端一个非常重要的话题,如果安全出现了问题,一旦漏洞被恶意攻击者利用,就会给我们的系统带来巨大的损失。所以这小节就来解决这个问题。
Redis 默认会绑定在 0.0.0.0:6379,这会将 Redis 服务直接暴露在公网上。如果 Redis 没有做身份认证,将会出现严重的安全漏洞。 漏洞重现方式:https://cloud.tencent.com/developer/article/1039000
为什么会出现不需要密码也能登录的情况呢?这里的核心原理其实是攻击者利用了 Redis 的权限去篡改了 Linux 系统的 SSH 免密登录配置。SSH 免密登录需要在服务器的 authorized_keys 文件中存入客户端的公钥。而 Redis 的漏洞在于,攻击者可以在未授权登录的情况下,利用 Redis 的持久化功能,强行把自己的 SSH 公钥“写入”到 Linux 服务器的 /root/.ssh/authorized_keys 文件中,从而直接获取服务器的最高权限。
漏洞出现的核心原因有以下几点:
- Redis 未设置密码(未授权访问)
- 攻击者利用了 Redis 的
config set命令动态修改 Redis 的持久化路径和文件名 - 使用了 Root 账号权限启动 Redis,导致 Redis 有权限向系统核心目录写入文件
所以:如何解决呢?我们可以采用如下几种方案
为了避免这样的漏洞,这里给出一些建议:
- Redis 一定要设置强密码(
requirepass) - 禁止线上使用高危命令:
keys、flushall、flushdb、config set等。可以利用rename-command重命名或直接禁用。 bind:限制网卡绑定,禁止外网网卡直接访问,仅允许内网或本机访问- 开启防火墙,严格限制 6379 端口的访问来源 IP
- 不要使用 Root 账户启动 Redis,应使用权限受限的普通用户
- 尽量不使用默认的 6379 端口,改为其他非常用端口
内存划分和内存配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。
有关碎片问题分析
Redis底层分配并不是这个key有多大,他就会分配多大,而是有他自己的分配策略,比如8、16、32、48等,假定当前key只需要10个字节,此时分配8肯定不够,那么他就会分配16个字节,多出来的6个字节就成为内部碎片(该分配单元内无法被当前 key 使用的空间),这就是我们常说的碎片问题
进程内存问题分析:
这片内存,通常我们都可以忽略不计
缓冲区内存问题分析:
一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,所以这部分内存是我们需要重点分析的。
| 内存占用 | 说明 |
|---|---|
| 数据内存 | 是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题 |
| 进程内存 | Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略。 |
| 缓冲区内存 | 一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出。 |
于是我们就需要通过一些命令,可以查看到Redis目前的内存分配状态:
- info memory:查看内存分配的情况

- memory usage
:查看key的主要占用情况

看到了这些配置,最关键的缓存区内存应该如何定位和解决呢?
内存缓冲区常见的有三种:
- 复制缓冲区:主从复制的repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlog-size来设置,默认1MB
- AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行rewrite的缓冲区。没有直接可配置的容量上限
- 客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设置
以上复制缓冲区和AOF缓冲区通常不会有问题,最关键就是客户端缓冲区的问题
客户端缓冲区:指的就是我们发送命令时,客户端用来缓存命令的一个缓冲区,也就是我们向redis输入数据的输入端缓冲区和redis向客户端返回数据的响应缓存区,输入缓冲区最大1G且不能设置,所以这一块我们根本不用担心,如果超过了这个空间,redis会直接断开该客户端连接,因为本来此时此刻就代表着redis处理不过来了,我们需要担心的就是输出端缓冲区,其配置如下

我们在使用Redis过程中,如果处理大量的big value,那么会导致我们的输出结果过多,如果输出缓冲区过大,会导致Redis直接断开。而默认配置下,普通客户端的输出缓冲区默认是没有大小限制的,这其实是一个巨大的隐患。一旦遇到BigKey或慢客户端,缓冲区就会无节制地膨胀,疯狂吞噬服务器内存。当内存占用达到物理极限时,不仅会触发操作系统的 OOM Killer 导致 Redis 进程崩溃,Redis自身的保护机制也可能强制断开连接。所以解决方案有两个:
- 设置一个大小(即设置输出缓冲区的硬限制和软限制)。
- 增加我们带宽的大小,可以缓解传输压力,但根本还是要合理设置输出缓冲区大小或控制big value的数量。
集群最佳实践
集群完整性问题
在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务:

为了保证高可用特性,这里建议将 cluster-require-full-coverage 配置为 false
集群带宽问题
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:
- 插槽信息
- 集群状态信息
集群中节点越多,集群状态信息数据量也越大,例如10个节点的相关信息可能达到1kb左右,此时每次节点间通信需要的带宽会非常高,这样会导致集群中大量的带宽都会被ping信息所占用,这是一个非常可怕的问题,所以我们需要去解决这样的问题
解决途径:
- 避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
- 避免在单个物理机中运行太多Redis实例
- 配置合适的cluster-node-timeout值
命令的集群兼容性问题
有关这个问题前面已经探讨过了,当我们使用批处理的命令时,redis要求我们的key必须落在相同的slot上,大量的key同时操作时,是无法完成的,所以客户端必须要对这样的数据进行处理,这些方案之前已经探讨过了,所以不再这个地方赘述了。
lua和事务的问题
Lua和事务都需要保证原子性。如果你的key不在同一个节点(即属于不同slot),那么Redis无法执行该Lua脚本或事务。所以在集群模式下,只有当你操作的key全部落在同一个节点时,才能成功执行Lua脚本和事务。
选主从还是集群
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,不是万不得已尽量不搭建Redis集群