Redis面试题大全

文章目录

  • redis单线程为什么这么快
  • redis单线程如何处理这么多的并发客户端连接
  • redis是怎样实现持久化的
    • redis4.0混合持久化
    • aof和rdb优缺点
    • 触发aofrewrite的操作
  • 缓存淘汰策略
  • redis怎么进行选举的
  • 怎么认定某个集群节点出现故障
  • 怎么对redis集群水平扩展
  • 怎么删除redis集群的节点
  • 分布式寻址算法
    • 使用虚拟槽分区的好处
  • redis过期key处理
  • redis主从复制
  • redis断点续传
  • 解决热点key大value解决方案
    • 你在项目中有没有遇到过redis热点问题,一般都是什么原因引起的
    • 热点key或大value会造成那些故障呢
    • 真实项目中,热点问题时如何定位的
      • 如何解决热点问题
      • redis支持丰富的数据类型,那么集合数据类型存储大value如何解决,线上遇到这种情况么
  • redis雪崩,穿透,击穿
    • 关于redis雪崩,穿透,击穿你是怎么理解的
      • redis雪崩
      • redis穿透
      • redis击穿
    • 工作中有没有遇到过redis雪崩类的问题,能举个例子么
    • BloomFilter过滤器
  • redis4.0支持的新功能

首先介绍实践方式,然后介绍原理,然后分析优缺点,提出总结改进方案

redis单线程为什么这么快

  1. redis数据都存储在内存中,并且都是纯内存访问,内存响应时间大约为100纳秒,这是redis的QPS过完的重要基础
  2. 非阻塞IO
    读写方法不会阻塞,而是能读多少读多少,能写多少写多少。取决于内核为套接字分配的缓冲区的大小
  3. IO多路复用
    单线程可以处理多个并发客户端的请求
  4. 单线程避免了线程切换和解决同步问题的时间消耗
    a)简化数据结构和算法的实现
    b)锁和线程切换通常都是性能杀手
    单线程的问题:redis只适用于那些需要快速执行的场景。因为若某个命令执行时间太长,会阻塞其他命令

redis单线程如何处理这么多的并发客户端连接

redis通过epoll方式来实现IO多路复用,将事件放到队列中,然后文件事件分派器再依序将事件分发给事件处理器。
C10K问题:假如有1w连接则基于进程/线程模型就需要创建1W个线程进行处理,如何突破单机性能是高性能网络编程要面对的问题
解决:一个进程/线程同时处理多个连接,这就是IP多路复用

  1. select方式:使用fd_set结构体告诉内核同时监听的那些文件句柄,然后通过遍历的方式判断文件句柄就绪或者超时。主要有两个缺陷1. 文件句柄有上限,2. 遍历耗时
  2. pool方式:
  3. epool方式:它不去轮询所有文件句柄是否已经就绪,而只对发生变化的文件句柄有兴趣。
    使用事件的就绪通知方式,通过epoll_ctl注册文件描述符fd,一旦fd就绪,内核通过回调机制激活该fd,epoll_wait便收到通知,通知应用程序。
    epoll使用一个fd管理多个fd,将用户fd的事件放到内核的事件表中,这样就只需从内核缓存拷贝一次到用户进程地址空间。也就是epoll通过内核与用户空间共享内存的方式实现实现事件就绪消息传递

redis是怎样实现持久化的

  1. rdb快照方式:将redis数据库中的二进制数据保存到dup.rdb文件中。可以通过配置save second times来修改生成rdb文件的时机,也可以通过bgsave进行手动生成RDB文件。但是rdb方式如果发生故障停机,可能丢失未保存的数据,导致丢失大量数据。
    有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为**定时生成 RDB 快照(snapshot)**非常便于进行数据库备份
  2. aof快照方式:aof方式将修改的每一条指令都记录到appendoly.aof中,通过配置appendonly yes来开启aof功能,而且可以使用fsync命令手动将aof缓冲区的数据同步到磁盘,而且可以通过配置复制积压缓冲区的大小改变fsync同步的策略,使用bgrewriteoaf瘦身aof文件。

