Redis相关面试题

Redis


设计

单线程

  1. 为什么能用单线程

    基于reactor模式的I/O多路复用技术,通过socket对接多个客户端,使用IO多路复用程序,将请求全部放到一个队列里,使用文件时间分派器,交给不同的处理器进行处理。

  2. 多线程的好处

    避免线程切换带来的开销,可维护性高。

  3. 多线程的弊端

    IO操作占用大部分CPU时间,举例:大键值对删除会阻塞

多线程

  1. 处理方式

    1. 多线程只用来处理网络数据的读写和协议解析,命令执行仍然使用单线程。

    2. 默认不开启。

新版本6.0特性

  1. 多线程更完善

  2. 支持ACL权限控制列表

  3. 支持客户端缓存

基本用法


一、redis的基本概念及使用

基本数据类型
  1. String

    1. 描述:最常用的类型,一般存数据就是用这个,为key-value格式

    2. 用途:

      1. 分布式锁

        一般是使用setnx,如果设置成功,返回1,否则返回0

      2. 共享session

      3. 计数器

        通过incr命令加1减1,比如微博评论数,点赞数,分享数,销售量等等

    3. 命令:get/set/del/incr/decr/incrby/decrby/exist/mget/mset/expire

  2. hash

    1. 描述:里边有两个键,一个是redis的键,一个是hash的键,格式类似于 key1 key2 value

    2. 用途:

      1. 管理bean信息,存一个对象,对象名叫做key1,key2就存属性名,value存值
    3. 命令:hset/hget/hgetall/hdel,注,这种hset,hget,hdel需要指定两个key的名字,如果想要删除整个哈希,直接调用del即可

  3. list

    1. 描述:类似于LinkList,可以了理解为String类型的双向链表,有序,可重复,插入极快,获取速度慢。

    2. 用途:

      1. 分布式栈:先进后出这种,

      2. 分布式队列/阻塞队列(消息队列,通过命令brpop这种实现,brpop是阻塞式弹出数据,可以设置个等待时间,但最好使用mq)

      3. 可用于存储一些热点数据列表,例如粉丝列表,文章评论列表,消息排行等

      4. 有限集合,可以用于定时计算的排行榜,如果是实时计算,最好使用zset这种类型

    3. 命令:通过 lpop,lpush,rpop,rpush 这几种命令的组合,可以实现栈,队列这种功能。 lrange,ltrim,lindex,brpop这种阻塞的

  4. set

    1. 描述:元素不重复,底层通过hashtable实现,添加删除查找的时间复杂度都是o(1),hashtable会随着元素的增多减少,动态调整hashtable的大小,但在调整的时候,会出现进程阻塞的情况。可以进行交集,并集,差集操作。

    2. 用途:

      1. 计算两个集合中的共同好友,共同喜好,全部喜好,独有的喜好等等,适合集合操作。

      2. 黑名单,白名单,判断是否在其中

    3. 命令:sadd, smember,sismember,scard,spop,srandmember(随机展示)

    4. 注:如果是集群,操作的多个key,必须在同一个slot中

  5. zset

    1. 描述:key score value 这种结构的

    2. 用途:

      实时排行榜

    3. 命令:zadd/zrange/zscore/zrem

key的优化:
  1. 避免使用大key

  2. 设置合理过期时间

  3. 选择合适的类型

  4. 避免使用del命令,除非String这个类型,否则,最好使用渐进命令删除

key的过期删除策略
  1. 定时删除:每隔多长时间删除一次

  2. 惰性删除:遇到这个键再判断是否到期

  3. 定期删除:每秒扫10次,一次扫20个Key,如果国企比例超过25%,继续扫描别的key

Redis满了,内存淘汰机制

如果redis达到上限(maxmemory参数配置的上限,并非真正满了),会有几种算法进行key的删除。

  1. volatile-lru/lfu/ttl/random:在即将过期的key中选择

  2. allkeys-lru/lfu/random no-enviction:在所有的Key中进行选择

二、redis用作缓存

只读缓存策略

如果要使用缓存,先读redis,之后再读数据库,如果redis中有,就从redis中拿,如果redis中没有,查数据库,同时将数据保存到redis中。数据发生了更新,就把redis中的数据删掉。

读写策略策略

读写策略有两种,一种是同步写回,一种是异步写回,同步写回需要往redis中写过之后,将数据也同步到数据库中,这样效率比较慢。异步写回的话,在redis中删改之后,使用异步线程或者消息队列对数据库再进行修改。但是有数据不同步的风险。

