Redis面试题

133 阅读12分钟

如何保证缓存和数据库双写一致性?

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

缺点:写多的场景下,每次更新操作都要更新下缓存,性能损耗非常大,一般不用

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

Q1: 更新数据操作成功,缓存却删除失败,其他请求每次查询到缓存都是错误的数据,怎么解决数据不一致?
A1: 通过消息队列MQ进行删除补偿,这种方案会大量侵入业务代码,可通过订阅BinLog日志优化,会增加系统复杂度

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

Q1: 并发下,读请求A在写请求B更新操作期间同时查询缓存为空,再查询数据库为旧值(事务还没提交或还没更新成功),导致数据不一致?
A1: 延时双删

Q2: 延时双删在数据库主从架构下,主从同步延迟导致数据不一致?
A2: 强制读主库

Redis内存淘汰策略有哪些?

当内存使用超过配置时的内存淘汰策略:不处理/LRU/LFU/随机/过期时间

noeviction: 不会驱逐任何key

allkeys-lru: 对所有key使用LRU算法进行删除

volatile-lru: 对所有设置了过期时间的key使用LRU算法进行删除

allkeys-random: 对所有key随机删除

volatile-random: 对所有设置了过期时间的key随机删除

volatile-ttl: 删除马上要过期的key

allkeys-lfu: 对所有key使用LFU算法进行删除

volatile-lfu: 对所有设置了过期时间的key使用LFU算法进行删除

LRU内存淘汰策略?

最近最少使用对象淘汰算法(基于时间)

核心:如果数据最近被访问过,那么将来被访问的几率也越高

实现步骤

1、底层结构使用链表,插入删除效率高O(1),查询效率低O(n)

2、将缓存命中的Key先从链表遍历删除,再将Key存储到链表头部

3、当内存达到设定的阈值时,先删除链表尾部的Key

Q1: 怎么解决链表遍历查询效率低?

A1: 使用HashMap保存Key和链表的索引值(空间转时间)

A2: 使用JDK的实现LinkedHashMap

LFU内存淘汰策略?

最近最不常用对象淘汰算法(基于访问频次)

核心:如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

实现步骤

1、底层结构使用HashMap<String,Integer>对应key信息和访问次数,

2、插入/删除/查询指定Key的时间复杂度O(1),遍历最少访问次数的Key时间复杂度O(n)

Q1: 怎么解决查询次数最少的key耗时问题?

A1: 使用双Hash解决,另一个HashMap把访问频次作为Key,对应的频次的数据组成链表作为Value

缓存雪崩、缓存穿透、缓存击穿?

缓存雪崩:同一时间大面积Key失效,导致所有请求都达到数据库,严重会引发大面积服务挂掉

A1: 设置Key的时候采用失效时间+随机值避免过期时间一致导致同一时间集体失效

A2:设置热点数据永不过期

A3: 集群部署缓存服务,解决大量Key同时失效导致Redis缓存服务挂掉

缓存穿透:请求访问不存在的Key,同时数据库也不存在该数据,导致每次请求都穿透缓存打到数据库

A1: 布隆过滤器(BloomFilter),可判断某条数据是否存在数据库中

A2: 根据具体业务给对应Key设置不同的标识符value=null/数据不存在/0,设置过期时间为30s

A3: Nginx控制同一IP多次请求自动加入黑名单

缓存击穿:某个热点Key被并发访问,待Key过期失效因没重新设值,请求直接击穿缓存打到数据库上

A1: 设置热点数据永不过期

A2: 使用分布式锁控制并发请求,只让其中一个请求成功后,其他请求即可访问缓存拿到数据

A3: 请求接口限流熔断降级

Redis如何保证高可用?

哨兵集群Sentinel:必须用三个以上实例去保证自己的健壮性

集群部署:主从+哨兵保证集群的高可用

数据持久化:集群不能保证数据不丢失,RDB做冷备,AOF做热备

Redis主从原理?

Redis单机模式下能支持的QPS是有上限的,读10W+,写8W1,而且实际应用中既要读又要写。

