面试必掌握的redis的问题

高频面经汇总:https://blog.csdn.net/qq_40262372/article/details/116075528

三、数据库:redis原文PDF获取详情见文字末尾)

3.1 redis的简介

Redis 是一个非关系型数据库,但是与传统的数据库相比,Redis 的数据是存在内存中的, 所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。Redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务、持久化、Lua 脚本、LRU 驱动时间,多种集群方案。

为什么要用redis/为什么要用缓存

为什么用一个东西,肯定是以前的东西不好,所以出了这个东西,这个东西在内存,快啊。所以,主要从“高性能”和”高并发”这两点来看待这个问题。

高性能:

假如用户第一个访问数据库中的某些数据库的时候,是从磁盘中取,取到内存,从内存读取。

如果将数据库的数据就放在内存,我们就会少一个磁盘的读取时间,那么大大增加了我们的读取效率。

如果数据库的数据改变后,那么我们缓存中的数据跟改变即可。

高并发:

直接操作缓存能承受的请求远远大于操作数据库的数量。所以我们可以将数据库的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

为什么要用redis 而不用 guava/map 做缓存?

缓存分为本地缓存和分布式缓存。Guava/map 只是本地缓存,如果 jvm 关闭后,那么数据会丢失,在多实例的情况下,每个实例都需要保存一份缓存,缓存不具有一致性。

使用 redis 这种为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 服务的高可用,整个程序构架上较为复杂。

 3.2 redis 的线程模型

Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事情处理器进行处理。文件事件处理器的结构包含 4 个部分:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件都放入队列中,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

 3.3 IO 多路复用

说到 IO 模型,我们必须要了解进程与线程的概念。一个进程至少能创建一个线程,多个线程共享一个进程的内存。程序的最终是靠着线程来完成操作的。

线程执行程序流程是这样的:

1.给 CPU 进行程序命令的执行。

2.IO 的操作(读取或输出数据)或者请求网络数据。

IO 复用是指,多个 IO 流用一个进程处理。当用户进程调用了 select 请求服务进程, 整个进程会被 Lock,内核会同时监视所有 select 里面的 socket,当其中任何一个 socket 的数据准备好后,select 就会返回一个标志。用户进程就会调用 read 操作,然后数据从内核拷贝到用户进程开始处理。

这 IO 多路复用之外还有,阻塞 IO、非阻塞 IO、异步 IO,这个会之后开一个专题讲解。

3.4 redis 常见数据结构以及使用场景分析

String

当一个线程进入后,setnx,因为不存在,所以返回 1,可以获得锁。另外线程又来的时候, setnx key 的时候发现上一个线程还在跑,那么就不能获得锁,就不会获取资源。

String 数据结构式简单的 Key-value 类型,value 其实不仅仅可以是 String,也可以是数字。常规 value 缓存应用;常规计数:微博数,粉丝数等。

Hash

常见命令:hget,hset,hgetall 等。

 

 Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:

List

  常用命令:lpush,rpush,lpop,rpop,lrang 等

 

 Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带了部分额外的内存开销。

 另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。

 Set

常用集合:sadd,spop,smembers,sunion 等

Set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:

Sorted Set

常用命令:zadd,zrang,zrem,zcard 等

和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。

举例:在直播系统中,实时排行信息包含在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行储存。

 3.5 redis 设置过期时间

Redis 中有个设置时间过期的功能,即对储存在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。 

我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。

如果假设你设置了一批 Key 只能存活 1 个小时,那么接下来 1 小时后,redis 是怎么对这批key 进行删除的?

定期删除+惰性删除

定期删除:

Redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想加入 redis 存了几十万个Key,每隔 100ms 就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载

惰性删除:

定期删除可能会导致很多过期 key 到了时间并没有删除掉。所以就有了惰性删除。假如你的过期 Key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key, 才会被 redis 给删除掉。这就是所谓的惰性删除,也是很懒!!

