《硬核Redis》阅读笔记

硬核Redis

1 Redis为什么变慢了?一文讲透如何排查Redis性能问题

Redis为什么变慢了?一文讲透如何排查Redis性能问题

本篇文章的前

Redis真的变慢了吗?

比较高效的做法是,在服务内部集成链路追踪,也就是在服务访问外部依赖的出入口,记录下每次请求外部依赖的响应延时。

使用复杂度过高的命令

# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500

获取慢日志中的内容

SLOWLOG get 5

最佳实践

  • 尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
  • 执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回

操作bigkey

如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。同样的,当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 我们一般称之为 bigkey。

BigKey扫描命令

redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01

扫描命令注意事项

  • 对线上实例进行 bigkey 扫描时,Redis 的 OPS 会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定 -i 参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒
  • 扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况

最佳实践

  • 业务应用尽量避免写入 bigkey
  • 如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响
  • 如果你使用的 Redis 是 6.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行

集中过期

Redis 的过期数据采用被动过期 + 主动过期两种策略:

  • 被动过期:只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
  • 主动过期:Redis 内部维护了一个定时任务,默认每隔 100 毫秒(1秒10次)就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环
    注意,这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。

最佳实践:

  • 集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期 key 的压力
  • 如果你使用的 Redis 是 4.0 以上版本,可以开启 lazy-free 机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程
# 释放过期 key 的内存,放到后台线程执行
lazyfree-lazy-expire yes

实例内存达到上限

最佳实践

  • 避免存储 bigkey,降低释放内存的耗时
  • 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整)
  • 拆分实例,把淘汰 key 的压力分摊到多个实例上
  • 如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes)

fork耗时严重

你可以在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒。

# 上一次 fork 耗时,单位微秒
latest_fork_usec:59477

这个时间就是主进程在 fork 子进程期间,整个实例阻塞无法处理客户端请求的时间。

最佳实践:

  • 控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
  • 合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite
  • Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久
  • 降低主从库全量同步的概率:适当调大 repl-backlog-size 参数,避免主从全量同步

开启内存大页

我们都知道,应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是 4KB。

Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存。

应用程序每次向操作系统申请的内存单位变大了,但这也意味着申请内存的耗时变长。

最佳实践:关闭内存大页。

其实,操作系统提供的内存大页机制,其优势是,可以在一定程序上降低应用程序申请内存的次数。

但是对于 Redis 这种对性能和延迟极其敏感的数据库来说,我们希望 Redis 在每次申请内存时,耗时尽量短,所以我不建议你在 Redis 机器上开启这个机制。

开启AOF

幸运的是,Redis 提供了一个配置项,当子进程在 AOF rewrite 期间,可以让后台子线程不执行刷盘(不触发 fsync 系统调用)操作。

这相当于在 AOF rewrite 期间,临时把 appendfsync 设置为了 none,配置如下:

# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作
# 相当于在这期间,临时把 appendfsync 设置为了 none
no-appendfsync-on-rewrite yes

当然,开启这个配置项,在 AOF rewrite 期间,如果实例发生宕机,那么此时会丢失更多的数据,性能和数据安全性,你需要权衡后进行选择。

如果占用磁盘资源的是其他应用程序,那就比较简单了,你需要定位到是哪个应用程序在大量写磁盘,然后把这个应用程序迁移到其他机器上执行就好了,避免对 Redis 产生影响。

当然,如果你对 Redis 的性能和数据安全都有很高的要求,那么我建议从硬件层面来优化,更换为 SSD 磁盘,提高磁盘的 IO 能力,保证 AOF 期间有充足的磁盘资源可以使用。

绑定CPU(不推荐)

Redis 在 6.0 版本已经推出了这个功能,我们可以通过以下配置,对主线程、后台线程、后台 RDB 进程、AOF rewrite 进程,绑定固定的 CPU 逻辑核心:

# Redis Server 和 IO 线程绑定到 CPU核心 0,2,4,6
server_cpulist 0-7:2

# 后台子线程绑定到 CPU核心 1,3
bio_cpulist 1,3

# 后台 AOF rewrite 进程绑定到 CPU 核心 8,9,10,11
aof_rewrite_cpulist 8-11

