自学内容网 自学内容网

redis精选面试题

Redis的单线程指什么

Redis 单线程主要指的是网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求的时候包括获取(Socket),解析,执行,内容返回(Socket写)等由一个顺序串行的主线程处理,这即为单线程

但Redis的其它功能,比如持久化,异步删除,集群数据同步等等,都是由额外的线程执行的,是多线程

即Redis工作线程是单线程的,但是,整个Redis来说是多线程的

Redis单线程为什么很快

1、基于内存操作:Redis的所有数据都存在内存中,因此所有的运算都是内存级别

2、数据结构简单:Redis的数据结构简单,查找和操作时间大部分时间复杂度是O(1)

3、多路复用和非阻塞:Redis使用多路复用功能来监听多个Socket链接客服端,这样可以一个线程处理多个请求,减少线程切换带来的开销,同时也避免了IO阻塞操作

4、避免上下文切换:因为是单线程模型,因此避免了多线程切换和多线程竞争,减少时间和性能的消耗

Redis系列第一讲:Redis是单线程吗 - 掘金 (juejin.cn)

谈谈redis的对象机制(redisObject)

Redis的5种基础数据类型,在底层是采用对象机制实现的。

Redis的每种对象其实都由对象结构(redisObject) 与 对应编码的数据结构组合而成,而每种对象类型对应若干编码方式,不同的编码方式所对应的底层数据结构是不同的。

redisObject 是 Redis 类型系统的核心, 数据库中的每个键、值, 以及 Redis 本身处理的参数, 都表示为这种数据类型。

在这里插入图片描述

Redis相关知识----对象机制_小舟~的博客-CSDN博客

redis 优化措施

  1. 避免大key的存在

  2. 设置合理的淘汰策略

内存尽可能大; 淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整)

  1. 谨慎fork操作带来的阻塞

控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久

  1. 关闭操作系统的内存大页

应用在申请内存时,linux会给一个完整的内存页大小, 申请的多了, 分配内存也就大了 , 耗时也就长了

对于 Redis 这种对性能和延迟极其敏感的数据库来说,我们希望 Redis 在每次申请内存时,耗时尽量短

  1. 开启AOF后设置 重写时不刷盘
# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作
# 相当于在这期间,临时把 appendfsync 设置为了 none
no-appendfsync-on-rewrite yes

建议使用 固态硬盘, 加快数据落磁盘

  1. 关闭swap 内存

操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。

Redis进阶 - 性能调优:Redis性能调优详解 | Java 全栈知识体系 (pdai.tech)

热key的处理

  1. 利用二级缓存

将热key加载到jvm中, 利用本地缓存抗住

  1. 备份热key

将热key多备份几份,用后缀区分, 这样请求就会分发到不同的key上了

检测热key

  1. 经验预估, 比如秒杀key
  2. 客户端收集并统计
  3. redis 自带的命令 , monitorhotkeys 但都有性能问题

【原创】谈谈redis的热key问题如何解决 - 孤独烟 - 博客园 (cnblogs.com)

缓存穿透

缓存穿透,是指频繁查询一个数据库一定不存在的数据,这种redis一般不会保存,这样redis被穿过了,直接去访问了数据库,这种穿透多了,数据库就压力大。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象

缓存被穿透了,因为总是查询一些在redis中查不到的值

解决方案:

  1. 使用锁,将这次请求锁住,等数据库返回后才释放(分布式环境用分布式锁,单机用普通锁(synchronized等))
  2. 接口限流与熔断、降级
  3. 布隆过滤器(java的guava包中默认误判率3%(最合适)) (将全部"可能"数据存到布隆过滤器中,当redis中找不到时,先经过布隆,如果过滤器说不存在,就不用访问数据库了,直接返回)
  4. 将这种key存到redis,设置较短的过期时间,比如1分钟/5分钟

缓存雪崩

缓存雪崩,是指在某一个时间段,缓存集中过期失效,大批量key失效,命中redis的概率就小了,只能去查数据库了.

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,那么那个时候数据库能顶住压力,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。

解决方案:

  1. 也是像解决缓存穿透一样加锁排队

  2. 建立备份缓存, 采用哨兵或者集群 ,多起几个节点

  3. 给同时添加key时设置不同过期时间

  4. 事前:确保Redis本身的高可用性,数据恢复备份、主从架构+哨兵,Redis cluster,一旦主节点挂了,从节点跟上

  5. 事中:当redis不可用时,少量请求可以走缓存生产服务的本地缓存ehcache获取数据,基于hystrix对商品服务和redis操作做限流保护 配置降级、超时、熔断策略;从而保证发生缓存雪崩时缓存生产服务不会被拖死

  6. 事后:基于redis的数据备份,快速将redis重新跑起来对外提供服务

缓存击穿