redis4.0混合持久化

重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。redis4.0使用aof-use-rdb-preamble开启混合持久化。

aofrewrite也是先写一份全量数据到新AOF文件中再追加增量只不过全量数据是都是命令。redis4.0混合持久化则是先写入旧RDB全量数据追加增量日志,这样既可以提高aof文件的恢复速度又可以避免丢失大量数据。

aof和rdb优缺点

rdb优点是恢复速度块,适合进行备份以及异地容灾。缺点是生成文件时间长,一旦宕机丢失数据。数据路量大可能造成停止处理客户端请求。
aof优点是灵活设置同步策略,减小宕机丢失的数据。缺点是aof文件大,恢复速度慢

触发aofrewrite的操作

  1. 收到客户端发送的bgrewriteaof命令,等到持久化操作执行后,再重写aof
  2. 配置auto-aof-rewrite-percentage和auto-aof-rewrite-min-size参数,当aof文件大小,aof文件增长率高于配置值,则自动触发aof rewrite
  3. 配置appendonly yes时,调用startAppendOnly函数

缓存淘汰策略

当redis内存超出物理内存限制时,内存数据会和磁盘频繁进行交换swap,交换会让redis的性能急剧下降,最终导致redis不可用。

maxmemory-policy noeviction
我们可以设置maxmemory <bytes>,当数据达到限定大小后,会选择配置的策略淘汰数据

redis使用maxmemory来限制最大使用内存,当实际内存超出maxmemory时,redis提供了几种淘汰策略让用户自己决定该如何腾出新的空间继续提供读写服务。

  1. noeviction(默认策略):拒绝写请求,放行读请求

  2. volatile-lru:淘汰过期key,最少使用key优先被淘汰。没有设置过期时间的key不会被淘汰

  3. volatile-ttl:淘汰过期key,剩余生存时间最短的优先被淘汰。没有设置过期时间的key不会被淘汰

  4. volatile-random:淘汰过期key,过期key集合随机被淘汰。没有设置过期时间的key不会被淘汰

  5. allkeys-lru:淘汰所有key,最少使用优先被淘汰

  6. allkeys-random:淘汰所有key,随机淘汰key

如果只是拿redis做缓存,那么应该使用allkeys-xxx
如果使用redis的持久化功能,那就使用volatile-xxx,可以保证不过期的key不会被淘汰。

redis怎么进行选举的

  1. slave发现自己master变为fall
  2. 将自己记录的集群currentEpoch+1,并广播failover_auth_request信息
  3. 其他节点收到信息,只有master进行响应,判断请求者的合法性,并发送failover_auth_ack
  4. 尝试failover的slave收集failover_auth_ack
  5. 超过半数后变成新得master
  6. 广播pong通知其他集群节点

怎么认定某个集群节点出现故障

当某个节点超过cluster-node-timeout设置的时间时,才会认定该节点出现故障,需要进行主从切换,这样可以防止网络抖动发生频繁的主从切换。

怎么对redis集群水平扩展

Redis面试题大全_第1张图片

  1. 分别启动6个节点
  2. redis-cli --cluster create --cluster-replicas 1 ip:port创建集群
  3. redis-cli -c -h -p连接实例,查看集群状态cluster info,查看节点信息cluster nodes
  4. 启动新加两个节点8007-8008
  5. 通过redis-cli --cluster add-node <新节点ip><集群内主节点ip> 增加新节点,通过cluster-nodes发现未分配槽道
  6. redis-cli --cluster reshard <8001ip>进行重新分片,会询问要分多少个槽道出来??分给哪个节点??这里有两种方式,一种是all,将所有节点用作槽道的源节点,一种是done,自己输入源节点id
  7. 连接redis客户端,连接8008,然后执行cluster replicate 8007的节点id为当前节点指定一个主节点