# 后台 RDB 进程绑定到 CPU 核心 1,10,11
# bgsave_cpulist 1,10-1

最佳实践

这里我需要提醒你的是,一般来说,Redis 的性能已经足够优秀,除非你对 Redis 的性能有更加严苛的要求,否则不建议你绑定 CPU。

使用Swap

什么是 Swap?为什么使用 Swap 会导致 Redis 的性能下降?

如果你对操作系统有些了解,就会知道操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。

问题就在于,当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍!

# 先找到 Redis 的进程 ID
$ ps -aux | grep redis-server

# 查看 Redis Swap 使用情况
$ cat /proc/$pid/smaps | egrep '^(Swap|Size)'

预防实践

  • 增加机器的内存,让 Redis 有足够的内存可以使用
  • 整理内存空间,释放出足够的内存供 Redis 使用,然后释放 Redis 的 Swap,让 Redis 重新使用内存

碎片整理

最佳实践

  • 如果你使用的是 Redis 4.0 以下版本,只能通过重启实例来解决
  • 如果你使用的是 Redis 4.0 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理
# 开启自动内存碎片整理(总开关)
activedefrag yes

# 内存使用 100MB 以下,不进行碎片整理
active-defrag-ignore-bytes 100mb

# 内存碎片率超过 10%,开始碎片整理
active-defrag-threshold-lower 10
# 内存碎片率超过 100%,尽最大努力碎片整理
active-defrag-threshold-upper 100

# 内存碎片整理占用 CPU 资源最小百分比
active-defrag-cycle-min 1
# 内存碎片整理占用 CPU 资源最大百分比
active-defrag-cycle-max 25

# 碎片整理期间,对于 List/Set/Hash/ZSet 类型元素一次 Scan 的数量
active-defrag-max-scan-fields 1000

网络带宽过载

Redis 的高性能,除了操作内存之外,就在于网络 IO 了,如果网络 IO 存在瓶颈,那么也会严重影响 Redis 的性能。

其他原因

1:频繁短连接

你的业务应用,应该使用长连接操作 Redis,避免频繁的短连接。

频繁的短连接会导致 Redis 大量时间耗费在连接的建立和释放上,TCP 的三次握手和四次挥手同样也会增加访问延迟。

2:运维监控

3:其它程序争抢资源

最后需要提醒你的是,你的 Redis 机器最好专项专用,只用来部署 Redis 实例,不要部署其他应用程序,尽量给 Redis 提供一个相对「安静」的环境,避免其它程序占用 CPU、内存、磁盘资源,导致分配给 Redis 的资源不足而受到影响。

2 如何从0到1构建一个稳定、高性能的Redis集群

如何从0到1构建一个稳定、高性能的Redis集群?(附16张图解)

从最简单的开始:单机版 Redis

数据持久化:有备无患

RDB:只持久化某一时刻的数据快照到磁盘上(创建一个子进程来做)
AOF:每一次写操作都持久到磁盘(主线程写内存,根据策略可以配置由主线程还是子线程进行数据持久化)

RDB 采用二进制 + 数据压缩的方式写磁盘,这样文件体积小,数据恢复速度也快
AOF 记录的是每一次写命令,数据最全,但文件体积大,数据恢复速度慢

如果你的业务对于数据丢失不敏感,采用 RDB 方案持久化数据
如果你的业务对数据完整性要求比较高,采用 AOF 方案持久化数据

我们可以对 AOF 文件定时 rewrite,避免这个文件体积持续膨胀,这样在恢复时就可以缩短恢复时间了。

Redis 4.0 以上版本才支持混合持久化。

主从复制:多副本

缩短不可用时间:master 发生宕机,我们可以手动把 slave 提升为 master 继续提供服务
提升读性能:让 slave 分担一部分读请求,提升应用的整体性能

当 master 宕机时,我们需要「手动」把 slave 提升为 master,这个过程也是需要花费时间的。

虽然比恢复数据要快得多,但还是需要人工介入处理。一旦需要人工介入,就必须要算上人的反应时间、操作时间,所以,在这期间你的业务应用依旧会受到影响。

哨兵:故障自动切换

哨兵工作流程

  1. 哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 哨兵发现异常,发起主从切换

