redis卷卷面试题

Redis与memcache的区别

  1. Redis支持多种数据类型,memcache只支持一种数据类型
  2. Redis支持数据持久化,memcache不支持数据持久化
  3. Redis是单线程,memcache是多线程

Redis为什么这么块

  1. 基于内存实现,省去了磁盘I/O的时间
  2. 几种数据类型都使用了高效的数据结构,如sds,hashtable,skiplist,ziplist
  3. 使用了单线程模式,避免了多线程的上下文频繁切换,并且用了I/O多路复用的模型,提高了效率

Redis几种数据结构

字符串

使用简单动态字符串,有以下优点:

o(1)的时间复杂度来获取字符串长度

空间预分配,不用在拓展字符串的时候频繁的重新分配空间

并且是二进制安全

如果字符串较短, 长度len或者是剩余空间free用一个int来表示, 比较浪费空间, 所以还会用好几种结构来表示字符串, 主要是用来压缩头部信息在源码里用sdshdr5 sdshdr8来表示, 长度少于32时候用sdshdr5还能省去free这个字段, 只用flag字段中的闲置的五位来表示当前字符串长度

字典

因为是作为k-v型数据库,这样的键值是用字典来进行存储,而字典是使用了两张哈希表来实现的,哈希冲突用的是链地址法, 采用头插法插入到冲突链表中,

请添加图片描述

sizemask = size-1

used表示有多少个元素
请添加图片描述

跳表

在链表的基础上,增加多级索引提升查找效率,平均时间复杂度是logN,最坏是N

每相邻两个节点增加一个指针, 让指针指向下下个节点, 所有新增加的指针, 连成一个新的链表, 但是它包含的节点个数只有原来的一半

请添加图片描述

整数集合

一个集合只包含整数值元素, 并且集合元素不多的时候, 会用整数集合来实现

typedef struct intset{
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
}intset;

contents存的数据, length存储了包含多少个元素, encoding表示整数集合的类型是什么

新元素比原来所有的元素类型长度都要长的时候就会进行升级

但是不支持降级

插入操作就会挨个比对, 没有才插入

元素多了采用hashtable来实习集合

压缩列表

zltail用于计算最后一个entry位置的偏移量
请添加图片描述

prevlength可以是一个字节

请添加图片描述

也可以是五个字节

请添加图片描述

  • entry的前8位小于254,则这8位就表示上一个节点的长度
  • entry的前8位等于254,则意味着上一个节点的长度无法用8位表示,后面32位才是真实的prevlength。用254 不用255(11111111)作为分界是因为255是zlend的值,它用于判断ziplist是否到达尾部。

encoding前两位是11代表整形

前两位00代表是字符串类型

encoding也有1个字节或者是5个字节的

如果插入在列表中的数据, 后面的节点的prelength只有一个字节, 不能表示前面的长度, 会进行扩展

数据编码

Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。

  • String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。

  • List:如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码

  • Hash:哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。

  • Set:如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码。

  • Zset:当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码

Redis是单线程还是多线程

6.x版本之前是单线程,使用epoll的多路复用模型

6.x版本之后是多线程,但是工作线程只有一个,会有多个IO线程,去读取客户端连接发过来的数据,对于发过来的数据进行计算还是主线程上,进行串行计算,将结果分给不同的线程,去输出

多IO线程能够提高系统的吞吐量,能够减轻的网卡缓存压力,以及socket的缓存队列的压力

Redis存在线程安全的问题吗

Redis内部是线程安全的,因为只有单线程,操作都是串行执行,但是外部业务发送的顺序是无法保障的,需要业务上自行去解决该问题。

缓存穿透,缓存击穿,缓存雪崩

缓存请求原理:

在这里插入图片描述

缓存穿透

原理

请求大量不存在的key,穿过缓存,访问到持久层,导致持久层的压力过大

解决方案

  • 设置key-null

针对大量的请求,但是只请求了少量的key值,可以使用该方法来拦截这些请求,在缓存层的时候就返回的null值,不会访问到持久层

  • 布隆过滤器