怎么删除redis集群的节点

  1. 删除从节点
    redis-cli --cluster del-node ip:port 节点id 执行删除节点
  2. 删除主节点
    a)通过reshard <8007ip> 命令将要删除节点的哈希槽移到其他节点,会进行询问
    b) 通过del-node <8007ip> <8007nodeid>

询问的详情

How many slots do you want to move (from 1 to 16384)? 1000
What is the receiving node ID? fea53768189af3e3e4849038af13607f59ec84b0
Please enter all the source node IDs.
  Type 'all' to use all the nodes as source nodes for the hash slots.
  Type 'done' once you entered all the source nodes IDs.
Source node #1: 662809cf2d5bb138912dea7fb1e452f6e0f149da
Source node #2: done

Ready to move 1000 slots.
  Source nodes:
    M: 662809cf2d5bb138912dea7fb1e452f6e0f149da 192.168.5.100:8001
       slots:[0-5460] (5461 slots) master
       1 additional replica(s)
  Destination node:
    M: fea53768189af3e3e4849038af13607f59ec84b0 192.168.5.100:8007
       slots: (0 slots) master
  Resharding plan:
    Moving slot 0 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
    Moving slot 1 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
    Moving slot 2 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
    Moving slot 3 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
    .
    .
    .
    Moving slot 996 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
    Moving slot 997 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
    Moving slot 998 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
    Moving slot 999 from 662809cf2d5bb138912dea7fb1e452f6e0f149da
Do you want to proceed with the proposed reshard plan (yes/no)? yes
.
.
.
Moving slot 995 from 192.168.5.100:8001 to 192.168.5.100:8007: 
Moving slot 996 from 192.168.5.100:8001 to 192.168.5.100:8007: 
Moving slot 997 from 192.168.5.100:8001 to 192.168.5.100:8007: 
Moving slot 998 from 192.168.5.100:8001 to 192.168.5.100:8007: 
Moving slot 999 from 192.168.5.100:8001 to 192.168.5.100:8007: 
[root@localhost 8007]# 

分布式寻址算法

  1. hash取余算法
    当一个节点宕机,大部分请求都会导致无法获取到缓存
  2. 一致性hash算法
    该算法将hash值空间组织成一个虚拟的圆环,然后将各个master节点进行hash确定其位置。确认key的node节点的方法是:计算hash值,顺时针寻找最近的master节点。
    优点:自动节点迁移,通过虚拟节点自动负载均衡
    缺点:节点宕机会将负载传递给下一个相邻节点-主要是紧耦合
  3. redis cluster数据分片算法,又叫虚拟槽分区
    所有的键都映射到16384个槽道中,redis每个节点负责维护一部分槽道。确认key的node节点方法是:计算hash值,然后通过slots数组判断槽所属的节点

使用虚拟槽分区的好处

crc16(key) mod 16384

  1. 当某个master 宕机时,不会影响其他机器的数据,因为找的是hash slot,从而实现细粒度的控制管理
  2. 方便添加和移除新节点,当需要增加节点时,只需要将其他节点的某些槽道移动到新节点就可以了,当移除节点时,只需要将移除节点上的哈希槽移动到其他节点
  3. 哈希槽重分片时不会停止redis服务。通过slot解耦key的hash值和节点,从而添加节点还是删除节点都不会造成集群不可用。

redis过期key处理

redis使用的是惰性删除,和定期删除,惰性删除在操作过期键才会进行删除,而定期删除则是每隔一段时间主动进行删除

redis主从复制

  1. 客户端向从服务器发送slaveof master_ip master_port命令,让其复制主服务器
  2. 从服务器异步将ip和port保存到redisServer{msterhost属性,masterport属性}然后返回OK
  3. 从服务器根据ip+port向主服务器创建套接字连接,建立成功后,从服务器将为套接字关联事件处理器,主服务器将为客户端创建相应客户端状态
  4. 从服务器发送PING命令检查套接字读写是否正常,若主服务器 返回PONG回复,则说明网络连接状态正常
  5. 从服务器向主服务器发auth进行身份认证
  6. 从服务器通过命令replconf listening-port port向主服务器发送自己的监听端口
  7. 从服务器向主服务器发送psync命令执行同步操作
  8. 当同步完成之后,主从服务器互为对方客户端(可相互发送请求),主从进入命令传播阶段,主服务将写命令发送到总服务器中