因此可以使用主从架构模式让master节点负责写任务,slave节点负责读任务,让缓存服务负担减轻,而且能轻松实现水平扩容

主从数据复制原理:

1、当启动slave节点时,会发送psync命令给master

2、如果是第一次连接到master,他会先触发一次全量复制

3、master会fork出一个子进程生成RDB快照,同时会把新的写请求都缓存在内存

4、RDB文件生成后,master会将其发送给slave

5、slave拿到之后先写入本地的磁盘,再加载到内存

6、然后master再把内存里面缓存的新命令发给slave

Redis哨兵原理?

背景:当主节点master出现异常时不能实现主从复制节点自动切换、故障处理转移等操作。而哨兵机制正是基于Redis的主从复制进行的节点监控管理机制,是Redis高可用的一个重要实现。

核心机制:三个定时监控任务完成对各个节点的发现和监控

定时任务触发间隔功能
定时info10sSentinel节点获取最新redis节点信息和拓扑关系
定时pub/sub2sSentinel节点通过订阅master频道进行彼此信息通信
定时ping1sSentinel节点检查与所有redis节点、其他哨兵节点网络

故障转移

  • sentinel node节点监听到master节点出现故障,slave从节点无法对master进行数据复制
  • sentinel node发现master节点异常,会在sentinel集群节点中内部进行投票选举出leader来进行masterslave业务数据节点故障进行转移处理,并通知client客户端
  • 当新的master产生后,slave节点会复制新的master,但是还会继续监听旧的master节点
  • 当旧的master节点故障恢复后,由于sentinel集群一直监听,会重新将其作为新的slave节点纳入集群管理中,开始复制新的master节点,实现节点故障后的重复利用

哨兵leader选举(Raft算法)

  • 当一个sentinel节点确认redis集群的主节点下线后
  • 请求其他sentinel节点要求将自己选举为leader。被请求的sentinel节点如果没有同意过其他节点的选举请求,则默认同意,选举票数+1,否则不同意
  • 当一个sentinel节点获得选举票数达到最低票数(sentinel节点数/2+1)时,选举该节点为leader

下线判断(通过ping/pong消息实现节点间通信用来交互节点间的状态信息)

  • sentinel集群中每个节点会定时对redis集群中所有节点进行心跳检测
  • 如果一个节点在down-after-milliseconds时间内没回复sentinel节点的心跳包,则该sentinel节点认为其主观下线
  • 当节点被一个sentinel节点记为主观下线时,并不意味着该节点肯定故障了,还需要sentinel集群的其他sentinel节点共同判断(半数以上)为主观下线才行。

master节点选举

  • 过滤故障节点
  • 选择优先级slave-priority最大的slave作为master
  • 选择复制偏移量offset最大的slave作为master
  • 选择runId最小的slave作为master,随机方案兜底

Redis集群原理?

juejin.cn/post/692528…

数据分区路由规则:哈希取模算法、一致性哈希、虚拟槽

Gossip通讯协议:节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息

  • meet: 新节点加入触发,通知新节点加入
  • ping: 每秒触发,检测节点在线、交互状态信息
  • pong: 回复、广播触发,封装自身状态信息
  • fail: 节点异常触发,广播节点异常信息

优点:能够保持对集群节点信息的更新采集,较快响应异常

缺点:由于是点对点通讯,集群节点间通讯成本较高,且随集群规模越大,成本呈指数级增长

集群搭建流程

  • 准备节点:做好配置
  • 节点握手:cluster meet {ip} {port} 建立节点间连接,开始Gossip协议消息通讯,这时候节点集群状态还是fail的,还需要进行槽位分配
  • 分配槽:16384个槽位全部分配给节点后,建立集群节点与槽范围的映射关系,集群才进入在线状态

