Redis必须知道的


1. 写在前面

前面对于 MySQL 进行了一部分的学习,有些细节的点可能还有问题,不过整体的框架应该就是那些了。
下面我们对于 redis 的基本常见概念和问题进行整理,先顶一下面试吧。具体的细节问题,源码什么的之后再看。


2. 基本问题

这里的基本问题包括了8个:

  1. 简述redis(为什么要使用redis)★
  2. 为什么说 redis 快或者性能高 ★★
  3. reids 中的 5 种数据类型,8大数据结构
  4. redis 中的过期策略和缓存淘汰机制 ★★
  5. redis 中的持久化机制 ★★
  6. redis 集群的主从复制 ★
  7. 缓存雪崩和缓存穿透问题 ★
  8. 缓存和数据库的数据一致性问题

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 缓存产品比较

  1. redis 支持数据持久化:redis可以将内存中的数据持久化到磁盘中,重启的时候可以加载使用
  2. redis 支持多种数据类型和数据结构:redis 支持 5种数据结构,8种基本数据类型
  3. redis 支持数据备份:redis 支持 主从备份。

redis 的优势

  1. 性能高:读的速度最高到 11w次/s,写 8w/s
  2. 数据类型多:string, list, hash, set ,zset 等数据类型
  3. 原子性:操作都是原子性的(因为单线程,set(),get()等方法都是通过API来的,所以操作要么成功,要么失败)

这里顺带说一下 redis 的缺点:对持久化支持不是很好,所以一般不用作主存储数据库,配合传统的 MySQL来使用。

redis 应用场景

  1. 缓存
    redis 访问快,支持数据类型多,所以常常用作存储热点数据,结合设置 过期时间等,完成缓存的更新。
  2. 任务队列
    这个你在爬虫中也是这么用的,因为 有 list.pop() / list.push() 这样的方法

2.2 redis 为什么快

原因有3个:

  1. 基于内存:避免了 磁盘 IO 影响
  2. 单线程:避免了线程间切换的cost
  3. 多路复用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
典型使用场景

  1. 计数
    使用 incrby 命令,可以将 string 存的值 增加 返回
  2. 限制次数
    登录时候,3分钟内错误超过5次不能登录,实现计数的基础上,对该 key 设置过期时间即可。
2. hash 数据类型

hash 的 key 还是key, 但是 value 是一个键值对(key-value)。类似于 java 中的 Map>。


hash的例子

典型使用场景
存储用户信息:由于value 放的是键值对,所以可以做单点登录存放用户信息

3. list 数据类型

list 是一个简单的字符串列表,可以添加元素到 表头或者表尾,底层实际上是一个链表。
特点

  1. 有序
  2. 可重复
    典型使用场景
    由于这里的 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文件 中。

优缺点

优点

  1. 更好的保护数据:间隔小,最少丢失1s的数据。
  2. 持久化性能高:AOF 日志文件,以 append-only 只追加方式写入,没有磁盘寻址之类的开销,写入很快。

可以从逻辑角度来理解,文件好写,但是最后恢复不好用,不快。

  1. 不影响客户端
  2. 适合做误删除的紧急恢复:因为是记录操作的日志,所以如果发生了类似于 flushall 这样的误删除,直接将AOF 复制出来,把最后的 flushall 这一步删除,再使用其做恢复即可。

缺点

  1. AOF文件比较大:相比于 .rdb 文件,AOF 文件一般比较大。
  2. 数据恢复慢:由于需要回放、执行操作日志,所以比较慢,不适合做冷备。
  3. 有bug:AOF 的机制比较复杂,可能会产生一些难以预测的 bug,导致数据恢复出问题。并不像 RDB 那样简单暴力。

2. RDB(Redis DataBase)

定义

RDB 是对 redis 数据进行周期性的持久化。
会按照配置,每隔指定时间,就把数据的快照,保存到磁盘中,创建一个 dump.rdb 文件。redis重启的时候,通过这个 .rdb 文件来恢复。

过程

redis 会单独创建一个 fork() 子进程,将当前父进程的数据库数据 cp 到子进程的内存中,然后子进程写到临时文件中,持久化结束,用这个临时文件替换上一次的快照文件,子进程退出,内存释放。

总的来说,就是通过一个 子进程来cp当前数据库,做一个快照文件的。

优缺点

优点

  1. 适合做冷备:RDB 会生成多版本的快照文件,每个文件都代表了一个时刻中 redis 的数据,所以非常适合做 冷备份。
  2. redis 对外的性能影响小:RDB 是通过子进程 来做备份的,所以可以让主进程对外保持高性能。
  3. 速度快:RDB 是 基于RDB数据文件 .rdb 来恢复的,直接加载,非常快。而AOF恢复的时候是要回放、执行所有指令日志,相对慢一些。