热key问题
  1. 描述:如果突然有几十万以上的请求访问redis上的某个key,这个key就叫做热key,容易造成网卡崩了,redis服务器崩了,继而引起Mysql也崩了,俗称缓存击穿。

  2. 发现热key:

    1. 凭借开发经验判断

    2. 使用aop等方式进行统计

    3. 在代理层进行统计

    4. 使用redis本身的命令进行统计

    5. 使用第三方工具进行统计

    6. 使用大数据的方式,利用flink搭建流式系统,抓包监听redis端口数据,将数据放到kafka中,消费数据,拿到热key

  3. 解决方案

    1. 使用JVM维护的二级缓存,提前准备个ehcache或者hashmap,将热key放到里边,如果请求过来,命中了热key,就将当前请求转发到不同的redis服务器上。

    2. 集群备份热Key,先将热key加上随机数,放到redis集群中,之后请求过来,热key使用相同的算法,也加上随机数,这样就能指定访问某个redis节点了。

保持缓存一致性
  1. 缓存弊端:

    1. 缓存不可信:存的数据不见得是最新的,数据不一定是同步的;

    2. 缓存不可靠:缓存服务不一定随时保持可用的状态。

  2. 缓存一致性的处理策略

    1. cacheAside策略,即只读模式。读:redis有就从redis取,没有的话就从数据库中取,然后再加载到缓存中。更新:先更新数据库,然后删除缓存。核心就是,一切以后端数据库为准。

      注:cacheAside只读策略,在更新的时候会有三种方案:

      1. 双写模式:先更新缓存再更新数据库,或者先更新数据库再更新缓存。
      2. 失效模式:先淘汰缓存再更新数据库,或者先更新数据库再淘汰缓存。
      3. 使用第三方框架canal操作数据库日志。

      双写模式,在更新缓存和数据库的时候,数据处理起来比较复杂,所以一般采用失效模式。

      比较推荐的是,先更新数据库,再删除缓存,但是这种方案有一定的风险性,即,如果更新了数据库,但是该删除缓存的时候没有删掉,还是会造成数据不一致。

      为了解决这种问题,引入了一个方案,延迟双删,先删除缓存,再读数据库,等待一段时间,再删一次缓存。主要是考虑到,第一次删除缓存,数据库写入操作还没有完成的时候,另一个线程读取了数据库,同时把数据拉到缓存中,现在缓存中又有旧数据了。

      上边说的延迟双删其实还是有点问题,第二次删除缓存的时候,如果没有删掉,那么redis中的数据还是旧的,所以就引入了binlog操作,直接操作数据库。使用canal框架,订阅数据库的Binlog文件,将订阅后的数据,压入消息队列,当数据库数据发生变动之后,读取消息,对redis执行删除操作。

    2. read/write through策略,应用层只需要操作缓存,剩下的交给缓存层去实现。应用层读写只需要从缓存读,写数据只需要写到缓存中。

    3. write back策略,

      • 读:从redis中读,redis没有就从数据库中读,然后给到redis一份。

      • 写:写到redis中,当redis满了,一次将所有数据写到后台数据库中。这种设计方式存在问题,当redis还没满,系统挂了,那么redis中的数据就会丢失。

实现缓存一致性可能会遇到的问题
  1. 缓存穿透

    1. 描述:非正常的url查询数据,先查redis查不到,再查数据库也查不到

    2. 可能造成的问题:少了还好说,如果这种非正常请求多了,会造成计算资源的极大浪费,如果再有黑客弄清楚你的序列号生成规律,能把redis干崩掉。

    3. 解决方案:

      1. 将null值也放到redis中。遇到查询redis和数据库都查不到的数据,直接返回空,存到redis里边,下次就直接从缓存中拿到null;

      2. 白名单方式:利用布隆过滤器,或者手动维护一个bitmaps,只允许查在范围内的,超过这个范围直接拒掉。

      3. 运维方式:和运维进行配合,超过访问次数的ip或者什么直接进黑名单

      4. 用户鉴权,防止身份不明的请求

      5. 热点数据进行限流

  2. 缓存击穿

    1. 描述:针对某个热点key进行查询,该key承受着高并发的请求,当key失效的瞬间,所有流量全部进到数据库,数据库崩了。

    2. 解决方案:

      1. 提前延时:热点数据进行预热,提前存到redis中,同时对这些key的过期时间进行延迟

      2. 动态延时:通过业务设计,将这些热点key的过期时间动态延长

      3. 禁止高并发查询:设置分布式锁,查询热key的时候,先看数据库中有没有,如果没有,给查询数据库的语句上锁,此时不允许别的线程读取数据库,等到从数据库提取到redis之后,解锁,保证redis中重新有了这个key的缓存。

  3. 缓存雪崩

    1. 描述:大量数据的key同时到期,查不同的key,导致数据库流量也会激增,击垮。

    2. 解决方案:

      • 多级缓存架构,nginx缓存+redis缓存+jvm中的二级缓存(ehcache实现)

      • 使用锁或者队列,防止高并发同时查数据库

      • 设置提前量去检查,如果即将过期10分钟内进行了访问,就延长时间

      • 将缓存过期的时间分开,避免添加key的时候设置同一个过期时间

