redis 面试题

1. 介绍一下redis的数据结构

redis有String、List、Set、Hash、zSet这五种数据结构。

1.1 String

String是redis中最基本的一种数据结构,redis中String的底层实现是SDS(simple dynamic string),SDS与普通C语言字符串的区别:

  1. SDS的数据结构中维护了字符串长度这一变量,因此拿到SDS长度的时间复杂度为O(1);而C语言为O(n),因为要遍历一遍字符串。
  2. 重新分配内存的次数:在C中,每次增加或者缩短一个字符串的时候,都需要对保存该C串的字符数组重新进行内存分配。若不分配,当增长C串时,会缓存溢出;在缩短C串时,会内存泄漏;而在SDS中,由于空间预分配惰性空间释放这两个特点,可以减少内存重新分配次数。
    2.1. 空间预分配:在增长字符串时,SDS的API会申请额外的空间。
    2.2. 惰性空间释放:在缩短字符串时,SDS不会立即释放未使用的空间,而是用free关键字来记录未使用的空间。

1.2 List

List是元素的集合,其底层由ziplist(压缩列表)或linkedList(双向链表)实现。当每个节点的大小小于64字节,就用ziplist,如果超过,直接升级为linkedList。

  1. ziplist保存的各个元素在内存中是连续的,但是与普通数组不一样的是,ziplist中的每个节点的大小不是固定的,节点内容有多大,就开多大空间,避免空间浪费。因为节点大小不固定,所以每个节点前必须引入节点长度,这样才能实现顺序访问。ziplist的crud时间复杂度:
    头插/尾插:平均O(n),最坏O(n^2),因为可能引发连锁反应
    删除:平均O(n),最坏O(n^2),因为可能引发连锁反应
    查询:O(n)
    修改:平均O(n),最坏O(n^2),因为可能引发连锁反应
  2. redis的linkedList的实现是双向链表

1.3 hash(字典)

hash保存的是键值对,无序。其底层由ziplist或hashtable实现。当每个键和值的长度小于64字节,那就用ziplist,反之则升级为hashtable。
一个redis的字典中保存有一个长度为2的hash表数组,其中一个用来保存键值对,另一个只有扩容时使用。
在这里插入图片描述
redis的hash表的关键字段
table:用于存储键值对
size:表示哈希表的数组大小
used:表示哈希表中已经存储的键值对的个数
sizemask:大小永远为 size - 1,该属性用于计算哈希值
解决hash冲突:链地址法
计算负载因子:load_factor = ht[0].used / ht[0].size

1.3.1 rehash

Redis 哈希表不仅提供了扩容还提供了收缩机制,扩容与收缩都是通过 rehash 完成的。与 HashMap一样,Redis 中的哈希表想要执行 rehash 扩容操作也是需要一定条件的,主要为以下 2 个:
服务器目前没有执行BGREWRITEAOF 或者 BGSAVE 命令,切哈希表的负载因子大于等于 1 ;
服务器目前正在执行 BGSAVE 或者BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于5,因为redis开启子进程,使用cow进行写入,如果cow途中rehash,那会导致父进程重新开辟一块儿内存空间来rehash,浪费内存空间
下面是收缩 rehash 的条件: 哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作

字典的rehashidx用于保存当前的rehash进度

1.3.2 渐进式rehash

如果一次性完成rehash,那么会消耗太久的时间,redis是单进程单线程的,会造成服务器被阻塞,暂时不可用。

  1. 为h[1]开辟足够的空间;
  2. 初始化rehashidx为0,即表示从hash表的0索引开始rehash
  3. 每执行一条增删改查,就将原hash表对应索引的键值对全拷贝过来;
  4. 直到完成,设置rehashidx为-1
    渐进式rehash途中的hash表操作:更新、删除、查询都在h[0]和h[1]两个hash表中操作;新增操作只新增到h[1]中

1.4.set

set保存不重复的元素,无序;底层由intset或hashtable结合实现。
在这里插入图片描述

1.5.zSet

sortedset同时会由两种数据结构支持,ziplist和skiplist.
只有同时满足如下条件是,使用的是ziplist,其他时候则是使用skiplist:
有序集合保存的元素数量小于128个
有序集合保存的所有元素的长度小于64字节。

2. redis为什么这么快

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速;
  2. Redis 设计了一套自己的数据结构;
  3. 采用单线程来负责存取、交互,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  4. 使用多路 I/O 复用模型,非阻塞 IO;

3. redis的数据淘汰策略

redis内存数据数据集大小升到一定大的时候,就会实行数据淘汰策略(回收策略)。

volatile-lru:从设置了过期时间的数据集中,选择最近最久未使用的数据释放;
allkeys-lru:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放;
volatile-random:从设置了过期时间的数据集中,随机选择一个数据进行释放;
allkeys-random:从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放;
volatile-ttl:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作;
noeviction:不删除任意数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误。

4. redis的持久化机制

RDBAOF两种持久化机制:

4.1 RDB

RDB是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件。

有两种方式:SAVEBGSAVE
SAVE:服务器进程会忙于持久化,而不能处理来自客户端的请求。

4.1.1 RDB的BGSAVE的优缺点

下面谈谈BGSAVE
优点
使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能 ;
数据紧凑、适合做备份
缺点
一是持久化的过程很耗时;
二是RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;

4.2 AOF

AOF是将更新、删除、添加这些命令写入一个文件当中,恢复数据就从这个文件逐行读出命令并执行即可。