缺点

  1. 丢失数据多:因为 RDB 方式的时间间隔比较久,所以在出问题时候,这次间隔内的数据都没有备份。

所以,从这个角度来说,RDB 不适合做 第一优先的回复方案。

  1. 对客户端性能差:由于是要通过子进程来进行 .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 主从复制实现的

  1. 读写分离,性能提高
  2. 容灾快速恢复

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。
自动故障迁移

这里我们要详细看一下自动故障迁移,因为这个比较复杂。
哨兵网络
实际上,监控是用多个 哨兵 监控的,组成一个哨兵网络,进行通信。

如果只有一个,那哨兵挂了,就不能起到监控的作用了。

故障切换过程

  1. 投票(半数原则)
    任何一个哨兵发现 master 挂了,会通知其他 哨兵开会,投票确定 这个master 是不是下线了
  2. 选举
    哨兵确定 master 下线了,从所有的 slaves 中,选举一个新的 master,其他的 slave 转为这个新 master 的 slave

2.7 缓存雪崩、缓存穿透、缓存击穿 问题

设计一个缓存系统,就要考虑 缓存雪崩、穿透、击穿问题
客户端请求数据,先从缓存去拿,拿不到再去数据库中去拿(拿到了就更新到缓存中),再拿不到就返回空结果。

1. 缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而客户端不断发起请求。
这时候就有一个问题,重复请求不存在的数据时候,每次都要去 存储中拿(因为存储中没有,所以不会写到缓存中),这样子就失去了缓存的意义。
如果有人恶意利用这样的方式来访问我们的系统,那么在并发量很大的时候, DB 就可能挂掉,因为没有了缓存的保护。

缓存穿透 是 高并发访问 根本不存在的数据

解决方案

  1. 在接口层加校验:比如加一些基础校验,像是 id<=0 直接拦截
  2. 避免恶意使用同一个key重复攻击:缓存和存储中都访问不到的数据,我们可以设置成为 key-null,并将其过期时间设置短一点

2. 缓存击穿

缓存击穿 是指 缓存中没有,但是存储中有的数据。
如果并发很大的话,同时缓存还没有读到数据,同时又去数据库中读,会让数据库挂掉。

缓存击穿是 高并发访问 缓存中不存在的数据

解决方案

  1. 设置热点数据永不过期(永久缓存)
  2. 接口限流:重要的接口要做好限流,避免恶意的刷接口
  3. 布隆过滤器(Bloom Filter):布隆过滤器可以快速判断一个元素是在集合中,当有人恶意高并发访问缓存中没有的数据,布隆过滤器可以快速给出结果,避免数据库挂掉

3. 缓存雪崩

缓存雪崩指缓存中数据大批量到期,如果此时还是有高并发查询,就会给数据库带来很大压力

缓存雪崩是 缓存中数据大批量同时到期(相当于缓存一大块没用了)
注意,缓存击穿是对一条数据并发查询而言的,缓存雪崩是对多条数据并发查询造成的

解决方案

  1. 缓存数据的过期时间随机生成,避免同一时间大量数据过期
  2. 热点数据永不过期
  3. 可以用分布式将热点数据放在不同缓存中

2.8 redis 缓存和数据库一致的问题

参考这个博文

什么是二者一致性问题

之前说过,在 redis+内存 的架构中,读是先读缓存,再读内存的。读的过程是不会有二者一致性问题的。
但,在 数据库和缓存更新的时候,就会出现 二者间数据一致性的问题

这里的更新就是 写库+删缓存

  1. 先写库,再删缓存,如果写库的时候,挂了,那么缓存没删,二者不一致
  2. 写删缓存,再写库,如果还没写完,就有人来访问,缓存没有命中,访问库并写入缓存,二者不一致。

可能的解决方法

1. 延时双删策略 + 缓存超时

  • 删除缓存
  • 写库
  • 延时
  • 删除缓存

这里多了一个延时再删除,就是为了保证写库完成,再重新删除一次,保证用最新的数据库来更新redis

2. 异步更新缓存(基于binlog的同步)

  • 读redis:热点数据都在 redis 中
  • 写 MySQL:增删什么的都在 MySQL
  • 更新 redis:通过 MySQL 的 binlog 来更新 redis

核心就是将 MySQL 的 binlog 推送给 redis,redis 根据这个进行更新

你可能感兴趣的:(Redis必须知道的)