三、redis用作分布式锁

分布式锁简介

目前主流的分布式锁,有几种解决方案

  1. 基于数据库乐观锁的实现

  2. 基于缓存redis等的实现

  3. 基于Zookeeper的实现

Redis分布式锁需要满足的条件
  1. 互斥性:同一时刻,只有一个redis客户端可以持有锁

  2. 安全性:当竞争者在持有锁的时候崩溃,未能主动解锁,需要保证其持有的锁能够被正常释放,后续的竞争者也能加锁成功。

  3. 可靠性:只要大部分redis节点正常运行,那么就必须保证redis客户端能够加锁解锁成功。

  4. 对称性:加锁解锁必须是同一个客户端,A客户端不能把B客户端的锁给释放了。

单机redis模式下,分布式锁的具体实现
  1. 描述:能够实现分布式锁的最主要原因就是setnx命令,删除锁使用的是del命令。

  2. 可能会遇到的问题:

    1. 手动释放锁时del命令执行失败(非原子性)

      1. 解决方案:

        • 使用LUA脚本进行控制

          if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

          即,如果程序业务执行完毕,需要手动释放锁时,使用redisTemplate,excute这个script就行了,在方法中传入键,和值。注:键作为业务名,值用来区分到底是业务中的哪个锁,也可以只用业务名。

    2. 异常原因导致锁未手动释放:

      1. 解决方案:超时机制,设置超时时间

        由于上述分布式锁需要满足的条件,安全性的限制,需要设置一个超时时间,

        • setnx + expire

          一般是使用expire设置超时时间,但是setnx与expire是两个不同的命令,如果expire命令没执行成功,那么会出现被锁定的key永不失效,有可能会导致redis的内存空间不足。

        • set

          命令set [key] [value] nx ex [seconds],这个second一般设置为平均执行时间的35倍,如果被锁住的程序执行时间为200ms,一般设置为3*2005*200ms,超过这个时间,自动释放锁。

    3. 超时释放,释放了别人的锁:

      1. 描述:如果不设置身份标记,假如有这么一个A程序,执行完主逻辑之后会执行解锁的步骤,A卡死了,锁自动超时释放,此时竞争者B进来,执行B的逻辑,A又恢复了,接着往下执行,执行完A的主逻辑,A释放了锁,此时释放的就是B的锁,而B还在执行任务。

      2. 解决方案:给锁添加身份标记

        • 可以在某个业务里进行对比,key设置为一个定值,比如说“lock”,value设置为一个具体的内容,如果通过key拿到了存在redis中的value,跟之前设置的值进行对比,如果相同说明是同一个锁。如果不同,就不进行解锁。

        • 直接通过业务进行区分,直接将key设置为某个业务id,加锁就直接在redis中设置这个key,这个业务的别的进程进来,先判断,判断不通过了就等待。

        注:两者也可以都用,在同一个业务里确认是不是同一把锁。

    4. 线程业务未执行完,锁已经到期自动解锁了

      1. 解决方案:

        • 使用redission框架启动看门狗线程,看门狗每隔十秒检查当前线程是否持有锁,如果仍然持有锁,就一直续期,当然这个续期是有时间限制的,防止卡死的线程一直占用这个锁,可以进行设置锁的续期机制。而且这个redission实现了可重入锁,每一个使用这种锁的,会在锁的次数上加1,直到锁降为0才会解锁。使用的是hash这个数据结构,次数是写到了value中,如果hash不存在,或者hash中的key也不存在,则返回nil,否则就hincrby1或者hdecrby1。
集群redis,分布式锁的实现方案
  1. 问题描述:线程1在redis的master节点拿到了锁,master还没有同步给slave,master就挂了,slave升级为master,线程2此时就能获取到锁了

  2. 解决方案:RedLock机制

    1. 方案描述:启动多台不适用任何架构的redis实例,不采用主从,不采用哨兵集群等机制,完全是为了容错。当有超过一半的节点都能获得锁,且在锁的自动失效时间范围内,才算获取锁成功。redission这个框架已经实现了分布式redis的锁的redLock,就不用考虑这个东西了。