Redis Cluster中计算槽位hash值采用CRC16算法,16bit可以产生2^16=65536个值,为什么不mod(65536),而是mod(16384) ??

  1. 正常的心跳包会携带着节点的完整配置,通过使用与旧的配置信息幂等的配置来更新旧配置。 这意味着它们需要包含原始形式的节点的插槽配置信息,使用16384个slots的话将会占用2k的空间,但是如果使用65536个slots的话将会占用8k空间。
  2. 同时,由于其他设计权衡,RedisCluster不太可能扩展到超过1000个Master节点。

集群限制

  • mget/mset只支持相同槽值的key执行批量操作,对于映射为不同slot值的key可能存在于多个节点因此不被支持
  • 可通过pipeline方式实现,将mget、mset命令进行批量的get、set命令转换,执行后聚合数据返回
  • 当试图通过lua脚本来原子地执行一系列命令时,对Key的读写也必须遵循这个要求

Redis冷备、热备、数据恢复?

两张数据持久化方式:RDB、AOF

RDB: 全量持久化,执行bgsave命令后先fork出一个子进程生成RDB快照文件,保存fork前的全量数据

优点:

1、适合做冷备,可定时备份数据到远程服务器,可将数据回滚到指定时间点

2、对Redis的性能影响非常小,在同步数据时只是fork出一个子进程去做持久化

3、恢复数据速度比AOF快

缺点:

1、默认5分钟或者更久的时间才会生成一次,会丢失5分钟的数据

2、生成快照时如果文件很大,客户端可能会暂停几毫秒甚至几秒

AOF: 增量持久化,执行fsync命令后后台开启一个线程将aof buffer缓存区的新命令以append-only的方式追加到AOF日志文件

优点:

1、每秒一次通过后台线程fsync操作,最多丢一秒的数据

2、对日志文件的操作以append-only的方式去写,少了磁盘寻址的开销,写入性能惊人,文件不容易破损

3、通过非常可读的方式记录日志,适合做灾难性数据误删除的紧急恢复

缺点:

1、AOF文件比RDB的大,恢复速度慢

2、开启AOF后Redis支持写的QPS会比RDB支持写的要低

Redis分布式锁?

Redis数据类型?

基础数据类型

  • String
  • Hash
  • List
  • Set
  • SortedSet

高级数据类型

  • HyperLogLog: 供不精确的去重计数功能
  • Geo:保存地理位置,作位置距离计算、根据半径计算位置
  • Pub/Sub: 订阅发布功能,简单的消息队列
  • BitMap:实现BloomFilter
  • pipeline: 批量执行一组指令,一次性返回全部结果
  • lua: 提交lua脚本来执行一系列原子性的操作
  • 事务: 只保证串行执行指令,保证全部执行,但是执行指令失败是并不会回滚,而是继续执行下去

Module

  • BloomFilter
  • RedisSearch
  • Redis-ML

Redis为什么那么快?

1、基于内存实现并且KV存储,查找和操作的时间复杂度都是O(1)

2、数据结构简单,对数据操作也简单

3、单线程模型避免了不必要的上下文切换和竞争条件,也不存在多进程或多线程导致的切换而消耗CPU,不用去考虑各种锁的问题。不存在加锁释放锁死锁导致的性能消耗问题

4、IO多路复用模型:select/epoll机制,一个线程处理多个IO流

LUA脚本?

Redis单个命令是原子性的,有时候我们也需要能够组合多个Redis命令且保证原子性的执行,在2.6版本引入了Lua.

Redis怎么实现延时队列?

使用SortedSet数据类型实现,将时间戳当做score,lpush生产消息,rbpop拉取阻塞消息,根据score消费消息

Pipeline有什么用?

Redis6.0为啥改成多线程模型?单线程不是已经很快了吗?

redis6.0之前网络IO请求和数据读写都是单线程的,IO多路复用虽然能提升IO的利用率,但是在处理IO请求时,调用select的过程是阻塞的,也就是说这个过程是阻塞线程,如果并发量很高,此处可能会成为瓶颈。

redis6.0在网络IO请求的处理用了多线程,解决之前大量cpu时间片耗费在网络io的同步处理上,充分发挥多核的优势,减少由于网络io等待造成的影响。

Redis跳跃表?