但是仅仅通过过期时间还是有问题的。我们想一想:如果内存中有很多过期了的 key,然后你也没有及时去查询,那么过期的 key 一直存在,一直堆在内存中,最后导致 redis 内存满了, 无法进行其他新数据的存储。怎么解决这个问题呢?redis 内存淘汰机制。

3.6 redis 内存淘汰机制(MySQL 里有 2000W 数据,Redis 中只20W 的数据,如何保证 Redis 中的数据都是热点数据?)

Redis 提供 6 种数据淘汰策略:

1.volatile(不稳定的)-lru:

从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

2.volatile-ttl:

从已设置过期时间的数据集(server.db[i].expires)中挑选要过期的数据淘汰

3.volatile-random:

从已设置过期时间的数据集(server.db[i].expires)中挑选任意数据淘汰

4.allkeys-lru:

当内存不足以容纳新写入数据时,在键空间汇总,移除最近最少使用的 key(这种方法最常用)

5.allkeys-random:

从数据集(server.db[i].dict)中任意选择数据淘汰

6.no-eviction:

禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。

4.0 版本后增加以下两种:

7.volatile-lfu:

从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰

8.allkeys-lfu:

移除最不经常使用的key

3.7 redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)

因为 redis 是运行在内存的,所以断电即失,所以 redis 为了支持防止自己内存的数据丢失,支持两种不同的持久化操作:

1.快照(snapshotting,RDB[redis batabase]);

2.只追加文件(append-only file,AOF)。

3.7.1快照(snapshotting)持久化(RDB)

从上图中看出,RBD 是通过,父进程创建了一个子进程,该子进程的作用是将 redis 内存中的数据快照一份下来,写入一个临时的 RDB 文件,然后等持久化完毕后,然后覆盖掉上一次的 RDB 文件。这个过程主进程不进行任何 IO 操作,所以保证了 Redis 的极高性能。

触发机制:

1.满足了 save 的规则, 2.执行了 flushall 命令, 3.退出 redis

优点

  1. 适合大规模的数据恢复
  2. 对数据的完整性要求不高

缺点:

  1. 需要一定的时间间隔进行操作!如果中途宕机,那么最后一次修改的数据的就没了fork 进度的时候,会占用一定的内存空间。

 3.7.2只追加文件 AOF(append-only file)持久化

与快照持久化相比,AOF 持久化的实时性更好,但是 redis 的默认不是 AOF,所以要去配置文件中开启。

只追加文件,那么追加的是什么呢?什么会更改数据呢?当然是写操作了。所以 AOF 干的事就是把写操作都给他记录下来,重启 redis 之后,再执行这个记录了写操作的文件appendonly.aof

就算这个 aof 有错误,redis 内部有一个 redis-check-aof 可以去修复 aof 文件

持久化方式:

1.每次修改都记录

2.一秒记录一次

3.操作系统自己同步数据

重写机制:

AOF 随着命令的越来越多,他的容量也会越来越大。但是数据库的数据是可以由不同的语句得到。比如 list,我们先分别 rpush A~F,然后在 lpop A~B。那么 AOF 文件直接存 rpush C~F 即可,这就节约空间

了。这就是其中有些命令是可以多行合并成一行的!!

优点:

1.不容易丢失数据,因为复制频率高。

缺点:

1.对于数据文件来说,aof 远远大于 rdb,修复的速度也比 rdb 慢!

2.aof 运行效率也比 rdb 慢,所以 redis 的默认配置是 rdb.

3.8 redis 事务

Redis 事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制, 并且在事务执行期间,服务器不会中断事务而去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才会去处理其他客户端的命令请求。

Multi 开始一个事务,EXEC 触发事务

支持特性:一致性隔离性

原子性:因为打包的命令中有一条执行错误,其他的一样会执行。

持久性:不会同步到硬盘。

3.9 缓存雪崩和缓存穿透问题解决方案

3.9.1缓存雪崩(全去查 mysql)

就是缓存大面积失效(大批到了到期时间),后面的请求都去访问数据库了,造成数据库短时间内承受大量请求而挂掉。

