第五章-Redis
1、介绍下 Redis Redis 有哪些数据类型 难度系数:⭐
**Redis 全称(Remote Dictionary Server)**本质上是一个 Key-Value 类型的内存数据库,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据 flush 到硬盘上进行保存。因为是纯内存操作,Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB。 Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构,此外单个 value 的最大限制是 1GB,不像 memcached 只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能,比方说用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性 能消息队列服务,用他的 Set 可以做高性能的 tag 系统等等。另外 Redis 也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一 个功能加强版的 memcached 来用。 Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
常用基本数据类型如下:
| string | 字符串(一个字符串类型最大存储容量为 512M) |
|---|---|
| list | 可以重复的集合 |
| set | 不可以重复的集合 |
| hash | 类似于 Map< String,String> |
| zset(sorted set) | 带分数的 set |
2、Redis 提供了哪几种持久化方式 难度系数:⭐
RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储。
AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 redis 协议追加保存每次写的操作到文件末尾.Redis 还能对 AOF 文件进行后台重写,使得 AOF 文件的体积不至于过大。
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
你也可以同时开启两种持久化方式,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
(1)RDB 持久化:
每隔一段时间,将内存中的数据集写到磁盘
Redis 会单独创建(fork)一个子进程来进行持久化,会先将数据写入到个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。
保存策略:
save 9 00 1 900 秒内如果至少有 1 个 key 的值变化,则保存
save 300 10 300 秒内如果至少有 10 个 key 的值变化,则保存
save 60 1 0000 60 秒内如果至 10000 个 key 的值变化,则保存
(2)AOF 持久化: 以日志形式记录每个更新((总结、改)操作
Redis 重新启动时读取这个文件,重新执行新建、修改数据的命令恢复数据。
保存策略:
appendfsync always:每次产生一条新的修改数据的命令都执行保存操作;效率低,但是安全!
appendfsync everysec:每秒执行一次保存操作。如果在未保存当前秒内操作时发生了断电,仍然会导致一部分数据丢失(即 1 秒钟的数据)。
appendfsync no:从不保存,将数据交给操作系统来处理。更快,也更不安全的选择。
推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。
缺点:
1 比起 RDB 占用更多的磁盘空间
2 恢复备份速度要慢
3 每次读写都同步的话,有一定的性能压力
4 存在个别 Bug,造成恢复不能
(3)选择策略:
可读的日志文本,通过操作 AOF
官方推荐:
如果对数据不敏感,可以选单独用 RDB;不建议单独用 AOF,因为可能出现 Bug;如果只是做纯内存缓存,可以都不用
3、Redis 为什么快 难度系数:⭐
1)完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1)
2)数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的
3)采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
4)使用 I/O 多路复用模型,非阻塞 IO
IO:输入/输出
多路:多个输入/输出通道(socket)
复用:通过一种机制同时管理多个 I/O 操作
I/O 多路复用是一种操作 IO 的技术,我们可以理解采用单线程同时管理多个 Socket
多种 I/O 多路复用技术:select、poll、epoll
Redis 在 linux 使用 epoll 机制
I/O 多路复用体现在计算机通信的操作系统内核层,具体来说,是在处理网络通信的过程中。操作系统内核层使用 I/O 多路复用技术来同时监视和管理多个套接字(Socket)的状态,以实现高效的并发通信。以下是一些关键环节: 接收数据和分发:当有多个客户端连接到服务器时,每个连接都需要进行数据的接收。操作系统内核使用 I/O 多路复用来同时监听多个套接字的读取事件,一旦某个套接字有数据可读,内核会通知应用程序,并将数据从内核缓冲区复制到应用程序的内存中。 发送数据和缓冲:类似地,内核使用 I/O 多路复用来监视套接字的写入事件,以确保可以高效地将数据从应用程序发送到套接字。当套接字可写时,内核会将应用程序提供的数据从内存缓冲区复制到内核缓冲区,然后通过网络传输。 连接管理:在服务器端,当有新的客户端连接请求时,操作系统内核可以使用 I/O 多路复用来监听连接事件,以便及时接受新的连接并进行处理。 多客户端并发处理:I/O 多路复用还可以帮助服务器同时处理多个客户端连接的读写操作,而不需要为每个连接创建独立的线程或进程。这有助于提高服务器的性能和并发处理能力。
5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求
4、Redis 为什么是单线程的 难度系数:⭐
官方 FAQ 表示,因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了 Redis 利用队列技术将并发访问变为串行访问
1)绝大部分请求是纯粹的内存操作
2)采用单线程,避免了不必要的上下文切换和竞争条件
5、Redis 服务器的内存是多大 难度系数:⭐
配置文件中设置 redis 内存的参数:。
该参数如果不设置或者设置为 0,则 redis 默认的内存大小为:
32 位下默认是 3G
64 位下不受限制
一般推荐 Redis 设置内存为最大物理内存的四分之三,也就是0.75
命令行设置 config set maxmemory < 内存大小,单位字节>,服务器重启失效
config get maxmemory 获取当前内存大小
永久则需要设置 maxmemory 参数,maxmemory 是 bytes 字节类型,注意转换
6、为什么 Redis 的操作是原子性的,怎么保证原子性的 难度系数:⭐
对于 Redis 而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。
Redis 的操作之所以是原子性的,是因为 Redis 是单线程的。
Redis 本身提供的所有 API 都是原子操作,Redis 中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 将 get 和 set 改成单命令操作,incr 。使用 Redis 的事务,或者使用 Redis+Lua==的方式实现.
7、Redis 有事务吗 难度系数:⭐
Redis 是有事务的,redis 中的事务是一组命令的集合,这组命令要么都执行,要不都不执行,
redis 事务的实现,需要用到 MULTI(事务的开始)和 EXEC(事务的结束)命令 ;