四、Redis实现session共享


高可用

高可用实现方式速览

为了维护redis的高可用,有几种不同的实现方式

  1. 持久化

  2. 主从架构

  3. 哨兵机制

  4. 集群架构

Redis 持久化

持久化分为两种:分别是

  1. rdb

  2. aof

RDB

rdb是保存所有的key-value键值对,保存的是数据。

RDB快照有两种方式,

  1. SAVE

  2. BGSAVE

    1. BGSAVE是redis自动触发的

      原理:redis会fock出来一个线程(避免主进程被阻塞),异步生成rdb文件,拷贝原来的rdb文件到新的rdb文件里边,之后将原来的rdb文件删掉。

      注意点:子进程和主进程共享同一片内存区域,如果,要写入到新的rdb文件的数据集,正在被主进程正在修改,会使用一种叫“写时复制”的方案进行处理。即:对那一块物理内存进行复制,主进程对复制出的内容进行修改,之前的内容仍然由子进程写入到rdb文件中。

    注:rdb方案可以使用lzf算法进行压缩,所以rdb持久化方式的占用空间比aof更小。

AOF

aof记录的是所有的命令,一般来说根据记录的快慢,分为三种方式,即下面的刷盘策略:

刷盘策略
  1. 同步写入,redis接收到什么内容,就往硬盘中刷入什么内容

  2. 每秒写入,一秒写一次

  3. 依赖操作系统写入,完全交给操作系统了

子进程操作

因为AOF这种追加方案,没有压缩机制,会导致文件越来越大,所以会重启一个进程,对AOF文件整理,将之前的命令全部删掉,把数据集整理成AOF文件命令,命令是bgrewriteaof,

具体的操作步骤是,子进程读取数据库中的所有的键值对,将每个键值对都用一条命令写入到新的AOF文件中,全部记录完成之后,用新的AOF文件替换旧的。

注:这个进程用的内存跟上边RDB子进程一样,都是使用的主进程中的物理内存。同样,如果两个进程同时对共享内存发起写操作,也会进行缺页中断,写时复制,将那一块内存进行复制。

主从架构

为了解决单机redis容易发生单点故障的问题,及redis内存容量不够的问题,推出主从架构。

  1. 主从架构的实现原理

    1. 建立连接阶段:从机连接主机,从机从主机中拉取数据

      1. 从机从redis的配置文件中获取主机的ip及端口,执行replicaof命令发起连接请求

      2. master主机返回相应信息

      3. slave保存主机的ip及端口号,根据地址建立socket连接

      4. 从机依据ping-pong机制,持续向主机发送ping命令,master返回pong回应,若返回不正常则说明超时

      5. 建立连接后,进行权限校验,如果master中设置了密码,需要对slave进行权限校验

      6. 验证之后,从机向主机发送自己的监听端口,主机进行保存

    2. 数据同步阶段

      • 全量复制

        1. 建立连接

          1. 主从服务器建立连接,slave执行replicaof命令,向主机发送psync命令,携带?和-1,表示此时还不知道主机的runId,且为第一次连接,

          2. 主库收到psync请求,返回fullresync相应,携带两个参数,主库的runId和主库目前的复制进度,offset.

          3. 从库保存主库的核心信息,同时保存runId和offset

        2. 将全部数据同步给slave

          1. 主从建立连接的时候,主机可能会同时进行bgsave操作,将文件持久化的rdb操作,同时将数据存到repl_backlog_buf复制积压缓冲区

          2. 将数据全量发送给slave

          3. slave收到rdb文件后,清空自己的数据库,加载rdb文件

        3. 将发送rdb文件期间的命令从复制积压缓冲区拿出来,发送给从库,从库再进行执行。

      • 增量复制(闪断情况用的多)

        1. 网络抖动,主从断开连接,恢复之后重新连接,从机发送psync,将自己的runId和slave_repl_offset传送给master主机

        2. 主机判断,如果runId与之前匹配,且slave_repl_offset偏移量在积压缓冲区范围内,但从机的偏移量与主机记录的master_repl_offset偏移量不一致,就返回continue命令,表示可以增量复制。如果runid不匹配,或者偏移量不在积压缓冲区(说明积压缓冲区已经被覆盖了),那么就进行全量复制。

    3. 命令传播阶段

      第一次同步之后,主从之间维护一个长连接,不断将新增的写命令传递给从服务器。

哨兵机制