比如在写本文的时候,马上就要到双十二零点了,会快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时,那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所 i 有的请求都会达到存储层,储存层的调用量会暴增,造成储存层也会挂掉的情况.。

解决方案:

  1. redis 高可用:既然一台 redis 挂掉了,那么就多增几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
  2. 限流降级:在缓存失效后,通过加锁或者队列来控制数据库写缓存的线程数量。比如某个   key 值允许一个线程查询数据和写缓存,其他想要操作这个 key 那么必须等待。
  3. 数据预热:先把可能的数据先访问一次,访问的数据加入缓存中,再设置不同的过期时间,    让缓存失效时间点尽量均匀。

3.9.2 缓存穿透(查不到导致)

用户想要查询一个数据,发现 redis 内存数据库没有,也就是缓存没有命中,于是向数据库查询也没有,于是查询失败。当用户很多的时候,缓存都没有明红,于是都去请求持久层的数据库。这会给持久层的数据库造成很大的压力,相等于出现了缓存穿透。

150 左右,这个可以通过 Show variables like ‘%max_connection%’;命令来查询。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等条件都是其运行指标,这些指标超过了,mysql 就停止了。所以一般 3000 个并发请求就会打死大部分的数据库了。

解决方案:

1.布隆过滤器

    布隆过滤器是一种数据结构,对所有可能查询的 key 以 hash 形式存储,在控制层先进行校验,不负责则丢弃,从而避免了对底层存储系统的查询压力。

布隆过滤器就是一个很长的二进制数组,如果存在就是 1,不存在就是 0.

要经过 3 个哈希函数,去存到下标位置。

增:通过多个哈希函数,分别到计算出的位置处置 1

查:查询的时候都要查询,都是 1 才算存在。还是通过多个哈希函数

删:很难进行删除操作,如果把 1 换成 0,可能一个位置存在多个数。可能存在哈希冲突

优点

  1. 是二进制数,空间需求小
  2. 插入和查询的速度快,因为是计算哈希值,再由哈希值映射到坐标。时间复杂度:O(K) k 个哈希函数。
  3. 保密性好,存储的都是 0,1.

缺点

  1. 很难做删除
  2. 容易误判,不同的数据可能计算出来相同的哈希值。比如上面查询 hello,但是你好的哈希值跟 hello 一样,所以为判断存在。 只能减少误判的概率,可以在代码里设置误判率。误判率越小,计算时间就越大。

误判率的底层原理:

不同的误判率,布隆过滤器的空间与哈希函数就不同。

减少误判率,增加哈希函数,算出的哈希值也就越多,降低相同哈希值的概率,那么空间就会越大。

.2.缓存空对象

   当存储层不命中后,及时返回的空对象也将其缓存起来,同时会设置一个过期时间,后下一次访问的时候直接返回一个空对象,保护后端数据库。

但是这种方法会存在两个问题:

  1. 这样一来缓存里会存在较多的空对象,比较浪费内存空间。
  2. 即使对控制设置了过期时间,还是会存在缓存层和数据层会有一段时间窗口的不一致,这对于需要保持一致性的也会有影响。

3.9.3 缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

解决方案:

  1. 设置热点数据永不过期。
  2. 加互斥锁

与雪崩区别:

击穿是查单个 key,雪崩一群 key 到期了。

3.10 如何解决 Redis 的并发竞争 Key 问题

所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同!

推荐方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)

3.11Redis为何这么快

1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMapHashMap的优势就是查找和操作的时间复杂度都是O(1)

2.数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的

3.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多线程的切换导致消耗CPU,不用去考虑各种锁的问题。

4.使用多路I/O复用模型,非阻塞IO;

想要在学习的道路上和更多的小伙伴们交流讨论

请加Q群:725936761   

期待每一位努力学习的小伙伴加入

我们一起朝着目标加油!加油!                                         

想要了解更多请关注微信公众号:万小猿  

回复“redis”即可获取原文PDF文件

                                                                                                                              

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

万小猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值