缓存击穿,热点key在请求高峰期失效了,瞬间大量请求落到数据库,就像在一个屏障上凿开了一个洞。

解决方案:

  1. 本地缓存 + redis缓存的多级缓存
  2. 也是像解决缓存穿透一样加锁排队
  3. 设置永不过期或者定时更新过期时间

链接:https://baijiahao.baidu.com/s?id=1619572269435584821&wfr=spider&for=pc

https://blog.csdn.net/fanrenxiang/article/details/80542580

[如何解决redis缓存击穿问题_Sebastian Xia的博客-CSDN博客_redis缓存击穿怎么解决

三种部署模式

特别是集群模式和哨兵模式他们的区别, 要深入理解这两种模式

1. 主从复制

复制过程:

  1. 从服务器连接主服务器,发送SYNC(同步)命令;
  2. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  3. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;(从服务器初始化完成)
  7. 主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)

优点:

  1. 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成
  2. Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力。
  3. Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求。
  4. Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据

缺点

  1. Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  2. 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  3. Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

链接:https://www.jianshu.com/p/7967f95655b2

主从复制是基础,后面的哨兵和Cluster都是基于这种方式,但是这种方式太垃圾了,master死了就不能写操作了,所以一般会用后面两种
使用主从复制很简单,在配置文件中指定master/slave,并设置密码(必须使用)

fork耗时严重问题

我们可能会开启后台定时 RDB 和 AOF rewrite 功能。但如果你发现,操作 Redis 延迟变大,都发生在 Redis 后台 RDB 和 AOF rewrite 期间,在这期间有可能导致变慢的情况。

当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。

主进程创建子进程,会调用操作系统提供的 fork 函数

而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。

而且这个 fork 过程会消耗大量的 CPU 资源,在完成 fork 之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求。

如果此时你的 CPU 资源本来就很紧张,那么 fork 的耗时会更长,甚至达到秒级,这会严重影响 Redis 的性能。

也就是说 主从同步和AOF重写操作 时都是用fork函数来同步的

要想避免这种情况,你可以采取以下方案进行优化:

  • 控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
  • 合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite
  • Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久
  • 降低主从库全量同步的概率:适当调大 repl-backlog-size 参数,避免主从全量同步

Redis进阶 - 性能调优:Redis性能调优详解 | Java 全栈知识体系 (pdai.tech)

2.哨兵模式

哨兵的作用就是监控Redis系统的运行状况。它的功能包括以下两个。

  1. 监控主服务器和从服务器是否正常运行。

  2. 主服务器出现故障时自动将从服务器转换为主服务器。

弥补了主从模式下的master死亡的情况

哨兵是集群,有主节点和从节点,也会产生选举(基于Raft算法(但不一样),和kafka一样),会选举哨兵主节点,redis的master,( 每个redis的bin下都有一个哨兵启动文件),在sentinel模式下,客户端就不用直接连接Redis,而是连接sentinel的ip和port

哨兵是一个独立的进程,作为进程,它会独立运行

每个哨兵通过主节点去发现其他从节点和其他哨兵的

在这里插入图片描述

工作方式

  • 每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel实例发送一个 PING 命令
  • 如果一个实例距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被sentinel标记为主观下线
  • 如果一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率(发送info命令)确认master的确进入了主观下线状态
  • 当有足够数量(quorum)的sentinel在30s(down-after-milliseconds)内确认master的确进入了主观下线状态, 则master会被标记为客观下线
  • 在一般情况下, 每个sentinel会以每 10 秒一次的频率向它已知的所有master,slave发送 INFO 命令
  • 当master被sentinel标记为客观下线时,sentinel向下线的master的所有slave发送 INFO 命令的频率会从 10 秒一次改为 1 秒一次
  • 若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除; 若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除

redis自带哨兵,不需要借助第三方,只需要简单配置即可 ,生产环境建议让redis Sentinel部署到不同的物理机上。

原文链接:https://blog.csdn.net/miss1181248983/article/details/90056960

哨兵可以解决大量读的问题,如果有大量写,哨兵也扛不住,这需要Twemproxy,做数据的分片处理,再配合 Lvs 和 Keepalived 解决Twemproxy的单点故障

https://www.jianshu.com/p/84dbb25cc8dc

3. Cluster模式

redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了cluster模式,实现的redis的分布式存储,也就是说每台redis节点上存储不同的内容。(取数据的时候直接找到对应地址去取)

默认情况下,redis集群的读和写都是到master上去执行的,不支持slave节点读和写,跟Redis主从复制下读写分离不一样,因为redis集群的核心的理念,主要是使用slave做数据的热备,以及master故障时的主备切换,实现高可用的。

所以集群模式相比哨兵模式, 为了扩展写能力和存储能力

Redis-Cluster采用无中心结构,它的特点如下

  1. 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
  2. 节点的fail是通过集群中超过半数的节点检测失效时才生效。
  3. 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