针对大量的请求,并且请求了大量不存在的key值,key-null的形式不是能很好的解决这样的问题,就可以使用布隆过滤器来拦截。

布隆过滤器原理如下:

请添加图片描述

一个key值通过不同的hash函数,映射出不同的index,维持一个bitmap,这些index上置为1。

再有key值访问时,先通过不同的hash去计算几个index值,如果在bitmap中,这几个index值不全为1,则返回null

这样能够保证布隆过滤器告诉你不存在,那就一定不存在,告诉你存在的话,不一定存在。

能够有效的防护请求大量不存在的key值的情况。

缓存击穿

原理

缓存击穿是指缓存中没有,但是数据库中有,由于并发用户特别多,同时读缓存没有读到,都同时去取数据库,引起数据库压力瞬间增大。导致这样的原因可能是热点key过期,或者是某一冷门key突然热门。

解决方案

  • 设置互斥锁,首先查询缓存,没有,就去抢锁,抢到的拿到数据,写入缓存,返回数据,没有抢到锁的,睡上100ms,再回到查询缓存那一步,就能够在缓存上查到数据了。
  • 设置热点key永不过期,可以是物理上的永不过期,不设置过期时间,也可以是逻辑上的永不过期,过期就再启一个线程去把他写回缓存里。

缓存雪崩

原理

与缓存击穿一样,但是不是单个key值在缓存查不到,而是多个key值在缓存里查不到,但是能再持久化层查到。

解决方案

  • 在key值的失效时间上加上一个随机值,让缓存失效时间分散开来
  • 热key永不过期

缓存的回收与淘汰

缓存的回收

key过期了,后台有程勋轮询,如果发现已经过期的key,回将空间回收回来,对cpu不友好但是对内存友好

或者是在一次请求的时候,发现key已经过期了,就会将空间回收回来,对cup友好但是对内存不友好

Redis中同时使用了惰性过期和定期过期两种过期策略。

缓存的淘汰

在内存空间不够的情况下,会淘汰一些内容

lru:分两种,一种是从设置了时间的key中,使用lru淘汰最近最少使用的key,还有一种就是对所有的key去进行lru

lfu:同样分为两种,同上

random:随机淘汰,有两种方式,同上

ttl:会在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的会被优先淘汰

noeviction:默认策略,当内存不足的时候,新写入的就会报错

事务

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

Redis事务的主要作用就是串联多个命令防止别的命令插队。

但是在入队完成时, 还没有exec, 有其他的客户端发送请求, 修改了属性, 会导致冲突, 这个时候会使用乐观锁也就是watch来解决

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务**执行之前这个(**或这些) key 被其他命令所改动,那么事务将被打断。

悲观锁

请添加图片描述

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁。

乐观锁

在这里插入图片描述

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

缓存预热

提前把数据塞入redis,塞入热数据

没有考虑到的热数据,也就可以用缓存击穿,雪崩的预防方案

热Key问题

如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。

解决方案:

通过经验,或者是客户端的分析,服务端的分析,分析出热key,将热key分散到集群的不同的服务器里

实现消息队列

根据redis提供的发布和订阅功能,可以实现一个简单的消息队列系统。

持久化的机制

RDB

在指定的间隔内,以快照的模式,将redis里的所有数据都进行持久化。

优点:

适合大规模的数据恢复,备份

缺点:

容易丢数据,会丢最后一个间隔时间里面的数据

且新旧版本的redis的RDB文件格式也不兼容

AOF

类似于日志的形式,将每次写操作都追加写在日志里面

优点:

数据的一致性和完整性更高

缺点:

记录内容太多,文件大,恢复速度慢

如何实现Redis的高可用

主从模式

部署多台机器,有一个主节点,负责写操作,有多个从节点,负责读操作。

从节点的数据同过主从复制机制来获得

全量复制流程:

在这里插入图片描述

增量复制流程:

在这里插入图片描述

薪火相传模式

主节点只有一个从节点,但是从节点可以有从节点

这样可以减轻主节点同步从节点的负担,只需要同步一个从节点即可

但是一个从节点出现问题,随后的从节点就会都出问题