多个哨兵的流程

  1. 多个哨兵每间隔一段时间,询问 master 是否正常
  2. master 正常回复,表示状态正常,回复超时表示异常
  3. 一旦有一个哨兵判定 master 异常(不管是否是网络问题),就询问其它哨兵,如果多个哨兵(设置一个阈值)都认为 master 异常了,这才判定 master 确实发生了故障
  4. 多个哨兵经过协商后,判定 master 故障,则发起主从切换

在选举哨兵领导者时,我们可以制定这样一个选举规则:

  1. 每个哨兵都询问其它哨兵,请求对方为自己投票
  2. 每个哨兵只投票给第一个请求投票的哨兵,且只能投票一次
  3. 首先拿到超过半数投票的哨兵,当选为领导者,发起主从切换

这个算法还规定节点的数量必须是奇数个,这样可以保证系统中即使有节点发生了故障,剩余超过「半数」的节点状态正常,依旧可以提供正确的结果,也就是说,这个算法还兼容了存在故障节点的情况。

不足:当你的写请求量越来越大时,一个 master 实例可能就无法承担这么大的写流量了。

分片集群:横向扩展

3 Redis最佳实践:7个维度+43条使用规范,带你彻底玩转Redis

Redis最佳实践:7个维度+43条使用规范,带你彻底玩转Redis | 附实践清单

如何使用 Redis 更节省内存?

控制 key 的长度

避免存储 bigkey

  • String:大小控制在 10KB 以下
  • List/Hash/Set/ZSet:元素数量控制在 1 万以下

选择合适的数据类型

例如,String、Set 在存储 int 数据时,会采用整数编码存储。Hash、ZSet 在元素数量比较少时(可配置),会采用压缩列表(ziplist)存储,在存储比较多的数据时,才会转换为哈希表和跳表。

String、Set:尽可能存储 int 类型数据
Hash、ZSet:存储的元素数量控制在转换阈值之下,以压缩列表存储,节约内存

把 Redis 当作缓存使用

应用写入到 Redis 中的数据,尽可能地都设置「过期时间」。

实例设置 maxmemory + 淘汰策略

数据压缩后写入 Redis

如何持续发挥 Redis 的高性能?

避免存储 bigkey

你的业务应用尽量不要存储 bigkey,避免操作延迟发生。

如果你确实有存储 bigkey 的需求,你可以把 bigkey 拆分为多个小 key 存储。

开启 lazy-free 机制

如果你无法避免存储 bigkey,那么我建议你开启 Redis 的 lazy-free 机制。(4.0+版本支持)

当开启这个机制后,Redis 在删除一个 bigkey 时,释放内存的耗时操作,将会放到后台线程中去执行,这样可以在最大程度上,避免对主线程的影响。

不使用复杂度过高的命令

所以,你需要避免执行例如 SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE 等聚合类命令。

对于这种聚合类操作,我建议你把它放到客户端来执行,不要让 Redis 承担太多的计算工作。

执行 O(N) 命令时,关注 N 的大小

在查询数据时,你要遵循以下原则:

  1. 先查询数据元素的数量(LLEN/HLEN/SCARD/ZCARD)
  2. 元素数量较少,可一次性查询全量数据
  3. 元素数量非常多,分批查询数据(LRANGE/HASCAN/SSCAN/ZSCAN)

关注 DEL 时间复杂度

  1. List类型:执行多次 LPOP/RPOP,直到所有元素都删除完成
  2. Hash/Set/ZSet类型:先执行 HSCAN/SSCAN/SCAN 查询元素,再执行 HDEL/SREM/ZREM 依次删除每个元素

批量命令代替单个命令

批量操作相比于多次单个操作的优势在于,可以显著减少客户端、服务端的来回网络 IO 次数。

所以我给你的建议是:

  1. String / Hash 使用 MGET/MSET 替代 GET/SET,HMGET/HMSET 替代 HGET/HSET
  2. 其它数据类型使用 Pipeline,打包一次性发送多个命令到服务端执行

避免集中过期 key

Redis 清理过期 key 是采用定时 + 懒惰的方式来做的,而且这个过程都是在主线程中执行。

使用长连接操作 Redis,合理配置连接池

你的业务应该使用长连接操作 Redis,避免短连接。