工作方式

在redis的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。只有master节点会被分配槽位,slave节点不会分配槽位。

当Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息,并将其缓存在客户端本地。这样当客户端要查找某个 key 时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

客户端会存一份槽位的分布信息 , 如果槽位信息不对(例如槽位发生了迁移),客户端会更新为最新的槽位信息.

也就意味着, 客户端一般能直接命中key,不会发生二次访问

这个客户端是 smart客户端,就是指客户端本地维护一份hashslot => node的映射表缓存,大部分情况下,直接走本地缓存就可以找到hashslot => node,不需要通过节点进行moved重定向,(jedisCluster就集成了它)

Redis集群所有节点之间的通信机制

介绍集群模式时, 可以附带提一下通信机制 (如果面试官深问但你不懂, 可能会减分, 所以慎重)

在Redis集群中,不同的节点之间采用gossip协议进行通信,节点之间通讯的目的是为了维护节点之间的元数据信息。这些元数据就是每个节点包含哪些数据,是否出现故障,通过gossip协议,达到最终数据的一致性。

gossip协议,是基于流行病传播方式的节点或者进程之间信息交换的协议。原理就是在不同的节点间不断地通信交换信息,一段时间后,所有的节点就都有了整个集群的完整信息,并且所有节点的状态都会达成一致。每个节点会携带集群节点总数的1/10(至少3个)的信息,但只要这些节点可以通过网络连通,最终他们的状态就会是一致的。Gossip协议最大的好处在于,即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。

也意味着元数据的更新有延时,可能导致集群中的一些操作会有一些滞后, 同时结点数太多,意味着达到最终一致性的时间也相对变长,因此官方推荐最大节点数为1000左右。

redis cluster架构下的每个redis都要开放两个端口号,比如一个是6379,另一个就是加10000的端口号16379。

  • 6379端口号就是redis服务器入口。
  • 16379端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用的是一种叫gossip 协议的二进制协议

更多知识详见: Redis集群原理详解_张维鹏的博客-CSDN博客_redis集群原理

维护集群的元数据有集中式和 gossip两种方式, 像zookeeper就是集中式

Redis集群搭建及原理,肝了! - 知乎 (zhihu.com)

​ 由于Redis集群的去中心化以及gossip通信机制,Redis集群中的节点只能保证最终一致性。例如当加入新节点时(meet),只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,也就是Gossip协议是最终一致性的协议。

mget性能问题

现象: 使用mget时性能出现明显下降

原因: 要查的key分布在多个节点上,所以需要给多个节点发送请求且为串行, 我们使用了一个mget,实际上访问了很多次

在这里插入图片描述

浅谈Redis集群下mget的性能问题 - SegmentFault 思否

redisson分布式锁

Redisson是一个基于java编程框架netty进行扩展了的redis。 Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid),加锁和解锁都是通过lua脚本来执行

一把分布式锁在redis中是怎么存储的?

在这里插入图片描述

具体值: 这里数据类型是Hash类型,Hash类型相当于我们java的 <keyName,<field,value>> 类型,

这里keyName是指 ‘redisson’,

field值它的组成是:uuid + 当前线程的ID,

value是重入次数

uuid是客户端实例化时就创建好了,它是客户端的标识

这里还有个知识点:

  1. redisson怎么实现重入的?
    答: 通过控制value来控制重入次数, 通过判断key来判断是否抢锁还是重入锁
    重入过程:
    在这里插入图片描述

在持有锁的时间内,业务没有执行完,怎么办?

redisson有一个看门狗机制, 通过一个守护线程去判断当前业务是否执行完成, 若没有完成则会给锁自动续过期时间, 当获取锁时不设置锁时间, 看门狗才会生效, 例如

redissonClient.getLock(""lockName"").lock(); // 看门狗生效, 默认锁定30s, 续锁也是30s
redissonClient.getLock(""lockName"").lock(10, TimeUnit.SECONDS); // 看门狗失效, 锁10秒后释放

集群模式下, 锁只在一个节点上, 若此时挂了, 可能导致锁会被重复拥有, 应该怎么办?

由于节点之间是采用异步通信的方式。如果刚刚在 Master 节点上加了锁,但是数据还没被同步到 Salve。这时 Master 节点挂了,它上面的锁就没了,等新的 Master 出来后(主从模式的手动切换或者哨兵模式的一次 failover 的过程),就可以再次获取同样的锁,出现一把锁被拿到了两次的场景。

redisson有redLock方案,
红锁是多把锁合成一把锁,获得锁时会向全部节点发送lua脚本申请锁,只有获得(n/2+1)个节点的锁时才真正获得锁,(获得锁需要时间,而锁也有过期时间,所以最终时间是过期时间减去获得锁花费的时间)


原文地址:https://blog.csdn.net/Q176782/article/details/144001183

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!