哨兵模式

如果主节点故障,需要从节点执行slave of no one来升级为从节点,再重新绑定其他从节点。这过程是手动的。

可以使用哨兵模式来自动的执行这一过程。

设置一个或者多个哨兵,来监视主节点和所有的从节点,多哨兵模式,哨兵之间也会互相监督。

Cluster集群模式

主从模式下,每个节点存储一样的内容,浪费内存,且在一个单机中,不好实现扩容,所以可以使用集群

通讯模式

在这里插入图片描述

  • meet消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。

  • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。

  • pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。

  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

特别的,每个节点是通过集群总线(cluster bus) 与其他的节点进行通信的。通讯时,使用特殊的端口号,即对外服务端口号加10000。例如如果某个node的端口号是6379,那么它与其它nodes通信的端口号是 16379。nodes 之间的通信采用特殊的二进制协议。

插槽

** 插槽算法 ** 把整个数据库被分为16384个slot(槽),每个进入Redis的键值对,根据key进行散列,分配到这16384插槽中的一个。使用的哈希映射也比较简单,用CRC16算法计算出一个16 位的值,再对16384取模。数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点都可以处理这16384个槽。

集群中的每个节点负责一部分的hash槽,比如当前集群有A、B、C个节点,每个节点上的哈希槽数 =16384/3,那么就有:

  • 节点A负责0~5460号哈希槽

  • 节点B负责5461~10922号哈希槽

  • 节点C负责10923~16383号哈希槽

redis实现分布式锁

通过setnx实现上锁,del实现释放锁

expire 设置过期时间,防止死锁

上锁之后突然出现异常,无法设置过期时间

set user 10 nx ex 12,利用原子性,在上锁的同时设置时间

请添加图片描述

使用uuid防止误删,set user uuid nx ex 12,利用原子性,在上锁的同时设置时间

释放锁的时候判断

在这里插入图片描述

使用lua脚本,保证一系列操作的原子性,防止其他操作插队

redlock

使用setnx + ex + uuid + lua 脚本还是可能会出现问题

只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

在Redis的master节点上拿到了锁;

但是这个加锁的key还没有同步到slave节点;

master故障,发生故障转移,slave节点升级为master节点;

导致锁丢失。


  • 获取当前时间(单位是毫秒)。
  • 轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
  • 客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
  • 如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
  • 如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。

在这里插入图片描述

失败重试

当一个客户端获取锁失败时,这个客户端应该在一个随机延时后进行重试,之所以采用随机延时是为了避免不同客户端同时重试导致谁都无法拿到锁的情况出现。同样的道理客户端越快尝试在大多数Redis节点获取锁,出现多个客户端同时竞争锁和重试的时间窗口越小,可能性就越低,所以最完美的情况下,客户端应该用多路传输的方式同时向所有Redis节点发送SET命令。 这里非常有必要强调一下客户端如果没有在多数节点获取到锁,一定要尽快在获取锁成功的节点上释放锁,这样就没必要等到key超时后才能重新获取这个锁(但是如果网络分区的情况发生而且客户端无法连接到Redis节点时,会损失等待key超时这段时间的系统可用性)

数据库与缓存不一致如何解决

新更新缓存, 再更新数据库

如果事务回滚, 缓存已经存了, 数据库并没有该数据

先更新数据库, 再更新缓存

会有脏数据

先删除缓存, 在更新数据库

请添加图片描述

先更新数据库, 再删除缓存

小概率发生

请添加图片描述

大概率发生

请添加图片描述

延迟删除

解决小概率发生的情况, 让小概率情况变成大概率情况

延迟双删

基于先删除缓存, 再更新数据库, 再延迟一段时间, 再次删除

有序队列

放在有序队列里, 先删除缓存, 再更新数据库, 其他请求请求缓存会miss, 加到更新数据库的操作之后, 去请求数据库, 所以会等更新数据库完了之后再请求到最新的数据库值, 这样再更新缓存的时候, 就是一致的了

Mysql读写分离的情况

请添加图片描述

订阅binlog日志

不删除redis中的值, 就不会去从库查了

请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值