当使用短连接操作 Redis 时,每次都需要经过 TCP 三次握手、四次挥手,这个过程也会增加操作耗时。

同时,你的客户端应该使用连接池的方式访问 Redis,并设置合理的参数,长时间不操作 Redis 时,需及时释放连接资源。

只使用 db0

  1. 在一个连接上操作多个 db 数据时,每次都需要先执行 SELECT,这会给 Redis 带来额外的压力
  2. 使用多个 db 的目的是,按不同业务线存储数据,那为何不拆分多个实例存储呢?拆分多个实例部署,多个业务线不会互相影响,还能提高 Redis 的访问性能
  3. Redis Cluster 只支持 db0,如果后期你想要迁移到 Redis Cluster,迁移成本高

使用读写分离 + 分片集群

如果你的业务写请求量很大,单个 Redis 实例已无法支撑这么大的写流量,那么此时你需要使用分片集群,分担写压力。

不开启 AOF 或 AOF 配置为每秒刷盘

如果对于丢失数据不敏感的业务,我建议你不开启 AOF,避免 AOF 写磁盘拖慢 Redis 的性能。

如果确实需要开启 AOF,那么我建议你配置为 appendfsync everysec,把数据持久化的刷盘操作,放到后台线程中去执行,尽量降低 Redis 写磁盘对性能的影响。

使用物理机部署 Redis

Redis 在做数据持久化时,采用创建子进程的方式进行。

而创建子进程会调用操作系统的 fork 系统调用,这个系统调用的执行耗时,与系统环境有关。

虚拟机环境执行 fork 的耗时,要比物理机慢得多,所以你的 Redis 应该尽可能部署在物理机上。

关闭操作系统内存大页机制

高可靠

按业务线部署实例

提升可靠性的第一步,就是「资源隔离」。

你最好按不同的业务线来部署 Redis 实例,这样当其中一个实例发生故障时,不会影响到其它业务。

这种资源隔离的方案,实施成本是最低的,但成效却是非常大的。

部署主从集群

如果你只使用单机版 Redis,那么就会存在机器宕机服务不可用的风险。

所以,你需要部署「多副本」实例,即主从集群,这样当主库宕机后,依旧有从库可以使用,避免了数据丢失的风险,也降低了服务不可用的时间。

在部署主从集群时,你还需要注意,主从库需要分布在不同机器上,避免交叉部署。

这么做的原因在于,通常情况下,Redis 的主库会承担所有的读写流量,所以我们一定要优先保证主库的稳定性,即使从库机器异常,也不要对主库造成影响。

而且,有时我们需要对 Redis 做日常维护,例如数据定时备份等操作,这时你就可以只在从库上进行,这只会消耗从库机器的资源,也避免了对主库的影响。

合理配置主从复制参数

在部署主从集群时,如果参数配置不合理,也有可能导致主从复制发生问题:

主从复制中断
从库发起全量复制,主库性能受到影响
在这方面我给你的建议有以下 2 点:

设置合理的 repl-backlog 参数:过小的 repl-backlog 在写流量比较大的场景下,主从复制中断会引发全量复制数据的风险
设置合理的 slave client-output-buffer-limit:当从库复制发生问题时,过小的 buffer 会导致从库缓冲区溢出,从而导致复制中断

部署哨兵集群,实现故障自动切换

只部署了主从节点,但故障发生时是无法自动切换的,所以,你还需要部署哨兵集群,实现故障的「自动切换」。

而且,多个哨兵节点需要分布在不同机器上,实例为奇数个,防止哨兵选举失败,影响切换时间。

以上这些就是保障 Redis「高可靠」实践优化,你应该也发现了,这些都是部署和运维层的优化。

除此之外,你可能还会对 Redis 做一些「日常运维」工作,这时你要注意哪些问题呢?

日常运维 Redis 需要注意什么?

禁止使用 KEYS/FLUSHALL/FLUSHDB 命令

执行这些命令,会长时间阻塞 Redis 主线程,危害极大,所以你必须禁止使用它。

如果确实想使用这些命令,我给你的建议是:

SCAN 替换 KEYS
4.0+版本可使用 FLUSHALL/FLUSHDB ASYNC,清空数据的操作放在后台线程执行