redis断点续传

支持部分重同步的数据结构有:主从偏移量,复制缓冲区,运行ID
redis通过psync实现断点续传
a) 从服务器首次复制执行psync ? -1命令执行完整重同步,否则向主服务器发送psync runid offset命令
b) 主服务器根据runid,offset判断执行何种重同步,如果返回fullresync runid offset,则执行完整重同步,如果返回+continue回复,则执行部分重同步,如果主服务返回-rr回复,表示主服务器低于redis2.8,不支持psync

解决热点key大value解决方案

实际工作经验中,热点数据问题在工作总相比雪崩更容易遇到,只是大部分时候热点不够热,都会被被提前告警解决,但这个问题一旦控制不了造成的线上问题也是足够让你年绩效垫底了。

正常情况下,Redis集群中的数据都会均匀分配到每个节点上,请求也会均匀的分布到每个分片上,但在一些特殊场景下,比如外部攻击,热点商品等,最典型的就是明星离婚啊,吃瓜群众来留言导致评论功能奔溃。这种短时间对某些key访问量过大,如果对相同key的请求汇聚到一台节点上,就会导致该节点负载过高成为瓶颈问题,导致雪崩等一系列问题。

你在项目中有没有遇到过redis热点问题,一般都是什么原因引起的

发现问题——>分析问题——>解决问题

  1. 高访问量的key,也就是热点key。根据经验如果一个key访问的QPS超过1000就要高度关注了,比如热门商品,热门话题
  2. 大value,有些key虽然访问QPS不高,但是由于value很大,造成网卡负载较大,单台机器网速可能出现 千兆/秒,造成IO故障
  3. 热点key+大value同时存在,这就是服务器杀手

热点key或大value会造成那些故障呢

  1. 造成QPS倾斜
  2. 造成机房网卡流量不足
  3. 导致集群宕机
  4. 导致redis缓存失效,从而使得数据库被击穿的连锁反应

真实项目中,热点问题时如何定位的

这个问题要看不同的业务场景。比如公司组织促销活动,那参加促销的商品肯定是有办法提前统计的,这种场景下使用“预估法”。对于突发事件,redis会自己监控热点数据

  1. 预估法
    人肉统计or系统统计可能会造成成为热点的数据,如促销活动商品,热门话题,纪念日活动等
  2. redis客户端收集法
    通过计数的方式统计key的请求次数,代码请入性强
  3. redis集群代理层统计
    类似Temproxy,codis这些基于代理的redis分布式架构,可以在Proxy层做收集上报。缺点是并非所有的redis集群都有proxy
  4. redis服务端收集
    监控redis单个节点的qps,发现qps倾斜到一定程度的节点进行monitor,获取热点key。高并发条件下会存在内存暴涨等可能,所以这种方法适合短时间内使用,但是只能统计一个节点的热点key,对于集群需要汇总统计
    Monitor 命令用于实时打印出 Redis 服务器接收到的命令,分析热点key
  5. 修改redis源代码(读源码想到的思路)
    redis4.0带来很多新特性,包括基于LFU的热点key发现机制,有了这个特性,我们就可以在此基础上实现热点key的统计

如何解决热点问题

  1. key拆分
    如果当前key的类型是一个字典类型,如果字典元素过多可以考虑将当前字典进行拆分,从而将热点key拆分成若干个新的key分不到不同redis节点上,从而减轻压力
  2. key迁移
    可以将热点key所在slot单独迁移到一个新得redis节点上,这样热点key就算qps很大,也不会影响到整个集群中的其他业务。还可以定制化开发,热点key自动迁移到独立节点上
  3. key限流
    对于读命令,我们可以通过迁移热点key添加从节点解决
    对于写命令我们可以通过单独针对这个热点key来限流
  4. 增加本地缓存
    对于数据一致性不很高的业务。可以将热点key缓存到业务机器的本地缓存中。当数据更新时,可能会造成业务的redis数据不一致问题

