1. 写在前面
前面对于 MySQL 进行了一部分的学习,有些细节的点可能还有问题,不过整体的框架应该就是那些了。
下面我们对于 redis 的基本常见概念和问题进行整理,先顶一下面试吧。具体的细节问题,源码什么的之后再看。
2. 基本问题
这里的基本问题包括了8个:
- 简述redis(为什么要使用redis)★
- 为什么说 redis 快或者性能高 ★★
- reids 中的 5 种数据类型,8大数据结构
- redis 中的过期策略和缓存淘汰机制 ★★
- redis 中的持久化机制 ★★
- redis 集群的主从复制 ★
- 缓存雪崩和缓存穿透问题 ★
- 缓存和数据库的数据一致性问题
2.1 简述一下 redis(为什么要使用redis)
先要明白的几个概念
- 首先要明白 SQL(Structured Query Language)结构化查询语言的定义:SQL 是具有 数据操作和数据定义等功能的数据库语言。
- 数据库分为两种:SQL 数据库 和 NoSQL(Not only SQL)数据库。二者之间有着许多差别:
简单来说就是 SQL 数据库是精确的,适用于有着精确标准和明确定义的项目。典型的应用场景比如在线商店和银行系统。
而 NoSQL 数据库是多变的。适合于具有不确定性需求的数据。典型的使用场景就是社交网络等。
redis 简述
Redis(Remote Dictionary Server) 是用 C语言 开发的一个开源高性能 单线程 基于内存的 键值对(key-value)数据库。
redis 与 其他 key-value 缓存产品比较
- redis 支持数据持久化:redis可以将内存中的数据持久化到磁盘中,重启的时候可以加载使用
- redis 支持多种数据类型和数据结构:redis 支持 5种数据结构,8种基本数据类型
- redis 支持数据备份:redis 支持 主从备份。
redis 的优势
- 性能高:读的速度最高到 11w次/s,写 8w/s
- 数据类型多:string, list, hash, set ,zset 等数据类型
- 原子性:操作都是原子性的(因为单线程,set(),get()等方法都是通过API来的,所以操作要么成功,要么失败)
这里顺带说一下 redis 的缺点:对持久化支持不是很好,所以一般不用作主存储数据库,配合传统的 MySQL来使用。
redis 应用场景
- 缓存
redis 访问快,支持数据类型多,所以常常用作存储热点数据,结合设置 过期时间等,完成缓存的更新。 - 任务队列
这个你在爬虫中也是这么用的,因为 有 list.pop() / list.push() 这样的方法
2.2 redis 为什么快
原因有3个:
- 基于内存:避免了 磁盘 IO 影响
- 单线程:避免了线程间切换的cost
- 多路复用I/O:虽说是单线程,但是多路复用IO
基于内存
这个点就不解释了,内存的操作当然远远快于磁盘操作
单线程
1. 为什么使用单线程
总的来说,就是 单线程够用了。
从官方文档来看,Redis 使用的瓶颈不是 CPU,而是受到内存和网络的限制。所以使用单线程,都可以获取到足够的CPU资源,够用了。
2. 单线程快在哪里
简单来说,就是避免了线程间切换、资源竞争、锁的操作等
多路复用I/O(阻塞IO)
- I/O阻塞:线程发出IO请求之后,操作系统内核会查看数据是否就绪,没有就绪,就会阻塞该线程,线程交出CPU。数据就绪后,内核会将数据cp到线程,这时候线程接触阻塞。
- redis多路复用IO:IO 多路复用,实际上是在 单个线程中利用 select、poll、epoll来 跟踪每一个 socket (IO流)的状态来管理多个 IO流。空闲时候就把当前线程阻塞掉,有IO流事件时候,就唤醒线程,按照顺序处理就绪的流。
这里的 多路指多个网络连接,复用 指复用同一个线程。采用多路复用IO可以使得单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)。
2.3 redis 中的5种数据类型,8大数据结构
redis 中的基本数据类型
首先来看5种(6种)数据类型:string / hash / list / set / zset / stream
1. string 数据类型
string 可以包含任何数据,是二进制安全的,甚至是一个图片或者序列化的对象。一个 string 的 value 中可以存 521M
典型使用场景
- 计数
使用 incrby 命令,可以将 string 存的值 增加 返回 - 限制次数
登录时候,3分钟内错误超过5次不能登录,实现计数的基础上,对该 key 设置过期时间即可。
2. hash 数据类型
hash 的 key 还是key, 但是 value 是一个键值对(key-value)。类似于 java 中的 Map
典型使用场景
存储用户信息:由于value 放的是键值对,所以可以做单点登录存放用户信息
3. list 数据类型
list 是一个简单的字符串列表,可以添加元素到 表头或者表尾,底层实际上是一个链表。
特点
- 有序
- 可重复
典型使用场景
由于这里的 list 添加和 弹出都是可以 在 头/ 尾 的,所以可以利用 list 来实现多种数据结构。比如 stack 、queue、消息队列等
4. set 数据类型
set 是一个 string 类型的无序集合
特点:无序,不可重复
典型使用场景
利用集合的交集、并集等求一些社交网络中的东西,共同好友等。
5. zset 数据类型
zset(sorted set),是一个 string 类型的有序集合,其中每个元素都会关联一个 double 类型的分数,通过这个分数来对元素进行排序。
典型使用场景
可以做一些排行榜业务。
6. stream 数据类型
redis 5.0 中,给出了一个新的数据类型 stream。
stream 内部其实也是一个 list,每一个 key 对应不同的 list。然而list 内部是有 msgid 来对应不同的消息的。
> XADD mystream * sensor-id 1234 temperature 19.8
1518951480106-0
这个就向 key 是 mystream 的 stream 中添加了一个条目,给出了其对应的 msgid,然后根据这个 msgid 就可以查询到不同的条目了。
典型使用场景
用来实现消息队列。
这里底层的数据结构,我们先不看了,太多了。
2.4 redis 中的过期策略和缓存淘汰机制
在我们设置 key 的时候会设置一个 expire 过期时间,那么当 这个 key 过期之后,redis 是采用一定的过期策略。
删除策略有3种:
- 定时删除:就是设置过期时间,就会启动一个定时器,只要过期,就执行删除操作。
定时删除对于内存是友好的,因为时刻监控,释放内存。但是对于CPU是不友好的。
- 惰性删除:放任键过期不管,但是每次从键空间获取键时,都检查取得的键是否过期,过期的话就删除该键,没过期就返回该键
惰性删除对于CPU 是友好的,但是对于内存是不友好的。
- 定期删除:每隔一段时间就对数据库进行一次检查,删除里面的过期键,至于要删除多少个过期键,检查多少个数据库,由算法决定
定期删除相当于是之前两种删除策略的折中。
Redis 使用的策略
redis 中,使用 惰性删除 + 定期删除
但是问题来了, 定期删除,是随机抽取键值的策略,所以不可能删除掉所有过期的 key,因此需要内存淘汰机制。
内存淘汰机制
当 redis 内存超过限制之后,会进行内存淘汰,有如下策略
- volatile-lru: 使用 LRU算法进行数据淘汰(即淘汰上次使用时间最早的,使用次数最少的key),只淘汰设定了有效期的 key
这里的LRU算法,只是在数据集中随机挑选一些键值对,在这些挑选的键值对中,进行LRU,并不是对所有的键值对。
- allkeys-lru:LRU,所有key
- volatile-random:随机淘汰,只淘汰有效期的key
- allkeys-random:随机淘汰,所有key
- volatile-ttl:淘汰剩余有效期最短的 key
TTL 也是随机选 TTL 表中的一部分,并不是所有键值。
- no-eviction:不删除任何数据。
LRU(Least Recently Used,最近最少使用)算法,是根据历史使用记录来淘汰数据的,一般是用在缓存中。认为“最近访问的数据,就是高频数据,以后被访问的概率高。”
所以来说,就是,用一个 list 来记录数据,当一个数据被访问时候,将该数据放在 链表头;表满了的时候,从表尾删除数据。
总的来看,就是,把常用的保留,不常用的删除,为了控制内存。
二者的区别
过期键删除策略,是对过期了的键,做删除。
内存淘汰机制,是内存不够时候,及时键没有过期,也要删除一部分。
2.5 Redis 持久化机制
持久化主要是,做 数据恢复 的。 有时候 redis 挂了,我们再重启,如果没有持久化到磁盘中的数据,就无法恢复了。所以一般都是通过持久化到磁盘中的文件进行数据恢复的。
1. AOF(Append Only File)
定义
AOF 以 日志 的形式,记录每个写操作,以 append-only(只追加文件)的方式将写命令添加到日志中。
在 redis 重启的时候,通过回放 AOF 日志中的写入指令来重新构建数据集,回复数据。
过程
redis 每隔 1s,调用操作系统的 fsync 操作,强制将 缓存中的数据刷新到 AOF文件 中。
优缺点
优点
- 更好的保护数据:间隔小,最少丢失1s的数据。
- 持久化性能高:AOF 日志文件,以 append-only 只追加方式写入,没有磁盘寻址之类的开销,写入很快。
可以从逻辑角度来理解,文件好写,但是最后恢复不好用,不快。
- 不影响客户端:
- 适合做误删除的紧急恢复:因为是记录操作的日志,所以如果发生了类似于 flushall 这样的误删除,直接将AOF 复制出来,把最后的 flushall 这一步删除,再使用其做恢复即可。
缺点
- AOF文件比较大:相比于 .rdb 文件,AOF 文件一般比较大。
- 数据恢复慢:由于需要回放、执行操作日志,所以比较慢,不适合做冷备。
- 有bug:AOF 的机制比较复杂,可能会产生一些难以预测的 bug,导致数据恢复出问题。并不像 RDB 那样简单暴力。
2. RDB(Redis DataBase)
定义
RDB 是对 redis 数据进行周期性的持久化。
会按照配置,每隔指定时间,就把数据的快照,保存到磁盘中,创建一个 dump.rdb 文件。redis重启的时候,通过这个 .rdb 文件来恢复。
过程
redis 会单独创建一个 fork() 子进程,将当前父进程的数据库数据 cp 到子进程的内存中,然后子进程写到临时文件中,持久化结束,用这个临时文件替换上一次的快照文件,子进程退出,内存释放。
总的来说,就是通过一个 子进程来cp当前数据库,做一个快照文件的。
优缺点
优点
- 适合做冷备:RDB 会生成多版本的快照文件,每个文件都代表了一个时刻中 redis 的数据,所以非常适合做 冷备份。
- redis 对外的性能影响小:RDB 是通过子进程 来做备份的,所以可以让主进程对外保持高性能。
- 速度快:RDB 是 基于RDB数据文件 .rdb 来恢复的,直接加载,非常快。而AOF恢复的时候是要回放、执行所有指令日志,相对慢一些。
缺点
- 丢失数据多:因为 RDB 方式的时间间隔比较久,所以在出问题时候,这次间隔内的数据都没有备份。
所以,从这个角度来说,RDB 不适合做 第一优先的回复方案。
- 对客户端性能差:由于是要通过子进程来进行 .rdb 文件,如果这时候数据库很大,那么子进程占用就会很多,会导致对客户端的服务暂停一下。
上面优点的2中,提到的是对外性能影响小。这两者不矛盾。
3. 如何选择持久化机制呢
其实,是应该 AOF 和 RDB 两种结合使用。
1)如果仅仅使用 RDB,那么会丢失很多数据
2)如果仅仅使用 AOF,那么
- 恢复数据速度比较慢
- AOF备份机制比较复杂,会产生一些 bug,持久化不够健壮。
3)综合使用两种,用 AOF做第一优先,保证数据不丢失,同时,用RDB做不同程度的冷备,在AOF文件出问题的时候,可以用 RDB 文件来快速恢复,作为最后的防线。
2.6 redis 集群的主从复制
1. redis 集群
集群就是添加服务器的数量,提供相同的服务,从而样服务器达到一个稳定、高效的状态。
在 redis 集群中,一个redis 称作一个节点,分为:主节点master、从节点 slave。
master 可读可写,slave 只能读。
redis 集群是基于 redis 主从复制实现的
- 读写分离,性能提高
- 容灾快速恢复
2. redis 主从复制
主从复制是为了 分担读的压力
在主从复制模型中,只有一个 master,多个 slave。
只要连接正常,master 会一直将自己的数据同步给 slave,保持 主从同步。
这样一来,整个集群的读写性能提高。
将读的压力给slave,也是变相的提高了master写的能力,从集群的角度来看,整体性能提高了。
主从复制原理
主从复制分为 全量复制 和 增量复制
全量复制
全量复制发生在 slave 初始化的时候。
分为3个阶段:
- 同步快照:master 创建 rdb 快照发送给 slave,slave解析加载快照。同时master将这个阶段的 写命令 存储到 缓冲区
- 同步写缓冲:master 将缓冲区中的写指令发给slave
- 同步增量:slave 执行来自 master 缓冲的写指令,一次同步完成
核心就是,快照+写指令。因为快照解析的时候,master 可能有其他操作,也是需要同步过来的。
增量复制
增量复制一般发生在 slave 正常工作后,也可以是其重连后。
- master 每执行一个写操作就会向slave发送相同写命令,完成增量复制
3. Sentinel 哨兵模式
主从模式缺点
当 master 挂了,那么这个集群就没有可以写的节点了。
这时候就需要把一个slave 变成 master,这就是 sentinel哨兵 的作用
哨兵的任务
redis 的 sentinel 系统用于管理 redis集群(多个redis服务器),哨兵系统执行3个任务:
- 监控(monitoring):哨兵会不断的检查 master 和 slaves 运行是否正常
监控可以监控多个 集群(多个 主从模式)
- 提醒(Notification):当某个 redis 服务器节点出问题,哨兵通过 API 向应用程序发出通知
- 自动故障迁移(Automatic failover):mater 出问题,哨兵会进行自动故障迁移,通过选举来把一个 slave 升级成为 master。
自动故障迁移
这里我们要详细看一下自动故障迁移,因为这个比较复杂。
哨兵网络
实际上,监控是用多个 哨兵 监控的,组成一个哨兵网络,进行通信。
如果只有一个,那哨兵挂了,就不能起到监控的作用了。
故障切换过程
- 投票(半数原则)
任何一个哨兵发现 master 挂了,会通知其他 哨兵开会,投票确定 这个master 是不是下线了 - 选举
哨兵确定 master 下线了,从所有的 slaves 中,选举一个新的 master,其他的 slave 转为这个新 master 的 slave
2.7 缓存雪崩、缓存穿透、缓存击穿 问题
设计一个缓存系统,就要考虑 缓存雪崩、穿透、击穿问题
客户端请求数据,先从缓存去拿,拿不到再去数据库中去拿(拿到了就更新到缓存中),再拿不到就返回空结果。
1. 缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而客户端不断发起请求。
这时候就有一个问题,重复请求不存在的数据时候,每次都要去 存储中拿(因为存储中没有,所以不会写到缓存中),这样子就失去了缓存的意义。
如果有人恶意利用这样的方式来访问我们的系统,那么在并发量很大的时候, DB 就可能挂掉,因为没有了缓存的保护。
缓存穿透 是 高并发访问 根本不存在的数据
解决方案
- 在接口层加校验:比如加一些基础校验,像是 id<=0 直接拦截
- 避免恶意使用同一个key重复攻击:缓存和存储中都访问不到的数据,我们可以设置成为 key-null,并将其过期时间设置短一点
2. 缓存击穿
缓存击穿 是指 缓存中没有,但是存储中有的数据。
如果并发很大的话,同时缓存还没有读到数据,同时又去数据库中读,会让数据库挂掉。
缓存击穿是 高并发访问 缓存中不存在的数据
解决方案
- 设置热点数据永不过期(永久缓存)
- 接口限流:重要的接口要做好限流,避免恶意的刷接口
- 布隆过滤器(Bloom Filter):布隆过滤器可以快速判断一个元素是在集合中,当有人恶意高并发访问缓存中没有的数据,布隆过滤器可以快速给出结果,避免数据库挂掉
3. 缓存雪崩
缓存雪崩指缓存中数据大批量到期,如果此时还是有高并发查询,就会给数据库带来很大压力。
缓存雪崩是 缓存中数据大批量同时到期(相当于缓存一大块没用了)
注意,缓存击穿是对一条数据并发查询而言的,缓存雪崩是对多条数据并发查询造成的
解决方案
- 缓存数据的过期时间随机生成,避免同一时间大量数据过期
- 热点数据永不过期
- 可以用分布式将热点数据放在不同缓存中
2.8 redis 缓存和数据库一致的问题
参考这个博文
什么是二者一致性问题
之前说过,在 redis+内存 的架构中,读是先读缓存,再读内存的。读的过程是不会有二者一致性问题的。
但,在 数据库和缓存更新的时候,就会出现 二者间数据一致性的问题。
这里的更新就是 写库+删缓存
- 先写库,再删缓存,如果写库的时候,挂了,那么缓存没删,二者不一致
- 写删缓存,再写库,如果还没写完,就有人来访问,缓存没有命中,访问库并写入缓存,二者不一致。
可能的解决方法
1. 延时双删策略 + 缓存超时
- 删除缓存
- 写库
- 延时
- 删除缓存
这里多了一个延时再删除,就是为了保证写库完成,再重新删除一次,保证用最新的数据库来更新redis
2. 异步更新缓存(基于binlog的同步)
- 读redis:热点数据都在 redis 中
- 写 MySQL:增删什么的都在 MySQL
- 更新 redis:通过 MySQL 的 binlog 来更新 redis
核心就是将 MySQL 的 binlog 推送给 redis,redis 根据这个进行更新