扫描线上实例时,设置休眠时间

不管你是使用 SCAN 扫描线上实例,还是对实例做 bigkey 统计分析,我建议你在扫描时一定记得设置休眠时间。

防止在扫描过程中,实例 OPS 过高对 Redis 产生性能抖动。

慎用 MONITOR 命令

从库必须设置为 slave-read-only

你的从库必须设置为 slave-read-only 状态,避免从库写入数据,导致主从数据不一致。

除此之外,从库如果是非 read-only 状态,如果你使用的是 4.0 以下的 Redis,它存在这样的 Bug:

从库写入了有过期时间的数据,不会做定时清理和释放内存。

这会造成从库的内存泄露!这个问题直到 4.0 版本才修复,你在配置从库时需要格外注意。

合理配置 timeout 和 tcp-keepalive 参数

调整 maxmemory 时,注意主从库的调整顺序

Redis 安全如何保证?

在这里插入图片描述

4 颠覆认知——Redis会遇到的15个「坑」

颠覆认知——Redis会遇到的15个「坑」

常见命令有哪些坑?

1) 过期时间意外丢失?

在这里插入图片描述
总结:如果一个key需要设置过期时间,后续修改的操作也应该设置过期时间。

2) DEL 竟然也会阻塞 Redis?

https://redis.io/commands/del/

Time complexity:
O(N) where N is the number of keys that will be removed.
When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash.
Removing a single key that holds a string value is O(1).

总结:不同数据结构使用del命令的时间复杂度不同,对于大key不应该执行该命令。谨慎存储大key,对于集合类型数据应该先查询元素数量、判断数量多少选择批量或者del删除。

3) RANDOMKEY 竟然也会阻塞 Redis?

RANDOMKEY
randomkey

随机获取一个key
在这里插入图片描述
总结:由于过期时间的关系,当随机到一个过期的key会做清除的动作然后返回key,如果批量过期的时候执行该命令会导致循环清除过期的key。如果在从库会有更大的问题,因为从库的del命令是主库发过来的。该命令能不用则不用。

4) O(1) 复杂度的 SETBIT,竟然会导致 Redis OOM?

在这里插入图片描述
总结: 当你在使用 SETBIT 时,也一定要注意 offset 的大小,操作过大的 offset 也会引发 Redis 卡顿。

5) 执行 MONITOR 也会导致 Redis OOM?

https://redis.io/commands/monitor/

总结:你需要谨慎使用 MONITOR,尤其在 QPS 很高的情况下。

数据持久化有哪些坑?

5 把Redis当作队列来用,真的合适吗

把Redis当作队列来用,真的合适吗?

redis 队列 命令 官网

使用 push 和 pop 命令来生产和消费消息

在这里插入图片描述
基于 lpop 或者 rpop 的伪代码如下所示

while true:
msg = redis.rpop("queue")
if msg == null:
continue
handle(msg)

当一段时间内没有新的消费生产的时候,会存在cpu时间片的浪费在无意义的循环上面。
改进方案,当获取不到消息的时候,休眠一小会,例如2s,这样会带来一个新的问题,消息延迟。
至多为2s。如果减小时间就会增大cpu空转的比例。

阻塞获取

在这里插入图片描述

blpop list1 0 ,参数0代表不设置过期时间,一直阻塞。

在这里插入图片描述
工具写入队列值

在这里插入图片描述
查看redis-cli

在这里插入图片描述
可以看到 成功返回 值2 和阻塞时间。

在这里插入图片描述
如果指定时间内没有获取到则返回null

在这里插入图片描述
那么现在这种模型的消息队列有什么问题呢?

主要有两点 消息丢失无法重复消费

list 这一个结构仅仅支持最基本的添加和删除,无法对应多消费者的模型,pop后就从队列中删除了。

执行 pop 后如果程序宕机那么消息就会丢失。

pub / sub 发布 订阅

在这里插入图片描述

6 深度剖析:Redis分布式锁到底安全吗

深度剖析:Redis分布式锁到底安全吗

7 缓存和数据库一致性问题,看这篇就够了

缓存和数据库一致性问题,看这篇就够了

8 读懂Redis源码,我总结了这7点心得

读懂Redis源码,我总结了这7点心得

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值