Redis常见阻塞原因
命令阻塞
首先,Redis 是单线程的内存数据库,所有命令都在主线程中串行执行。而且 Redis 中的大部分命令都是 O(1) 时间复杂度,也有少部分 O(n) 时间复杂度的命令。
这意味着,O (1) 命令不会显著阻塞主线程,O (n) 会明显占用主线程,导致后续所有请求排队等待,这就表现成客户端阻塞,超时,响应慢等问题
那么这种情况就是使用不当的命令导致的阻塞,就好像是在 MySQL 那边
select *联查了所有字段导致了全表扫描一样导致的性能问题,但是你
MySQL 能加索引将 O (n) 降为 O (log
n),所以说,当你API或数据结构使用不合理,肯定也会导致这种情况
首先这种全量遍历类是O(n)肯定会影响性能
keys:获取所有的 key 操作HGETALL:会返回一个 Hash 中所有的键值对。SMEMBERS:返回 Set 中的所有元素。
对于集合计算类,也是O (n),不当的使用也可能会导致问题
SINTER/SUNION/SDIFF:计算多个 Set 的交集/并集/差集。
范围操作类命令,例如:
ZRANGE/ZREVRANGE:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m)ZREMRANGEBYRANK/ZREMRANGEBYSCORE:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。
由于这些命令时间复杂度比较高,很容易一扫扫整个表,随着 n 的增大,执行耗时也会越长,从而导致客户端阻塞。不过, 这些命令并不是一定不能使用,但是需要明确 n 的值。
另外,有遍历的需求可以使用
HSCAN、SSCAN、ZSCAN
替代全量命令。因为SCAN系列命令是分批遍历,每次只返回少量元素,时间复杂度被均摊到多次请求,把阻塞分配了,这个思路很类似于
MySQL 那边的分页查询。
无论是 MySQL 还是 Redis,使用命令的核心都是避免对大数据量执行全量 / 大范围操作,避免查询无用数据,把它们拆分小批次执行,且必须明确操作涉及的数据量 n/m 的大小
API或数据结构使用不合理
本质上是不正当的使用命令导致的阻塞
一般解决是发现慢查询,通过slowlog get {n}获取慢查询日志
要么就是发现大对象
SAVE 创建 RDB 快照
RDB 持久化是 Redis
将当前内存中的所有数据以二进制快照的形式写入磁盘文件来进行持久化的一个机制,SAVE
是触发 RDB 持久化的同步命令
而上面也说了 Redis
是单线程的,所以说,所有数据写入磁盘,这是一个高IO的操作,写入导致的耗时不能忽视,SAVE
命令的执行全程占用这个主线程,主要是三个阶段,每个阶段都会让主线程无法处理其他请求
- 遍历内存数据,生成快照数据,也就是遍历自己管理的所有数据结构,将这些数据序列化为二进制快照,根据上面说的那O(n)遍历肯定是个大耗时
- 将快照数据写入磁盘,这是主要的 IO 阻塞阶段,序列完成后主线程会将生成的快照数据一次性写入磁盘文件
- 写入完成了还会更新 RDB 文件元信息,替换旧的 RDB 快照,这部分通常不会阻塞
而破局之道就在 Redis 自己中,Redis 提供了两个命令来生成 RDB 快照文件:
save: 同步保存操作,会阻塞 Redis 主线程;bgsave: fork 出一个子进程,子进程执行,不会阻塞 Redis 主线程,默认选项。
默认情况下,Redis 默认配置会使用 bgsave
命令。如果手动使用 save 命令生成 RDB
快照文件的话,就会阻塞主线程
想一下,这是不是和 MySQL 的持久化阻塞类似,因为 MySQL 是 InnoDB 刷盘时的 IO 阻塞,Redis 也是一个遍历自己的阻塞
AOF 导致的阻塞
AOF 日志记录阻塞
Redis AOF 持久化机制是在执行完命令之后再记录命令日志,这和关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复)不同
为什么是在执行完命令之后记录日志呢?
- 避免额外的检查开销,因为你都能执行,肯定是正确的命令,所以 AOF 记录日志不会对命令进行语法检查;
- 在命令执行完之后再记录,不会阻塞当前的命令执行,但是有可能阻塞下一个命令
这样也带来了风险(在介绍 AOF 持久化的时候也提到过):
- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
- 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)
AOF 刷盘阻塞
开启 AOF 持久化后,每执行一条会更改 Redis 中的数据的命令,Redis
就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再根据
appendfsync 配置的刷盘策略来决定何时将其同步到硬盘中的 AOF
文件。
AOF 的文件同步策略决定了刷盘的频率,也就是指令从缓冲区刷到磁盘的频率,复习一下
- appendfsync always:主线程调用
write执行写操作后,后台线程aof_fsync线程会立即调用fsync函数来同步 AOF 文件进行一次刷盘,fsync完成后线程才返回,这样会严重降低 Redis 的性能(write+fsync)。也就是每次写操作都刷盘。数据绝对安全,但频繁 IO 会严重拖慢性能。 - appendfsync everysec:主线程调用
write执行写操作后立即返回,由后台aof_fsync线程每秒钟调用fsync函数同步一次 AOF 文件,也就是每秒刷盘一次,最多丢 1 秒数据,性能损耗适中,是默认且最常用的策略。 - no:主线程调用
write执行写操作后立即返回,让操作系统决定何时进行同步,Linux 下一般为 30 秒一次,也就是说,由操作系统决定刷盘时机。性能最好,但数据丢失风险最高
其中,很明显,最大的压力就是当后台线程aof_fsync 线程调用
fsync 函数同步 AOF 文件时,需要等待,直到写入完成。
当磁盘压力太大的时候,会导致 fsync
操作发生阻塞,主线程调用 write
函数时也会被阻塞。fsync 完成后,主线程执行
write 才能成功返回。
AOF 重写阻塞
随着你越来越用,AOF 文件会变得越来越大,带来两个问题:
- AOF 文件过大,占用磁盘空间;
- AOF 文件涉及到载入的问题,Redis 重启时,重放 AOF 文件的耗时会极长。
AOF 重写的核心作用是生成一个简单版本的 AOF 文件,不再记录所有历史写命令,从而大幅缩小 AOF 文件体积。
过程是通过fork一个子进程,重新写一个新的AOF文件,该次重写不是读取旧的AOF文件进行复制,而是读取内存中的
Redis 数据库,重写一份AOF文件
- 主线程
fork出一个子线程来将原 AOF 文件进行重写,在执行BGREWRITEAOF命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子线程创建新 AOF 文件期间,记录服务器执行的所有写命令。 - 当子线程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。
- 最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
所以说,AOF 重写导致阻塞主要集中在如下两块
- 主线程
fork子进程的瞬间阻塞- 这会涉及到线程分配的相关阻塞,这个阻塞时间与 Redis 主线程占用的内存大小正相关,因为需要复制主线程的内存页表
- 主线程追加重写缓冲区数据的阻塞,将缓冲区中新数据写到新文件的过程中会产生阻塞。
- 子进程重写期间,主线程仍在处理新的写命令,这些命令会被同时写入普通 AOF 缓冲区和 AOF 重写缓冲区来保证命令不丢失,当子进程完成重写后,主线程需要将重写缓冲区中所有新命令一次性写入,这个追加过程是同步的且主线程操作的
为什么fork出的子进程的重写过程不阻塞主线程?
写时复制(子进程刚被
fork出来时,并不会复制主线程的所有内存数据,若主线程修改某块内存数据,Redis 会先复制该块内存,子进程访问的资源不变,这样避免了全量复制)
解决上述的各种 AOF 导致的阻塞,比较好的办法是混合持久化,这样重写后的 AOF 文件开头是 RDB 格式的数据最终形态,后面是增量 AOF 命令
大KEY
如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。
MEMORY USAGE key:精确查看 Key 内存占用
具体多大才算大,emmm,这个得根据你服务器的配置来考虑,但行业与云厂商(腾讯云、火山引擎)有统一判定标准:
- String:单个 Value > 10KB 需关注,> 100KB 为大 Key
- Hash/List/Set/ZSet:元素数 > 5000 或总大小 > 10MB*
- Stream:条目数 > 1000
大 key 造成的阻塞问题如下
- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
- 引发网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令,产生了阻塞
Redis 大 Key 导致阻塞的核心根源是单线程命令执行模型,大 Key 操作因数据量大、CPU/IO 耗时高,会独占主线程,导致后续所有请求排队阻塞。
具体的体现为如下
而且针对大 KEY 的优化就是大 Key 拆分
- String 拆分:按业务维度拆分为多个小 Key(如
user:1:info→user:1:base、user:1:ext) - Hash 拆分:按字段哈希取模拆分(如
hash:1→hash:1:0、hash:1:1) - List/Set 拆分:按时间 / 范围拆分(如
list:202602、list:202603)
搜索大KEY
redis-cli --bigkeys:扫描并输出各类型最大
Key,肯定涉及到遍历所有 Key,这就涉及到了上面说的
KEYS *全量遍历问题
当我们在使用 Redis 自带的 --bigkeys 参数查找大 key
时,最好选择在从节点上执行该命令,因为主节点上执行时,会阻塞主节点。
所以说,不少情况下,通过分析 RDB 文件来找出 big key
是一个相对可以的方案,网上有现成的工具,例如rdb-tools,redis-rdb-cli等
而且,我们可以使用 SCAN 增量遍历命令分批遍历来代替
--bigkeys,思路就是循环 SCAN 遍历 Key + MEMORY USAGE
计算大小
删除大 Key
删除操作的本质是要释放键值对占用的内存空间。
释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,遍历并回收所有元素占用的内存页,方便后续管理和在有必要的时候进行内存整理。
这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。
所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。
删除大 key 时建议采用分批次删除和异步删除的方式进行
用 UNLINK 替代
DEL,异步删除,主线程仅标记,后台线程回收内存,无阻塞,或者HSCAN
+ HDEL 分批删 Hash 字段
清空数据库
清空数据库和上面 bigkey
删除也是同样道理,flushdb、flushall
也涉及到删除和释放所有的键值对,也是 Redis 的阻塞点
CPU 竞争
主要就是这些情况,对应外部 CPU 抢占和自身 CPU 过载两类
- 服务器层面的 CPU 抢占:Redis 是典型的 CPU 密集型应用,不建议和其他多核 CPU 密集型服务部署在一起。当其他进程过度消耗 CPU 时,将严重影响 Redis 的吞吐量。
- 单线程过载导致Redis 自身的 CPU 耗尽:若 Redis 主线程执行大量 CPU 密集型命令,导致单核 CPU 100% 占用,后续命令排队等待执行,这本质上是命令不正确不恰当使用导致阻塞的进一步原因
- Redis 子进程或者后台线程的 CPU 竞争:Redis
的子进程(
BGSAVE/BGREWRITEAOF)、后台线程(异步删除、内存碎片整理)会占用 CPU 资源,与主线程竞争 - CPU 空耗:例如网不好导致的客户端频繁建立 / 断开连接,或者丢包导致丢数据丢请求等各种情况下的空耗 CPU;
可以通过redis-cli --stat获取当前 Redis
使用情况。通过top命令获取进程对 CPU
的利用率等信息,或者通过info commandstats统计信息分析出命令不合理开销时间,查看是否是因为高算法复杂度或者过度的内存优化问题。
1 | # 查看服务器整体 CPU 占用(按 CPU 排序) |
集群扩缩容
Redis 集群操作不是命令阻塞,但它会触发阻塞主线程的关键行为,例如大量 Key 迁移导致的主线程阻塞,主从切换、节点重启、槽重新分配带来的短暂不可用
Redis 集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。
集群扩容 / 缩容,本质就是把一部分 槽(slot) 从旧节点,迁移到新节点,所以说,在扩缩容的时候,需要进行数据迁移。Redis 为了保证迁移的一致性,迁移所有操作都是同步操作。
这个过程不仅是 IO 开销,网络开销也比较大,所以说执行迁移时,两端的 Redis 均会进入时长不等的阻塞状态,对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,严重的时候会触发集群内的故障转移,造成不必要的切换。
而且假如你访问了一个正在迁移的 key 时,如果判定了源节点没有这个 Key,会阻塞等待或者重定向,导致阻塞感强烈,类似缓存雪崩
因为阻塞感不止是客户端等待的时间,它包括对高可用的考虑,如果你请求三四次都失败了,这个阻塞感肯定是极其严重的
Swap 内存交换
Swap 直译过来是交换的意思,Linux 中的 Swap 常被称为内存交换或者交换分区,类似于 Windows 中的虚拟内存,Swap 是操作系统的内存管理机制
就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况。
具体体现有点复杂,总之就是系统会将内存中暂时不用的数据写入磁盘上的 Swap 分区 ,也就是所谓的虚拟内存,释放物理内存给活跃进程;
当进程需要访问这些数据时,再从 Swap 分区读回物理内存,这个 内存 ↔︎ 磁盘 的数据交换过程就是 Swap 交换。
因此,Swap 分区的作用就是牺牲少量硬盘,增加部分内存,解决 VPS 内存不够用或者爆满的问题。
所以说,Swap 对于 Redis 来说是非常致命的,Redis 保证高性能的一个重要前提是所有的数据在内存中,所有数据默认加载在物理内存中,如果触发 Swap,操作系统把 Redis 使用的部分内存换出硬盘,由于内存与硬盘的读写速度差几个数量级,而且可能会引发缺页中断,会导致发生交换后的 Redis 性能急剧下降,引发严重阻塞。
- 这里的缺页中断指的是当 Redis 主线程执行
GET key/SET key等命令时,若该 key 对应的内存页已被操作系统换出到 Swap 磁盘,CPU 会触发缺页中断,暂停 Redis 主线程的执行,操作系统会将该内存页从 Swap 磁盘读回物理内存,只有当数据回到物理内存后,Redis 主线程才能继续执行命令。
官方提示(Redis 运维手册):Swap is extremely dangerous for Redis performance. All Redis operations are in memory, and swapping makes them disk-bound.(Swap 对 Redis 性能极具破坏性,Redis 所有操作基于内存,而 Swap 会让操作变成磁盘绑定)
识别 Redis 发生 Swap 的检查方法如下
查看 Swap 分区使用量
1
2
3
4
5
6
7
8
9
10
11
12
13# 查看 Swap 分区使用量(单位:KB)
swapon --show
# 或
cat /proc/swaps
# 根据进程号查询内存交换信息
cat /proc/4476/smaps | grep Swap
Swap: 0kB
Swap: 0kB
Swap: 4kB
Swap: 0kB
Swap: 0kB
.....
预防内存交换的方法:
- 保证机器充足的可用内存
- 确保所有 Redis 实例设置最大可用内存(maxmemory),而且要合理设置,防止极端情况 Redis 内存不可控的增长
- 降低内存碎片化,Redis 4.0+ 启用
activedefrag yes(自动内存碎片整理),减少因内存碎片导致的物理内存不足;
网络问题
数据库一旦谈到性能就万年离不开的老问题之网络性能
连接拒绝、网络延迟,网卡软中断等网络问题也可能会导致 Redis 阻塞,只要是数据库,就会难以避免的在网络上产生一定的相对占比较大的开销
Redis内存碎片
这部分在part6中也有所涉及,但是我为了方便看,也搬过来了
什么是内存碎片
你可以将内存碎片简单地理解为那些不可用的空闲内存。
就假如,操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。
Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗,内存是很珍贵的
为什么会有 Redis 内存碎片
Redis 内存碎片产生比较常见的 2 个原因:
Redis 存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
Redis 实际使用的内存大小(RSS)远大于它真正存储数据所需的内存大小(Used Memory)。
Used Memory:Redis 认为“自己用了多少内存”(通过INFO memory查看)RSS(Resident Set Size):操作系统实际分配给 Redis 的物理内存(可通过ps aux | grep redis查看)
To store user keys, Redis allocates at most as much memory as the
maxmemorysetting enables (however there are small extra allocations possible).
来自,Redis 官方
Redis 使用 zmalloc 方法(Redis
自己实现的内存分配方法)进行内存分配的时候,除了要分配 size
大小的内存之外,还会多分配 PREFIX_SIZE
大小的内存,用于冗余,这样保证高可用。
zmalloc
方法源码地址:https://github.com/antirez/redis-tools/blob/master/zmalloc.c):
另外,Redis 可以使用多种内存分配器来分配内存( libc、jemalloc、tcmalloc),默认使用 jemalloc,而 jemalloc 按照一系列固定的大小(8 字节、16 字节、32 字节……)来分配内存的。jemalloc 划分的内存单元如下图所示:
那么,按照这种情况,按固定大小块分配内存,肯定会出现内存碎片的情况,而且出现内存碎片的关键是,只分配预定义的、固定大小的内存块,而不是任意大小,它会选择比你请求大的最小可用块。
但是,兄弟没办法,这种浪费是不可避免的,因为 jemalloc 的设计就是如此,这是高可用的重要保障
频繁修改 Redis 中的数据也会产生内存碎片。
当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统。
这个在 Redis 官方文档中也有对应的原话:
文档地址:https://redis.io/topics/memory-optimization
这两个一搞,肯定会产生一些内存碎片,所以我们需要收回
如何查看 Redis 内存碎片的信息?
使用 info memory 命令即可查看 Redis 内存相关的信息
1 | info memory |
重点关注:
1 | used_memory:1073741824 # Redis 实际使用内存(1GB) |
如下命令查看是否正在整理
1 | info stats |
输出包含:
1 | active_defrag_running:1 # 1=正在整理,0=未运行 |
如何清理 Redis 内存碎片?
Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。好消息,这样不用自己清理了
如下启用主动碎片整理,直接通过 config set 命令将
activedefrag 配置项设置为 yes 即可。
1 | # 动态开启(无需重启) |
Redis 主动碎片清理的原理是这样的,和大部分内存碎片清理算法差不多,简单说一下吧
当 Redis 发现内存碎片过多时,它会在后台线程中:
- 找到那些占用连续物理内存但逻辑上分散的数据
- 将它们复制到新的连续内存区域
- 释放原来的碎片空间
- 最终使空闲内存变成大块连续区域,提高内存利用率
这个过程是渐进式的,不会一次性阻塞主线程。
具体什么时候清理需要通过下面两个参数控制:
1 | # 内存碎片占用空间达到 500mb 的时候开始清理 |
其中关于内存相关内容的计算逻辑如下
- 碎片空间 =
used_memory_rss - used_memory- 碎片率 =
used_memory_rss / used_memory- 触发条件 =
(碎片空间 ≥ ignore-bytes) AND (碎片率 ≥ 1 + threshold-lower/100)也就是说,
mem_fragmentation_ratio内存碎片率的值越大代表内存碎片率越严重。如果想要快速查看内存碎片率的话,你还可以通过下面这个命令
1 redis-cli -p 6379 info | grep mem_fragmentation_ratio
通过 Redis 自动内存碎片清理机制可能会对 Redis 的性能产生影响,我们可以通过下面两个参数来减少对 Redis 性能的影响:
1 | # 内存碎片清理所占用 CPU 时间的比例不低于 20% |
另外,重启节点可以做到内存碎片重新整理。如果你采用的是高可用架构的 Redis 集群的话,你可以将碎片率过高的主节点转换为从节点,以便进行安全重启
Redis性能优化实战
主要核心问题就是一个,Redis 是基于单线程模型实现的,也就是 Redis 是使用一个线程来处理所有的客户端请求的,而且尽管 Redis 使用了 NIO,由于 Redis 是单线程执行的特点,因此它对性能的要求非常苛刻了
在开始优化之前,我们必须理解Redis查询的性能瓶颈在哪里。常见的瓶颈包括:
- 网络往返时间(RTT):每次查询都需要与Redis服务器通信,网络延迟是主要瓶颈
- 单个命令执行:逐条查询,导致多次网络往返
- 数据结构选择不当:错误的数据结构导致查询效率低下
- 缓存设计不合理:缓存命中率低,大量请求穿透到数据库
- 连接管理不善:频繁创建和销毁连接,增加系统开销
性能测试与基准
在开始优化前,首先要了解当前 Redis 的性能情况
使用redis-benchmark工具:
Redis自带的性能测试工具可以模拟各种负载场景
来进行使用的示例,试一下这玩意,我都没咋用过说实话
1
2# 全量压测
redis-benchmark -h 127.0.0.1 -p 6379 -a yourpassword -t set,get,lpush,lpop,sadd,spop,hmget,hset -n 100000 -c 50 -d 100那么来看看输出,
requests per second就是 QPS,然后看96.875% <= 1.047 milliseconds这个百分比,意味着 99.99% 的请求响应时间在 1ms 内,延迟越低越好
或者我们压测一个特定的类型,例如压测 List 类型,压测我项目中的 userlist1
1
2# 压测 LPUSH/LPOP 命令,模拟消息队列场景
redis-benchmark -h 127.0.0.1 -p 6379 -t lpush,lpop -n 50000 -c 20 -d 100比较有用的就是压测大 Key 场景,模拟你实例中可能存在的大 Key
1
2# 压测 10KB 大小的 String Key,模拟大 Key 读写
redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -n 10000 -c 10 -d 10240当然,也完全可以使用Redis内置命令监控性能
例如,INFO,
1
2
3
4
5
6
7
8
9
10
11# 查看所有指标
redis-cli INFO
# 查看指定模块(推荐,输出更简洁)
redis-cli INFO server # 服务器信息(版本、运行时间、配置)
redis-cli INFO clients # 客户端连接(连接数、阻塞数)
redis-cli INFO memory # 内存占用(核心,排查 Swap/内存溢出)
redis-cli INFO stats # 核心统计(QPS、命中数、慢查询)
redis-cli INFO commandstats # 命令执行统计(各命令耗时、调用次数)
redis-cli INFO persistence # 持久化状态(RDB/AOF 耗时、阻塞)
redis-cli INFO cpu # CPU 占用(排查 CPU 竞争)比较常用的还有一个慢查询监控
SLOWLOG1
2
3
4
5
6
7
8
9
10
11# 查看慢查询配置(阈值/最大条数)
redis-cli CONFIG GET slowlog-log-slower-than # 慢查询阈值(微秒)
redis-cli CONFIG GET slowlog-max-len # 慢查询日志最大条数(默认 128)
# 修改阈值(临时生效,重启失效)
redis-cli CONFIG SET slowlog-log-slower-than 5000 # 改为 5ms,更严格监控
# 查看慢查询日志(返回最近 N 条)
redis-cli SLOWLOG GET 10 # 查看最近10条慢查询
redis-cli SLOWLOG LEN # 查看慢查询日志总数
redis-cli SLOWLOG RESET # 清空慢查询日志
然后就是 内存与 Key 监控工具,
MEMORY,MEMORY命令用于精准分析 Key 的内存占用1
2
3
4
5
6
7
8# 查看单个 Key 的内存占用
MEMORY USAGE user:1
# 手动整理内存碎片
MEMORY DEFRAG
# 查看内存碎片率(>1.5 说明碎片严重,会触发 Swap)
MEMORY STATS还有一个用的少一点的
MONITOR,这个是实施抓包,和 Arthas 的 trace 是类似的,实时输出 Redis 执行的所有命令,但是它会增加性能开销,用的人比较少了其他 Redis 性能监控工具
emmm,我一直用的是那个 Redis Assistant,直接可视化了,同学用的是小米开源的open-falcon
针对 Redis 的内存优化
合理的内存配置
首先,合理的内存配置,重要的一个就是 maxmemory
参数,它决定了Redis实例能够使用的最大内存量。
而且一个关键的决策点是:你的Redis主要用作缓存还是持久化存储?
- 作为缓存:你可以也需要为其设置一个明确的内存上限,并配置相应的淘汰策略(maxmemory-policy),当内存不足时自动移除旧数据。
- 作为持久化数据库:你需要更加谨慎。虽然也可配置
maxmemory,但必须确保有足够的冗余,并配合AOF和RDB持久化,防止数据丢失。在开发环境中,如果数据量可控,有时甚至会暂时不设上限以观察实际消耗,但这不适用于生产环境。
在拥有16GB物理内存的开发机上,为Redis分配3GB到GB是一个合理的起步区间。
| 配置项 | 建议值 | 说明 |
|---|---|---|
maxmemory |
系统内存的30-50% | 为操作系统和其他进程留出足够空间 |
maxmemory-policy |
volatile-lru/allkeys-lru | 根据业务场景选择淘汰策略 |
hash-max-ziplist-entries |
512 | Hash使用ziplist编码的最大entry数 |
hash-max-ziplist-value |
64 | Hash使用ziplist编码的最大value大小 |
至于maxmemory-policy,淘汰策略如下:
volatile-lru:只淘汰有过期时间的keyallkeys-lru:淘汰所有key(推荐用于纯缓存场景)volatile-ttl:优先淘汰即将过期的keynoeviction:不淘汰,写操作返回错误(用于数据完整性要求高的场景)
优化键值设计
主要就是缩短键值对的存储长度,而且Key设计规范如下
1 | # 推荐格式 |
一般情况下,Key长度控制在128字节以内,value大小建议小于10KB
内存碎片整理
内存碎片率 = used_memory_rss / used_memory
正常值差不多是 1.0 ~ 1.5
Redis 4.0+ 主动碎片整理配置如下:
1 | # redis.conf 配置 |
手动触发碎片整理
1 | # 查看内存情况 |
命令优化
查询语句优化
对于查询语句,我们首先要考虑
避免操作大 KEY,对于大 KEY,尤其是 Hash 的大KEY,考虑拆分
避免在生产环境使用 KEYS 命令等涉及到全表扫描的命令,改用SCAN来循环逐步遍历使用
使用 MGET 和 MSET
当需要一次性获取多个键值对时,使用
MGET系列命令可以减少网络开销,提高效率。1
2MSET key1 "value1" key2 "value2" key3 "value3"
MGET key1 key2 key3
对于慢查询分析的配置,你可以自己设阈值
1 | # redis.conf 配置 |
查看慢查询的相关命令如下
1 | # 查看慢查询日志 |
选择合适的数据结构
当存储的数据是字符串类型时,选择合适的数据结构非常关键。使用字符串可以实现简单的键值对存储,但如果一旦数据具有更复杂的结构,要优先考虑使用哈希表(Hash)或者列表
1 | # 使用字符串 |
而且可以多用有序集合,有序集合(Sorted Set)适用于需要排序和唯一性的场景,例如排行榜,虽然有序集合可能略微增加内存,但是它排序太快太好用了
来个实际例子,假设你有一个用户系统,需要根据 用户ID、手机号、邮箱、昵称 多种方式查询用户信息。你会怎么存?
那么,如何根据手机号查用户? 你只能遍历所有 user:* 的
key,执行 SCAN 或 KEYS
这就是典型的数据结构误用,只考虑了写入,没考虑查询。
上述情况,我们可以使用 Set,因为它比较适合存储对象
而且也可以使用 Hash 存储对象加上索引,因为 Hash 本身不支持反向查询。我们需要手动构建索引。但是它的存储空间会翻倍,而且假如索引失败了,会导致脏数据
持久化优化
RDB 优化
RDB是在指定时间间隔内生成内存数据的快照文件。默认配置可能过于频繁,在Windows上可能引起短暂的I/O卡顿。
打开配置文件,找到类似以下的段落:
1 | # 触发条件(时间间隔 变更次数) |
AOF优化
在Windows上,我们需要特别关注AOF的写入策略
1 | # 开启AOF |
对于Windows环境,坚持使用 appendfsync everysec。此外,可以启用AOF重写压缩,防止文件无限膨胀
1 | # AOF重写配置 |
这表示当AOF文件比上次重写后的大小增长100%,且至少达到64MB时,自动触发重写。
使用混合持久化
Redis 4.0+ 推荐使用混合持久化方案:
1 | # 开启混合持久化 |
网络和连接管理
服务与Redis的连接
Redis在Windows上通常以服务形式运行,其网络绑定和客户端连接处理需要适配Windows的网络栈。
注意,当 bind 设置为非本地地址或注释掉时,Redis的“保护模式”会生效,阻止外部连接。你需要设置密码或显式关闭保护模式
而且 Windows 的TCP连接管理与Linux不同。调整 tcp-keepalive 可以帮助更快地检测和清理死连接,释放资源
1 | tcp-keepalive 600 |
将值设置为600秒,是一个合理的间隔。
另外,Windows系统对单个进程的并发连接数有一定限制。Redis的默认maxclients是10000
1 | # 最大客户端连接数 |
而且对于缓冲区,也可以自己根据经验来调整
1 | # 客户端输出缓冲区限制 |
使用Pipeline批量操作
Pipeline是Redis提供的批量操作机制,它允许我们将多个命令打包成一个请求发送给Redis,减少网络往返次数。
推荐使用管道和事务,客户端应使用管道(Pipeline)减少网络往返,通过使用 Pipeline,可以将多个命令一次性发送到服务器,减少了网络往返的时间。这在需要执行多个命令的场景下尤为有效。
在Redis中,网络通信是性能的主要瓶颈。一次网络请求的延迟通常在1ms左右,而Redis命令执行时间可能只有0.1ms。通过Pipeline,我们可以将多个命令合并为一次网络请求,大幅减少网络开销。
1 | /** |
应用层优化
缓存策略优化
合理的缓存策略可以显著提升系统响应速度并降低数据库负载
回忆一下三个策略
| 策略类型 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 旁路缓存 | 先读缓存,未命中则读DB并写入缓存;更新时先更新DB再删除缓存 | 简单、一致性好 | 存在短暂不一致 | 大多数读多写少场景 |
| 读写穿透 | 应用只与缓存交互,缓存负责与DB同步 | 应用层简单 | 缓存层复杂 | 缓存中间件场景 |
| 异步缓存写入 | 先写缓存,异步批量写入DB | 写性能高 | 数据丢失风险 | 允许短暂数据丢失场景 |
对于缓存穿透,查询不存在的数据,使用布隆过滤器或者缓存空值
对于缓存击穿,热点key过期,可以不设置物理过期,在value中存储逻辑过期时间,或者热点数据永不过期,后台异步更新
对于缓存雪崩,大量key同时过期,可以设置随机TTL,或者多级缓存,不同层级不同过期时间,比较好用的也有下面说的提前加载热点数据
缓存预热
缓存预热是指在系统启动或流量高峰期前,将热点数据提前加载到Redis中。这样可以避免在流量高峰时,大量请求穿透到数据库,导致系统性能下降。
延迟加载
延迟加载是一种按需加载数据的策略,可以有效减少初始负载和内存占用。
例如,对 Hash 字段懒加载
1 | // 只加载需要的字段,而非整个对象 |