当输入 MULTI 命令后,服务器返回 OK 表示事务开始成功,然后依次输入需要在本次事务中执行的所有命令,每次输入一个命令服务器并不会马上执行,而是返回”QUEUED”,这表示命令已经被服务器接受并且暂时保存起来,最后输入 EXEC 命令后,本次事务中的所有命令才会被依次执行,可以看到最后服务器一次性返回了两个 OK,这里返回的结果与发送的命令是按顺序一一对应的,这说明这次事务中的命令全都执行成功了。
Redis 的事务除了保证所有命令要不全部执行,要不全部不执行外,还能保证一个事务中的命令依次执行而不被其他命令插入。同时,redis 的事务是不支持回滚操作的。
8、Redis 数据和 MySQL 数据库的一致性如何实现 难度系数:⭐⭐

一、 延时双删策略
在写库前后都进行 redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠 500 毫秒(根据具体的业务时间来定)
4)再次删除缓存。
那么,这个 500 毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百 ms 即可。比如:休眠 1 秒。
二、设置缓存的过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
三、如何写完数据库后,再次删除缓存成功?
上述的方案有一个缺点,那就是操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。
1、方案一具体流程
(1)更新数据库数据;
(2)缓存因为种种问题删除失败;
(3)将需要删除的 key 发送至消息队列;
(4)自己消费消息,获得需要删除的 key;
(5)继续重试删除操作,直到成功。
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的 binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
2、方案二具体流程
(1)更新数据库数据;
(2)数据库会将操作信息写入 binlog 日志当中;
(3)订阅程序提取出所需要的数据以及 key;
(4)另起一段非业务代码,获得该信息;
(5)尝试删除缓存操作,发现删除失败;
(6)将这些信息发送至消息队列;
(7)重新从消息队列中获得该数据,重试操作。
9、缓存击穿,缓存穿透,缓存雪崩的原因和解决方案(或者说使用缓存的过程中有没有遇到什么问题,怎么解决的) 难度系数:⭐
- 缓存穿透:数据库和缓存都不存在,解决:位图 布隆过滤器
是指查询一个不存在的数据,由于缓存无法命中,将去查询数据库,但是数据库也无此记录,并且出于容错考虑,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
解决方案:空结果也进行缓存,可以设置一个空对象,但它的过期时间会很短,最长不超过五分钟。 或者用布隆过滤器也可以解决,Redisson 框架中有布隆过滤器。
- 缓存雪崩:大量 key 同时失效,解决:随机时间
是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
解决方案:原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
- 缓存击穿: 热点 key 失效,解决:分布式锁 redisson
是指对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来之前正好失效,那么所有对这个 key 的数据查询都落到 DB,我们称为缓存击穿。
解决方案:在分布式的环境下,应使用分布式锁来解决,分布式锁的实现方案有多种,比如使用 Redis 的 setnx、使用 Zookeeper 的临时顺序节点等来实现
10、哨兵模式是什么样的 难度系数:⭐⭐
如果 Master 异常,则会进行 Master-Slave 切换,将其中一 Slave 作为 Master,将之前的 Master 作为 Slave
下线:
① 主观下线:Subjectively Down,简称 SDOWN,指的是当前 Sentinel 实例对某个 redis 服务器做出的下线判断。
② 客观下线:Objectively Down, 简称 ODOWN,指的是多个 Sentinel 实例在对 Master Server 做出 SDOWN 判断,并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后,得出的 Master Server 下线判断,然后开启 failover.
工作原理:
(1)每个 Sentinel 以每秒钟一次的频率向它所知的 Master,Slave 以及其他 Sentinel 实例发送一个 PING 命令 ;
(2)如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线;
(3)如果一个 Master 被标记为主观下线,则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 的确进入了主观下线状态;
(4)当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认 Master 的确进入了主观下线状态, 则 Master 会被标记为客观下线 ;
(5)在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有 Master,Slave 发送 INFO 命令
(6)当 Master 被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 ;
(7)若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除;
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除;
11、Redis 常见性能问题和解决方案 难度系数:⭐
(1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
(2) 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master < - Slave1 < - Slave2 < - Slave3...
这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变。
12、MySQL 里有大量数据,如何保证 Redis 中的数据都是热点数据 难度系数:⭐⭐
Redis 内存淘汰策略
redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
数据淘汰策略:
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但 DEL 和几个例外)
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
13、Redis 集群方案应该怎么做 都有哪些方案 难度系数:⭐⭐
(1)twemproxy,大概概念是,它类似于一个代理方式,使用方法和普通 redis 无任何区别,设置好它下属的多个 redis 实例后,使用时在本需要连接 redis 的地方改为连接 twemproxy,它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转接到具体 redis,将结果再返回 twemproxy。使用方式简便(相对 redis 只需修改连接端口),对旧项目扩展的首选。 问题:twemproxy 自身单端口实例的压力,使用一致性 hash 后,对 redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
(2)codis,目前用的最多的集群方案,基本和 twemproxy 一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新 hash 节点。
(3)redis cluster3.0 自带的集群,特点在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置从节点。具体看官方文档介绍。
(4)在业务代码层实现,起几个毫无关联的 redis 实例,在代码层,对 key 进行 hash 计算,然后去对应的 redis 实例操作数据。 这种方式对 hash 层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。
14、说说 Redis 哈希槽的概念 难度系数:⭐⭐
Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。
15、Redis 有哪些适合的场景 难度系数:⭐
(1)会话缓存(Session Cache)
最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台 Magento 也提供 Redis 的插件。
(2)全页缓存(FPC)
除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。
再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。
此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
(3)队列
Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。
如果你快速的在 Google 中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看。
(4)排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10 个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。
(5)发布/订阅
最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。
16、Redis 在项目中的应用 难度系数:⭐
Redis 一般来说在项目中有几方面的应用
- 作为缓存,将热点数据进行缓存,减少和数据库的交互,提高系统的效率
- 作为分布式锁的解决方案,解决缓存击穿等问题
- 作为消息队列,使用 Redis 的发布订阅功能进行消息的发布和订阅
具体的使用场景要结合项目去说,比如说项目中有哪些场景用到 Redis 来作为缓存,以及分布式锁等等。