AOF持久化的三种方式:
always:每当有命令被执行,都要添加到AOF文件
everysec:每秒写一次(每秒将缓存区中的命令写入文件)
no:由操作系统决定何时写入文件

4.2.1 AOF重写 bgrewriteaof

因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。

重写的实现:对于数据库中全部的key,获得其值,并用一条命令来记录这对键值对,来代替之前记录这对键值对的多对键值对。

4.2.2 AOF的优缺点

优点:

有多种持久化策略供选择,出现故障时,丢失的数据量少;
AOF文件过大时,可以重写AOF文件;

缺点:

对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。

5.Redis和Memcache区别

性能:memcache使用多核、redis使用单核。多核可以充分利用闲置cpu;
数据类型:memcache仅支持简单kv键值对,redis支持多种数据结构;redis还可以做一些简单的逻辑运算。
可靠性:memcache不支持持久化,如果突然断电,那么内存中的数据是不可复原的,而redis支持AOF和RDB两种持久化;
限制:Memcached单个key-value大小有限,一个value最大只支持1MB,而Redis最大支持512MB。

6. redis的key失效机制

一种是在访问key的时候,检查该key是否已过期,过期则删除;
另一种是定期检查key,在所有key里面随机抽取一些key来检查,若过期则删除。

7. redis缓存穿透、击穿、雪崩

缓存穿透

用户不断请求redis中没有且数据库中也没有的数据(例如id =
-1)。因为数据库中也没有该条数据,那么查询那些不存在的数据是不会写入缓存的,所以每次都会重新区数据库查。流量大的话,数据库就挂了。

解决方法

1、接口校验。在正常业务流程中可能会存在少量访问不存在 key
的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。

2、缓存空值。当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。

3、布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。

缓存击穿

某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

解决方案

设置热点数据永不过期,然后由定时任务去异步加载数据,更新缓存;
加互斥锁:在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。最理想的互斥锁是根据key的值来加锁。
在这里插入图片描述

缓存雪崩

大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。

解决方案

1、过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
2、热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
3、加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。

8. redis读写分离模型

只在master上写,然后同步到slave上,slave负责读。这种读写分离模型只适用于读多写少的场景。
缺点:如果很多的话,同步数据的压力会很大。因为每个节点都保存的是全部的数据。

9.redis数据分片模型

数据分片模型可以解决读写分离模型的痛点:每个节点只保存数据的子集,每个节点都是一个master。减小了单个节点的存储压力,也能充分利用每个节点的存储能力和计算能力。

10. Redis使用单线程模型为什么性能依然很好?

避免了线程切换的资源消耗
单线程不存在资源共享与竞争,不用考虑锁的问题
基于内存的,内存的读写速度非常快
使用非阻塞的 IO 多路复用机制
数据存储进行了压缩优化
使用了高性能数据结构,如 Hash、跳表等

11. Redis的同步机制

Redis 通过同步(sync)和指令传播(command propagate)两个操作完成同步
同步(sync):将从节点的数据库状态更新至与主节点的数据库状态一致。
步骤
从节点向主节点发送 SYNC 指令;
收到 SYNC 指令,主节点执行 BGSAVE 指令,在后台生成一个 RDB 文件,并使用一个缓冲区记录从现在开始执行的所有写指令;
主节点 BGSAVE 指令执行后,会将生成的 RDB 文件发送给从节点;
从节点接收、载入 RDB 文件,将数据库状态更新至主节点执行 BGSAVE 指令时的数据库状态;
从节点加载完 RDB 文件,通知主节点将记录在缓冲区里面的所有写指令发送给从节点,从节点执行这些写指令,将数据库状态更新至主节点当前数据库状态。
指令传播(command propagate):主节点数据被修改,会主动向从节点发送执行的写指令,从节点执行之后,两个节点数据状态又保持一致

12.Redis哨兵(Sentinel)模式

当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这时候可以用哨兵模式。
哨兵是一个独立的进程,作为进程,它会独立运行。
通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

哨兵挂了怎么办?答:哨兵也弄个集群。

13.如何保证Redis中存的都是热点数据

Redis存储在内存中的数据升到配置大小时,就进行数据淘汰
使用 allkeys-lru 策略,从数据集(server.db[i].dict)中挑选最近最少使用的数据优先淘汰,即可满足保存热点数据

14.Redis中如何找出已知前缀的key

使用 keys 指令可以查找指定模式的 key 列表
如果在线上使用,keys 指令会导致线程阻塞,直到执行结束。可以 使用 scan 指令,无阻塞的提取出指定模式的 key 列表,但会有一定的重复概率,需要在客户端做一次去重,整体耗时比直接用 keys 指令长

15.redis的集群

点击查看
cluster集群的master选举机制:
在这里插入图片描述

16. redis的IO多路复用

点击查看
重点

epoll_create(int size)//size是对大小的建议,并非真实大小

epoll_create:创建一个epoll文件描述符;创建eventpoll实例,其中包含红黑树cache和双向链表。

int epollctl(int epfd, int op, int fd, struct epollevent *event)

epollctl:可以对红黑树进行三种操作:增删改;如果是将新的fd加入红黑树,那么还会为该fd注册回调函数;op操作类型,用三个宏EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD,来分别表示增删改对fd的监听。

int epollwait(int epfd, struct epollevent *events, int maxevents, int timeout);

epollwait:读取就绪队列
在这里插入图片描述

17.一致性hash

点击查看

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值