目录
4.4.2.大 Key 对 「AOF 重写」和「RDB快照」的影响
5.3.6.Redis Cluster的Gossip通信机制
6.2.3.Redis 事务支持回滚吗?为什么Redis 不支持事务回滚?
6.3.5.Redis6.0默认开启多线程么?如何开启和设置线程数?
7.2.2.基于Streamer的消息队列解决方案:redis专门为消息队列设计的数据类型
7.3.Redis变慢CheckList \ 如何排查Redis性能问题
7.6.2.Read/Write Through(读穿 / 写穿)策略
7.9.1.Redis cluster模式下如何查询多个key,具体的查询过程是怎样的?
7.11.场景:有台 8 核机器,只部署 Redis,有什么办法可以尽可能提高 Redis 性能
Redis 五种数据类型的应用场景:
Redis 后续版本又支持四种数据类型,它们的应用场景如下:

- struct sds{
- int len; // buf数组已经使用字符串的长度
- int free; // buf数组未使用的长度
- char buf[]; // 保存字符串
- };
Q1:为什么要使用SDS,而不使用C字符串?
A1:
背景:redis作为缓存数据库,数据经常被修改,造成内存重分配,影响性能
原因详述
- // 哈希表
- typedef struct dictEntry {
- void* key; // 键
- union { // 值
- void* val;
- uint64_t u64;
- int64_t s64;
- } v;
- struct dictEntry* next; // 指向下一个哈希节点,形成链表
- }dictEntry_t;
-
- struct dictht {
- dictEntry_t **table; // 数组,每个元素都是dictEntry_t*,它是一个链表头,所有冲突的key挂在相同链表上
- unsigned long size; // 哈希表容量大小
- unsigned long used; // 当前已经使用大小
- }
-
- // 字典
- struct dict {
- dictht ht[2]; // 2个哈希表: ht[0]正常情况下使用, ht[1]在rehash时使用
- int rehashidx; // rehash索引 (没进行rehash时,该值为-1)
- }
- ```
Q1:为什么要进行rehash
A1:哈希表中键值对的增加/减少,都可能导致rehash(为了使哈希表的 负载因子 维持在合理的范围内):一般进行2倍扩充(算法导论中的平摊分析)
Q2:rehash过程
A2:
在rehash过程中,新增加的(key,value)键值对,怎么处理?
Redis为什么使用skiplist,不使用红黑树
范围查找的时候,平衡树比skiplist操作要复杂
在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现
而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现
平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
从内存占用上来说,skiplist比平衡树更灵活一些
一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
查找单个key,skiplist和平衡树的时间复杂度都为O(logn),大体相当
从算法实现难度上来比较,skiplist比平衡树要简单得多
Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
redis是单线程程序,当运行一个消耗较大的请求(大key删除),会导致所有请求排队等待
在redis4.0之前,没有lazy-free,DBA只能通过取巧的方法,类似scan big key,每次删除100个元素;但是面对“被动删除键”的场景,这种取巧的删除就无能为力了
lazy-free的使用分为2类
主动删除:unlink
被动删除:与下面4个配置有关
lazyfree-lazy-eviction:redis内存达到maxmeory && 设置有淘汰策略
lazyfree-lazy-expire:设有TTL的key,达到过期后
slave-lazy-flush:salve全量数据同步时,slave在加载master的rdb文件前,会运行flushall清理自己的数据
lazyfree-lazy-server-del:隐式del操作,如rename
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory
Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
1.不进行数据淘汰的策略
noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
2.进行数据淘汰的策略
针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。 在设置了过期时间的数据中进行淘汰:
在所有数据范围内进行淘汰:
Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。
Redis 共有三种数据持久化的方式:
数据持久化:redis是内存数据库,把数据保存在磁盘中,就是数据持久化
rdb备份,是每次将数据库中的所有键值对,保存到文件中
在指定时间间隔内,将内存中的数据和操作,通过【快照】的方式保存到RDB文件
因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。为了解决这个问题,Redis 增加了 RDB 快照。所谓的快照,就是记录某一个瞬间东西。(所以,RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据)
Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置:
- save 900 1 # 900 秒之内,对数据库进行了至少 1 次修改;
- save 300 10 # 300 秒之内,对数据库进行了至少 10 次修改;
- save 60 10000 # 60 秒之内,对数据库进行了至少 10000 次修改
优点
缺点
SAVE:在主线程执行,阻塞主线程
BGSAVE:创建子进程,专门用于写入rdb文件,主线程不会阻塞
RDB 在执行快照的时候,数据能修改吗?
可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW)。
执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。

如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。

Redis将每次更新写操作命令(这里不是数据,而是命令),都以追加方式写入AOF文件。重启时,只需要从头到尾执行一次AOF中的指令,可以恢复数据。

为什么先执行命令,再把数据写入日志呢?(为什么不使用WAL)
Reids 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个好处。
当然,这样做也会带来风险:
先来看看,Redis 写入 AOF 日志的过程,如下图:

具体说说:
Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。 在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:
Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
- 可以做到基本不丢数据,但是因为在执行写命令后有一个同步刷盘操作,不可避免的会影响主线程性能
- 最多可能丢失1条数据(因为AOF采用的不是WAL)
Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
优点
缺点
1. 为什么要引入rewrite机制
AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制。
AOF重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,全盘读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。
2. 什么情况下触发rewrite
- auto-aof-rewrite-percentage 100
- auto-aof-rewrite-min-size 64mb
3. AOF如何实现重写
AOF文件持续增长而过大时,会触发rewrite
4. 如何保证在rewrite时,主进程能继续提供服务?
触发重写机制后,主进程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
但是重写过程中,主进程依然可以正常处理命令,那问题来了,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,那么会发生写时复制,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?
为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。

也就是说,在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:
当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
信号函数执行完后,主进程就可以继续像往常一样处理命令了。
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
假设,RDB备份时间为N,执行过程如下
字节一二面时,被问到:Redis 的大 Key 对持久化有什么影响?
分别说说AOF 3种写回磁盘策略,在持久化大 Key 的时候,会影响什么?
当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
当使用 Everysec 策略的时候,由于是异步执行 fsync() 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。
当使用 No 策略的时候,由于永不执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):
Redis实现高可用,在于提供多个节点,通常有三种部署模式:主从模式,哨兵模式,集群模式。

主从库数据第一次同步的三个阶段
旧版本:复制功能的缺陷(断线后,重新复制):网络断线后,会重新全量复制
新版本解决了断线重复复制问题:复制积压缓冲区,主服务器和从服务器都维护了自己的复制偏移量
因此,通过主从服务器的复制偏移量,就能知道二者是否处于一致性状态!
Q1:Redis主从节点是长连接还是短连接?
A1:长连接
Q2:怎么判断 Redis 某个节点是否正常工作?
A2:通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接
Q3:主从复制架构中,过期key如何处理?
A3:主节点处理了一个key或者通过淘汰算法淘汰了一个key,这个时间主节点模拟一条del命令发送给从节点,从节点收到该命令后,就进行删除key的操作。
Q4:Redis 是同步复制还是异步复制?
A4:Redis 主节点每次收到写命令之后,先写到内部的缓冲区,然后异步发送给从节点
Q5:主从复制中两个 Buffer(replication buffer 、repl backlog buffer)有什么区别?
A5:replication buffer 、repl backlog buffer 区别如下:
在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。
为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。

哨兵将会监控集群中所有的节点:一般为了防止单哨兵节点故障,将配置多个哨兵协同合作
切换过程
主观下线:哨兵A检测到主节点下线后,将不会立即切换主节点,而是认为它客观下线
询问:哨兵A会询问监听该主节点的其他哨兵,收集汇总信息,当有足够多的主观下限信息时,判断是否为客观下线
选取新领头哨兵:当有一个哨兵判断为客观下限后,将会选举出领头哨兵,由它进行切换主节点操作
主从复制面临的3个问题
哨兵机制的基本流程 ==> 哨兵机制是实现主从库自动切换的关键机制,它有效解决了主从复制模式下故障转移的3个问题。哨兵主要负责的就是3个任务,如下:
引入:哨兵集群的配置,只需要设置主库的IP和端口,并没有配置其他哨兵的连接信息
问题:这些哨兵不知道彼此的地址,又是怎么组成集群的呢?==>哨兵集群的组成和运行机制
哨兵集群的组成和运行机制:基于发布订阅的哨兵集群组成
一个集群通常有多个服务器节点组成
槽指派
将槽分配到集群中某个节点的算法
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽
各个分片组成主从复制:上面已经知道,集群中某个节点维护了某个区域的槽
故障检测
故障检测:每个节点会定时向集群中其他节点发送ping,检测对方是否在线
故障转移/raft选主算法
raft共识算法就是三个主要模块:leader选举,数据同步和分区共识
leader选举
所有节点都有一个定时器(每个节点的定时器都是随机值)。该定时器表示收到Leader的心跳包。
如果在定时器内没有收到Leader的心跳包,则自己变成Candidate状态,参与竞选Leader。(选举超时,自己当成候选者)
然后所有任参与选票,候选者的投票只会投给自己,其他人则投给某个候选者(可能出现多个候选者情况)
如果自己的票数过半,则成为新的Leader。
如果此时有人跟自己一样同时竞选Leader,那么这两个人都会在起一个定时器(随机值),进行在一轮的选票
数据同步
当leader接收到请求后,将该请求写到自己的日志中
leader将该请求发送给其他节点,要求其他节点也写到日志中
其他节点将请求写到日志后,回一个ack给leader
leader统计有过半节点回复了ack后,将请求数据更新
leader发送请求给其他节点,要求其他节点将数据更新
分区共识
集群5个节点,一开始1个leader和4个Follower
然后网络出现问题,把1个leader和1个Follower划分为一个区A。而其他三个节点为另外一个区B
另外三个节点为一个区A后,进行重新选举leader
有个clientA向分区A发送请求,但是这个时候分区只有两个节点,没有超过半数所以该请求无效
有个clientB向分区B发送器请求,有过半节点同意该请求,所以请求有效,更新数据
当网络恢复后,出现了两个leaderA和leaderB。因为leaderB是新一代的leader,所以leaderA放弃做leader。并且将分区B的数据更新到分区A的每个节点上
问题:一致性并不一定代表完全正确性!三个可能结果:成功,失败,unknown
Gossip:流言蜚语
缺陷
为了让让集群中的每个实例都知道其他所有实例的状态信息,Redis 集群规定各个实例之间按照 Gossip 协议来通信传递信息。

上图展示了主从架构的 Redis Cluster 示意图,其中实线表示节点间的主从复制关系,而虚线表示各个节点之间的 Gossip 通信。
Redis Cluster 中的每个节点都维护一份自己视角下的当前整个集群的状态,主要包括:
也就是说上面的信息,就是集群中Node相互八卦传播流言蜚语的内容主题,而且比较全面,既有自己的更有别人的,这么一来大家都相互传,最终信息就全面而且一致了。
Redis Cluster 的节点之间会相互发送多种消息,较为重要的如下所示:
通过上述这些消息,集群中的每一个实例都能获得其它所有实例的状态信息。这样一来,即使有新节点加入、节点故障、Slot 变更等事件发生,实例间也可以通过 PING、PONG 消息的传递,完成集群状态在每个实例上的同步。下面,我们依次来看看几种常见的场景。
客户端订阅服务端的频道
当服务器向该频道发送消息时,频道中所有的客户端收到该消息,执行响应的动作
大概的意思是,作者不支持事务回滚的原因有以下两个:
这里不支持事务回滚,指的是不支持事务运行时错误的事务回滚
Q:Redis multi/EXEC命令、lua脚本可否保证原子性操作
A:Redis中的事务是不满足原子性的,详细分析见下:
存在「语法错误」的情况下,所有命令都不会执行
存在「运行错误」的情况下,除执行中出现错误的命令外,其他命令都能正常执行
那么为什么Redis不支持回滚呢,官方文档给出了说明,大意如下:
Redis命令失败只会发生在语法错误或数据类型错误的情况,这一结果都是由编程过程中的错误导致,这种情况应该在开发环境中检测出来,而不是生产环境
不使用回滚,能使Redis内部设计更简单,速度更快
回滚不能避免编程逻辑中的错误,如果想要将一个键的值增加2却只增加了1,这种情况即使提供回滚也无法提供帮助
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
redis单线程模型是怎样的?

图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情:
初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:
之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:
我们都知道单线程的程序是无法利用服务器的多核 CPU 的,那么早期 Redis 版本的主要工作(网络 I/O 和执行命令)为什么还要使用单线程呢?我们不妨先看一下Redis官方给出的答案。

核心意思是:CPU 并不是制约 Redis 性能表现的瓶颈所在,更多情况下是受到「内存大小」和「网络I/O」的限制,所以 Redis 核心网络模型使用单线程并没有什么问题,如果你想要使用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式。
除了上面的官方回答,选择单线程的原因也有下面的考虑。
使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在「网络 I/O」 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。
Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。
Redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。
- //读请求也使用io多线程
- io-threads-do-reads yes
- // io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)
- io-threads 4
关于线程数的设置,官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。
因此, Redis 6.0 版本之后,Redis 在启动的时候,默认情况下会创建 6 个线程:
延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:
在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。

时间序列数据的特点
查询特点
实现方案:基于zset保存时间序列数据
- # 创建延迟任务 ZADD key score member [[score member] [score member] …]
- Zadd DelayQueueKey
- # 删除延迟任务
- Zrem DelayQueueKey
- # ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列
- # 获取某个延迟任务:从zset拿出score在[0, nowTime]的元素
- zrangebyscore DelayQueueKey 0 now_time limit 0 1
消息:有序、重复幂等处理、消息可靠性
当遇到redis变慢时,按照以下checklist依次排查
获取redis实例在当前环境下的基线性能
redis实例运行机器内存不足,导致swap发生
是否用了慢查询命令:如果是的话,使用其他命令代替慢查询命令
将聚合计算命令放在客户端做
keys
大量key集中过期
Redis 的过期数据采用被动过期 + 主动过期两种策略:
被动过期:只有当访问某个 key 时,才判断这个 key 是否已过期,如果已过期,则从实例中删除
主动过期:Redis 内部维护了一个定时任务,默认每隔 100 毫秒(1秒10次)就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环(注意,这个主动过期 key 的定时任务,是在 Redis 主线程中执行的)==> 也就是说,如果在执行主动过期的过程中,出现了需要大量删除过期 key 的情况,那么此时应用程序在访问 Redis 时,必须要等待这个过期任务执行结束,Redis 才可以服务这个客户端请求 ==> 此时就会出现,应用访问 Redis 延时变大
解决方案
在设置 key 的过期时间时,增加一个随机时间(不会因为集中删除过多的 key 导致压力过大,从而避免阻塞主线程)
Redis 4.0 以上版本,开启 lazy-free 机制(释放过期 key 的内存,放到后台线程执行)
操作大key
大key删除
使用Scan命令迭代删除
对于大key的集合查询和聚合操作,可以使用Scan命令在客户端完成
fork耗时严重:
为了保证 Redis 数据的安全性,我们可能会开启后台定时 RDB 和 AOF rewrite 功能。当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。
主进程创建子进程,会调用操作系统提供的 fork 函数。(而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时),而且这个 fork 过程会消耗大量的 CPU 资源,在完成 fork 之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求。
在使用缓存时,一定要考虑好下面两个问题:① 如何使用缓存?② 缓存使用时注意事项?
延迟双删
Canal+定时器兜底
常见的缓存更新策略共有3种:
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。

写策略的步骤:
读策略的步骤:
注意,写策略的步骤的顺序不能倒过来,即不能先删除缓存再更新数据库,原因是在「读+写」并发的时候,会出现缓存和数据库的数据不一致性的问题。
Cache Aside 策略适合读多写少的场景,不适合写多的场景。
Read/Write Through(读穿 / 写穿)策略原则是应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。
1.Read Through 策略
先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。
2.Write Through 策略
当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:
下面是 Read Through/Write Through 策略的示意图:

Read Through/Write Through 策略的特点是由缓存节点而非应用程序来和数据库打交道,在我们开发过程中相比 Cache Aside 策略要少见一些,原因是我们经常使用的分布式缓存组件,无论是 Memcached 还是 Redis 都不提供写入数据库和自动加载数据库中的数据的功能。而我们在使用本地缓存的时候可以考虑使用这种策略。
Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
实际上,Write Back(写回)策略也不能应用到我们常用的数据库和缓存的场景中,因为 Redis 并没有异步更新数据库的功能。
Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作系统中文件系统的缓存都采用了 Write Back(写回)策略。
Write Back 策略特别适合写多的场景,因为发生写操作的时候, 只需要更新缓存,就立马返回了。比如,写文件的时候,实际上是写入到文件系统的缓存就返回了,并不会写磁盘。
但是带来的问题是,数据不是强一致性的,而且会有数据丢失的风险,因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的。
这里贴一张 CPU 缓存与内存使用 Write Back 策略的流程图:

解决的问题:当我们请求一个分布式锁的时候,成功了;但是,此时slave还没有复制该锁,masterDown了;之后发生了主从切换,应用程序继续请求锁,会从新的master节点获取锁,也会成功。===> 这就会导致,同一个锁被获取了不止一次。
实现机制:过半加锁成功,才认为加锁成功;否则加锁失败,会释放锁
大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。
一般而言,下面这两种情况被称为大 key:
大 key 会带来以下四种影响:
1.redis-cli --bigkeys 查找大key
可以通过 redis-cli --bigkeys 命令查找大 key:
redis-cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys
使用的时候注意事项:
该方式的不足之处:
2.使用 SCAN 命令查找大 key
使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。
对于 String 类型,可以直接使用 STRLEN 命令获取字符串的长度,也就是占用的内存空间字节数。
对于集合类型来说,有两种方法可以获得它占用的内存大小:
LLEN 命令;Hash 类型:HLEN 命令;Set 类型:SCARD 命令;Sorted Set 类型:ZCARD 命令;MEMORY USAGE 命令(需要 Redis 4.0 及以上版本),查询一个键值对占用的内存空间。3.使用 RdbTools 工具查找大 key
使用 RdbTools 第三方开源工具,可以用来解析 Redis 快照(RDB)文件,找到其中的大 key。
比如,下面这条命令,将大于 10 kb 的 key 输出到一个表格文件。
rdb dump.rdb -c memory --bytes 10240 -f redis.csv
删除操作的本质是要释放键值对占用的内存空间,不要小瞧内存的释放过程。
释放内存只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。
所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。
因此,删除大 key 这一个动作,我们要小心。具体要怎么做呢?这里给出两种方法:
①分批次删除
对于删除大 Hash,使用
hscan命令,每次获取 100 个字段,再用hdel命令,每次删除 1 个字段对于删除大 List,通过
ltrim命令,每次删除少量元素。对于删除大 Set,使用
sscan命令,每次扫描集合中 100 个元素,再用srem命令每次删除一个键。对于删除大 ZSet,使用
zremrangebyrank命令,每次删除 top 100个元素。
②异步删除(Redis 4.0版本以上)
从 Redis 4.0 版本开始,可以采用异步删除法,用 unlink 命令代替 del 来删除。
管道技术(Pipeline)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
普通命令模式,如下图所示:

管道模式,如下图所示:

使用管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。
但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。
要注意的是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。
查询单个key的过程
当通过Redis Cluster对某个key执行操作时,计算该key落在哪个哈希槽HASH_SLOT = CRC16(key) mod 16384
如果是节点自身,则直接进行处理
如果是其他节点,会向客户端返回一个
MOVED错误。客户端可以根据返回结果中的MOVED错误信息,解析出负责该key的节点ip和端口,并与之建立连接,然后重新执行即可
查询多个key的过程
对redis cluster进行批量操作主要以pipeline的方式实现
对某个key执行操作时,计算该key落在哪个哈希槽HASH_SLOT = CRC16(key) mod 16384
按照 redis node 将这批 key 进行分组
计算 slot 定位对应 redis node 的连接,每组 key 就能分别进行 pipeline 逻辑了
Q:Redis 是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?
Redis cluster集群模式,10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务。
Q:有没有开启持久化机制确保可以进行数据恢复?
有,AOF+RDB混合持久化
Q:线上 Redis 给几个 G 的内存?设置了哪些参数?
32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10g 内存,一般线上生产环境,Redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
Q:压测后你们 Redis 集群承载多少 QPS?
每个节点的读写高峰 QPS 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求每秒。
机器是什么配置?
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?
商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
首先,我们了解了目前主流的架构:多核CPU架构、NUMA架构。
在多核CPU架构下,Redis如果在不同的核上运行,就需要频繁地进行上下文切换,这个过程会增加Redis的执行时间,客户端也会观察到较高的尾延迟了。所以,建议你在Redis运行时,把实例和某个核绑定,这样,就能重复利用核上的L1、L2缓存,可以降低响应延迟。
为了提升Redis的网络性能,我们有时还会把网络中断处理程序和CPU核绑定。在这种情况下,如果服务器使用的是NUMA架构,Redis实例一旦被调度到和中断处理程序不在同一个CPU Socket,就要跨CPU Socket访问网络数据,这就会降低Redis的性能。所以,我建议你把Redis实例和网络中断处理程序绑在同一个CPU Socket下的不同核上,这样可以提升Redis的运行性能。
虽然绑核可以帮助Redis降低请求执行时间,但是,除了主线程,Redis还有用于RDB和AOF重写的子进程,以及4.0版本之后提供的用于惰性删除的后台线程。当Redis实例和一个逻辑核绑定后,这些子进程和后台线程会和主线程竞争CPU资源,也会对Redis性能造成影响。所以,我给了你两个建议:
Redis的低延迟是我们永恒的追求目标,而多核CPU和NUMA架构已经成为了目前服务器的主流配置,所以,希望你能掌握绑核优化方案,并把它应用到实践中。