redis支持丰富的数据类型,那么集合数据类型存储大value如何解决,线上遇到这种情况么

由于redis是单线的,如果一次操作的value很大,会延长整个redis集群的响应时间
我先说说多大的value算大,根据公司基础架构给出的经验值
大:string类型value>10K,集合数据类型元素个数>1000
超大:string类型value>100k,集合数据类型个数>10000

由于 Redis 是单线程运行的,如果一次操作的 value 很大会对整个 redis 的响应时间造成负面影响,所以,业务上能拆则拆,下面介绍几个典型的分拆方案

  1. 一个较大的key-value拆分成几个key-value,将操作压力分摊到多个redis实例中,降低对单个redis的IO影响
  2. 集合类型中存储过多的元素,可以将这些元素分拆
    例如hash对象,原先存取流程为hget(hashkey,field) hset(hashkey,field,value)
    现在固定一个桶的数量,比如10000,先在本地计算field的hash之,模除10000,确定该field落在哪个key上。核心思想是set时将value打散,每次只get你需要的
newHashKey=hashkey+(hash(field)%10000)
hset(newHashKey,field,value)
hget(newHashKey,field)

redis雪崩,穿透,击穿

第一次线上遇到 Redis 雪崩的时候我是维护某电影线上平台(某首富家的)国内某电影平台,因为系统架构老旧,每年大年初一贺岁档电影上映,购票人达到高峰,系统就会发生因缓存失效问题导致的出票故障,当时底层是用的oracle小型机,依旧扛不住。大年初一,所有同事打开电脑被迫营业。关于缓存失效引起的类似问题,坚决要扼杀,对用户友好,对程序员友好,对老板也友好。

关于redis雪崩,穿透,击穿你是怎么理解的

程度:雪崩/穿透——>击穿

redis雪崩

redis雪崩指的是,缓存中大批热点数据过期后系统涌入大量查询请求,因为缓存失效而涌入数据库,导致数据库查询超时甚至宕机
解决:

  1. 将缓存失效时间尽量分散开,比如每个key的过期时间是随机的,防止同一时间大量数据过期的情况发生
  2. 缓存分布式部署,将热点数据均匀分布在不同redis节点中,有效分担压力
  3. 让redis数据永不过期

redis穿透

redis穿透指的是绕过redis和数据库,大规模发起不存在key的查询请求导致系统 压力过大最后故障
解决:

  1. 布隆过滤器
  2. 返回空值,遇到两者都不存在情况,在redis设置一个null值,过期时间段,目的在于同一个key再次请求立即返回null
try {
  Object value = dao.query(key); // 从数据库中查询数据
  if (value == null) {
    redis.set(key, null, 20);   // set null值,10ms过期时间
  } else {
    redis.set(key, value, 1000);
  }
} catch(Exception e) {
  redis.set(key, null, 20);
}

redis击穿

类似redis穿透,一般指一个热点key被穿透,成千上万次请求直接涌入数据库,百万QPS直接压垮数据库
解决:
公司常用方法是使用mutex,就是说,在缓存失效时不是立即取load db,而是先使用带成功返回值的操作(setnx)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存,否则就重试整个get方法。
总体思路是:通过带返回值的操作在redis设置一个信号量,使得多线程按序查询数据库并回设内存

public String get(key) {
    String value = redis.get(key);
    if (value == null) { //代表缓存值过期
        //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
        if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
            value = db.get(key);
                    redis.set(key, value, expire_secs);
                    redis.del(key_mutex);
            } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                    sleep(50);
                    get(key);  //重试
            }
        } else {
            return value;      
        }
}

工作中有没有遇到过redis雪崩类的问题,能举个例子么

