高频面经汇总: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
优点:
- 适合大规模的数据恢复
- 对数据的完整性要求不高
缺点:
- 需要一定的时间间隔进行操作!如果中途宕机,那么最后一次修改的数据的就没了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 有的请求都会达到存储层,储存层的调用量会暴增,造成储存层也会挂掉的情况.。
解决方案:
- redis 高可用:既然一台 redis 挂掉了,那么就多增几台 redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
- 限流降级:在缓存失效后,通过加锁或者队列来控制数据库写缓存的线程数量。比如某个 key 值允许一个线程查询数据和写缓存,其他想要操作这个 key 那么必须等待。
- 数据预热:先把可能的数据先访问一次,访问的数据加入缓存中,再设置不同的过期时间, 让缓存失效时间点尽量均匀。
3.9.2 缓存穿透(查不到导致)
用户想要查询一个数据,发现 redis 内存数据库没有,也就是缓存没有命中,于是向数据库查询也没有,于是查询失败。当用户很多的时候,缓存都没有明红,于是都去请求持久层的数据库。这会给持久层的数据库造成很大的压力,相等于出现了缓存穿透。
150 左右,这个可以通过 Show variables like ‘%max_connection%’;命令来查询。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等条件都是其运行指标,这些指标超过了,mysql 就停止了。所以一般 3000 个并发请求就会打死大部分的数据库了。
解决方案:
1.布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的 key 以 hash 形式存储,在控制层先进行校验,不负责则丢弃,从而避免了对底层存储系统的查询压力。
布隆过滤器就是一个很长的二进制数组,如果存在就是 1,不存在就是 0.
要经过 3 个哈希函数,去存到下标位置。
增:通过多个哈希函数,分别到计算出的位置处置 1
查:查询的时候都要查询,都是 1 才算存在。还是通过多个哈希函数
删:很难进行删除操作,如果把 1 换成 0,可能一个位置存在多个数。可能存在哈希冲突
优点:
- 是二进制数,空间需求小
- 插入和查询的速度快,因为是计算哈希值,再由哈希值映射到坐标。时间复杂度:O(K) k 个哈希函数。
- 保密性好,存储的都是 0,1.
缺点:
- 很难做删除
- 容易误判,不同的数据可能计算出来相同的哈希值。比如上面查询 hello,但是你好的哈希值跟 hello 一样,所以为判断存在。 只能减少误判的概率,可以在代码里设置误判率。误判率越小,计算时间就越大。
误判率的底层原理:
不同的误判率,布隆过滤器的空间与哈希函数就不同。
减少误判率,增加哈希函数,算出的哈希值也就越多,降低相同哈希值的概率,那么空间就会越大。
.2.缓存空对象
当存储层不命中后,及时返回的空对象也将其缓存起来,同时会设置一个过期时间,后下一次访问的时候直接返回一个空对象,保护后端数据库。
但是这种方法会存在两个问题:
- 这样一来缓存里会存在较多的空对象,比较浪费内存空间。
- 即使对控制设置了过期时间,还是会存在缓存层和数据层会有一段时间窗口的不一致,这对于需要保持一致性的也会有影响。
3.9.3 缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
- 设置热点数据永不过期。
- 加互斥锁
与雪崩区别:
击穿是查单个 key,雪崩一群 key 到期了。
3.10 如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同!
推荐方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
3.11Redis为何这么快
1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2.数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的
3.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多线程的切换导致消耗CPU,不用去考虑各种锁的问题。
4.使用多路I/O复用模型,非阻塞IO;
想要在学习的道路上和更多的小伙伴们交流讨论
请加Q群:725936761
期待每一位努力学习的小伙伴加入
我们一起朝着目标加油!加油!
想要了解更多请关注微信公众号:万小猿
回复“redis”即可获取原文PDF文件