流程步骤
  1. 确认下线状态

    1. 每个sentinel会以1hz的频率,向其本身知道的master,slave节点,发送ping命令,探测是否存活;
    2. 如果master实例超过了配置选项指定的时间,就默认该实例已经主观下线了;
    3. 如果master被标记主观下线,所有监视这个master的sentinel,都要进行确认是否对于自己的sentinel,也是主观下线,频率也是1赫兹,如果Ping通了,那取消主观下线状态
    4. 如果足够数量的sentinel都确认这个master主观下线了,就确认客观下线,如果没有足够数量的确认,就取消确认客观下线,仍然保持主观下线状态
  2. 确认拓扑结构

    1. 每个sentinel会以10s一次的频率,向所有的redis节点发送info命令,获取当前的redis拓扑状态,这个是无论下没下线,都要进行发送的
    2. 如果确认了master客观下线了,sentinel就会向该master的所有slave发送info命令,改成1s一次,确认这个拓扑结构
    3. 哨兵根据一定的规则,重新推选出一个哨兵leader,负责故障转移
    4. 这个哨兵leader,根据规则确定Master,构建全新的主从结构
哨兵机制的架构分析
  • 基础就是“一主二从三哨兵”:一个主节点,两个slave,这三个节点都有自己的哨兵进行监控
哨兵机制的宕机情况及对应解决方案
  1. 从机宕机:恢复后自动加入到之前的主从架构中了,差的那部分数据采用增量复制即可。

  2. 主机宕机:主机宕机了,根据选举模式,选出来三个哨兵哪个哨兵进行善后工作,选出来之后,这个哨兵进行主导,确认新的master节点,确认新的节点后,将之前的主从的拓扑结构断开,重新生成新的主从拓扑,将之前的主节点设置成从节点,等这个节点上线之后,执行slaveof命令,将其设置成slave。

哨兵的异常通知方式

当某个哨兵发现主节点下线了,会发送一个告警,但这个告警并不是给别的哨兵发的,而是有一个sentinel_hello_master频道,哨兵用的是发布/订阅模式接受别的哨兵的信息。这个哨兵发送的内容,也会被自己接到,有个run ID进行判断,如果是自己发的,就忽略。

哨兵选举Leader的方案

选举这个哨兵Leader的时候,一般是根据redis-sentinel的配置文件制定的哨兵优先级确定的,哨兵进行投票,但是这个投票是基于内部通信机制的,大多数情况下只要有候选者,都会超过半数的sentinel同意这个候选者作为Leader,只有在某种异常情况下,网络故障,哨兵配置出错,数据不一致的时候才会出现大多数哨兵不同意该候选人作为leader。

Redis的集群架构

redis5之后,可直接使用redis-cli命令行进行配置

  1. 引入原因:为了保证redis的高可用,在持久化机制,主从架构,哨兵机制仍然无法保证高可用的情况下,推出集群架构。

  2. 原理:

    1. 划分槽位:采用集群架构,会划分槽位,无论多少台redis服务器,一共只会划分16384个槽位,有多少个master节点,这些槽位就会被平均分成多少份。利用一致性哈希算法,将数据均分成不同的份数。

    2. 数据入槽:客户端存储key的时候,利用crc16算法,同时对16384进行取余操作,将key-value放到对应的hash槽中。也就是放到不同的redis服务器中。

  3. 弊端:

    1. 集群架构不允许针对在不同槽中的多个key进行批量操作,因为太影响性能,所以禁掉了。

    2. 不支持lua脚本

    3. 硬件投入高。

  4. 架构:多主多从。

  5. 消息通知算法:主节点之间通过gossip相互串通,当主节点挂了,slave节点顶上。gossip算法:某些节点随机将数据传递给周围节点,周围节点同样操作,最终达到一致。

  6. 客户端使用:客户端进行访问的时候,不指定连接的哪个master节点。根据缓存数据,如果key不在某个slot上,那么集群就会计算一份新的缓存进行重定位。

  7. 扩容:

    • 可以无限扩展,但是最好服务器不要超过1000个

    • 添加服务器,只需要将之前的节点的部分slot,转到新节点就行了,不需要停机操作。

  8. 集群容错:

    1. 节点失效:半数以上的master与该节点进行通信,超过配置设置的时间,那么就称为节点失效。

    2. 集群失效:如果某个节点的master失效,又没有slave,那么整个集群就失效,或者超过半数的master失效,也算集群失效。

  9. 集群脑裂

    • 定义:某个节点的主节点因为网络或者别的什么原因导致失效,该redis主从架构的从节点向集群中的别的Master发送选举广播且被选为主节点之后,该redis出现两个主节点,网络恢复后,需要将其中的一个主节点变为从,所以会损失一部分数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值