APP用户打开页面都要轮询一遍未读消息。比如对于京东 APP 首页,每个用户每次进入都会查询有没有未读消息,公司用户总数是数亿级别,但是实际上并不是真的每个用户都有未读消息,对于没有未读消息的用户,也需要查询未读消息,甚至需要查询DB,实际上是比较浪费的

接口初期的设计是这样的,在Redis记录了有消息的用户userId,用户打开app,查询一次Redis,返回true代表有消息,然后查询真正的Redis消息列表或者DB等其它后续操作
当用户量逐渐变大,内存或Redis占用的空间也越来越多,有几千万、甚至几亿用户,那么内存空间会增加很多。如果是存储URL之类的字符串,消耗的内存简直是不能忍受。某些场景对所有用户轮询DB也是不可取的。
基于以上考虑,最好采用一种新得方案过滤。因为之前看过业内其他公司采用BlomFilter来实现海量数据去重过滤,各方面性能表现优异。因此考虑基于BloomFilter来实现过滤无效user从而大大减小了查询的无用功。

BloomFilter过滤器

可以用于检索一个元素是否在一个集合中
BollomFilter检索一个元素是否在一个集合中,会出现一定的错误率,它可以告诉你某样东西一定不存在或可能存在。
实现原理:
基本思想是利用一个足够好的hash函数将一个字符串映射到二进制位数组的一位,但是不管hash函数 多么高效,还是会出现冲突。
BloomFilter有个缺点是不能删除数据,因为删除数据可能会影响到其他数据
BllomFilter十分适合海量数据去重、过滤,尤其是当检查的字符串比较大时,极大节省了内存空间

redis4.0支持的新功能

  1. 新命令增加,如memory\swapdb
    memory usage 若当前key存在则返回key的value实际使用内存估算值,若key不存在,则返回nil>不包含key的expire内存占用
    memory stats 返回当前实例的内存使用状态
    memory doctor 当used_memory<5m不会进行诊断
  • peak分配内存大于总内存的1.5倍,说明内存碎片率高
  • hign fragmentation/fragmentation大于1.4,说明内存碎片率高
  • 每个normal client平均使用内存大于200k,说明pipeline可能使用不当
  • 每个slave client平均使用内存大于10mb,说明master的写入流量过高
    memory purage 主动释放内存碎片
peek.allocated:redis实例运行过程中,allocator分配的内存峰值
total.allocated:allocator分配的当前内存字节数
startup.allocated:redis启动占用内存字节数
replication.backlog:redis复制积压缓冲区
clients.slaves:所有slave clinents消耗的内存字节数
clients.normal:常规客户端消耗内存字节数
overhead.total:redis额外总开销=分配总内存-数据实际占用内存
dataset.bytes:redis数据实际占用内存
dataset.percentage:redis数据占用内存占总内存百分比
keys.count:redis实例key的数量
peek.percentage:当前内存使用量与峰值时的占比
fragmentation:redis内存碎片率
  1. LazyFree机制,延迟删除大key,降低删除操作对系统资源的占用影响
    redis是单线程程序,当运行一个耗时较大的请求时,会导致所有请求排队等待,在请求处理完成之前,redis不能响应其他请求。
    redis删除大的集合键就是一种比较耗时的操作。
    主动删除:
    a) unlink主动删除,若集合键的元素个数大于64个,会开启子线程进行异步删除
    b) flushdb可添加async异步清理选项,redis清理整个实例都是异步的
    被动删除:
    被动删除的四种场景,每种场景都有一个配置参数
    lazyfree-lazy-eviction no //当内存使用达到maxmeory,设置淘汰策略时,是否采用lazy free机制
    lazyfree-lazy-expire no //针对设置过期时间的键,被redis删除时是否采用lazy free机制
    lazyfree-lazy-server-del no //针对有些带有隐藏del操作,是否采用lazy free机制
    slave-lazy-flush no //针对slave进行全量同步时,加载RDB文件前,会运行fushall来清理数据 是否采用lazyfree

  2. Psync2
    升级psync1解决副本重启,全量同步问题,减少同步数据和时间

你可能感兴趣的:(消息中间件)