Java面试-Redis-基础


  • 单线程的Redis为什么快
    • 纯内存操作
    • 单线程操作,避免了频繁的上下文切换
    • 合理高效的数据结构
    • 采用了非阻塞I/O多路复用机制
  • 持久化机制
    • RDB持久化:快照形式是直接把内存中的数据保存到磁盘文件中,定时保存
      • save触发方式:执行该命令时,Redis服务器处于阻塞状态,不能处理其他命令,直到RDB过程完成为止
      • bgsave触发方式:执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求
      • 优点:全量备份,非常适合用于备份和灾难恢复
      • 缺点:RDB方式数据没办法做到实时持久化/秒级持久化。因为每次执行bgsave都要执行fork操作创建子进程,频繁执行成本高
    • AOF持久化:把所有对Redis服务器进行修改的命令都通过write函数追加到aof文件的末尾。AOF持久化具有实时性,目前已经是Redis持久化的主流方式
      • 优点:数据安全性更高;aof的默认策略是每秒钟执行一次fsync,就算发生故障,最多也就丢失一秒钟的数据
      • 缺点:对于相同数据集而言,AOF文件通常大于RDB文件,在恢复数据集时速度比RDB慢
    • Redis默认是RDB持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全量同步结束后,进行增量同步(AOF)
  • 五大数据结构
    • String:简单动态字符串(Simple Dynamic String),简称SDS;Redis的字符串也会遵守C语言的字符串的实现规则,即最后一个字符为空字符。然而这个空字符不会被计算在len里头
      • SDS动态扩展,Redis会做三个操作:
        1. 计算出大小是否足够
        2. 开辟空间至满足所需大小
        3. 开辟与已使用大小len相同长度的空闲free空间(如果len < 1M);开辟1M长度的空闲free空间(如果len >= 1M)
      • 性能优势:快速获取字符串长度;避免缓冲区溢出;降低空间分配次数提升内存使用效率
        • SDS里存了已使用字符长度len,所以当想获取字符串长度时直接返回len即可,时间复杂度为O(1)
        • 由于每次追加字符串时都会检查空间是否够用,所以不会存在缓冲区溢出问题
        • 有了这个预分配策略之后会减少内存分配次数,因为分配之前会检查已有的free空间是否够,如果够则不开辟了
      • 操作示例:set key value get key del key mset key1 value key2 value2 mget key1 key2 strlen key append key value
    • hash:底层使用哈希表结构实现数据存储,每个hash可以存储232-1个键值对
      • 操作示例:hset key field value hget key field hmset key field1 value1 field2 value2 hlen key
      • 底层实现是双哈希表结构(ht[2])。ht [0] ht[1] 主要是用在 rehash过程才使用的
        • 初始k/v保存在ht[0]中,当冲突严重时,将ht[1]中桶的大小设置为ht[0]的两倍,并逐步将ht[0]中的元素迁移到ht[1]。等到所有元素都迁移完成后,再将ht[0]与ht[1]交换地址
      • 对哈希表的rehash是分多次的,渐进式的:当hash表表现太多碰撞的话,查找会有 O(1) 变成 O(maxLen),redis为了性能,会在碰撞过多的情况下rehash
      • rehash触发条件
        • 当新插入一个键值对的话,根据used/size得到一个比例(used是hash表节点数;size是数组的大小)
        • 自然 rehash:满足 used/size >= 1 && dict_can_resize 条件触发的
        • 强制 rehash:满足 used/size >= dict_force_resize_retio (默认为5) 条件触发的
      • rehash过程
        • 给ht[1]分配至少2倍于ht[0]的空间
        • 将ht[0]数据迁移到ht[1]
        • 清空ht[0],将ht[0] 指针指向ht[1], ht[1] 指针指向ht[0]
      • rehash过程中ht[0]只减不增
      • 当多次删除操作时,如果used/size的比例小于阈值(现在是10%)则会触发缩减空间rehash,过程和增加类似
    • List:底层使用双向链表存储结构实现
      • 操作示例:lpush key value1 rpush key value1
    • Set
      • 操作实例:sadd key value
    • Sorted_set:ZSet采用跳跃表实现
      • Redis的索引被提取为多层:所有的元素都会在L0层的链表中,根据分数进行排序,同时会有一部分节点有机会被抽取到L1层中,作为一个稀疏索引,同样L1层中的索引也有一定机会被抽取到L2层中,组成一个更稀疏的索引列表
      • 操作示例:zadd key score1 member1
  • Redis集群的数据分配:使用虚拟槽分区概念
    • Redis内部内置了序号 0-16383 个槽位,每个槽位可以用来存储一个数据集合,将这些槽位按顺序分配到集群中的各个节点。每次新的数据到来,会通过哈希函数 CRC16(key) 算出将要存储的槽位下标,然后通过该下标找到前面分配的Redis节点,最后将数据存储到该节点中
  • Redis常见问题
    • Redis的集群模式有哪些
      • 主从模式(Master/Slave):主机会自动将数据同步到从机
        • 优点:可支持读写分离,降低Master的压力;Master节点挂了以后,不影响slave节点的读(也有效地做了数据备份,防止特殊情况master无法恢复导致数据丢失);Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力
        • 缺点:Master挂了后不支持写操作且Slave不会重新选举出Master;Master宕机,宕机前有部分数据未能及时同步到从机,引入数据不一致的问题
      • 哨兵模式(Sentinel):监控Master(主服务器)和Slave(从服务器)是否正常运行,Master(主服务器)出现故障时自动将Slave(从服务器)转换为Master(主服务器)
        • 优点:主从可以自动切换,可用性更高
        • 缺点:难以扩容
      • Redis cluster集群模式:可以做到在多台机器上,部署多个Master(实例),每个Master存储一部分数据,同时每个Master node可以挂载多个Slave node(从实例),如果Master挂掉,就会自动将某个Slave切换成Master
        • 如果Master挂掉,怎么从多个Slave选出新的Master
          • 选新的Master过程基于Raft协议选举方式来实现的,当从节点发现自己的主节点变成已下线状态时,从节点会广播一条消息,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条消息,表示这个主节点支持从节点成为新的主节点如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于集群N/2+1张支持票时,这个从节点就成为新的主节点如果在一个配置纪元没有任何一个从节点能够收集到足够的支持票数,那么集群进入一个新的配置纪元,并再次进行选主,直到选出新的主节点为止,新的主节点出现后会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己,新的主节点对集群进行广播PONG消息,告知其他节点已经成为新的主节点,新的主节点开始接收和处理槽相关的请求
        • 每个Master存储一部分的数据,Redis数据怎么分到不同的节点
          • Redis内部内置了序号 0-16383 个槽位,每个槽位可以用来存储一个数据集合,将这些槽位按顺序分配到集群中的各个节点。每次新的数据到来,会通过哈希函数 CRC16(key) 算出将要存储的槽位下标,然后通过该下标找到前面分配的Redis节点,最后将数据存储到该节点中
        • 为啥Redis不用一致性Hash算法
          • 一致性哈希也就是将存储节点排列在首尾相接的Hash 环上,每个 key 在计算 Hash后会顺时针找到存储节点存放,如找到的节点不可用那就下再顺时针找下一个节点。而当有节点加入或退出时,仅影响该节点在 Hash 环上顺时针相邻的后续节点
          • 但是在实际运用中节点一般较少,一旦出现删除或增加某个节点对哈希环中的数据映射影响非常大,且无法保证负载均衡。除非增加一倍或减少一半的节点
        • 新增一个节点,Redis如何保证在Slot迁移过程中的正常写入与修改
          • 假设从 Node A迁移槽 1 位到 Node B
          • 迁移中会将 Node A 的这槽 1 状态标记为 Migrating(迁移)
          • Node B 的状态为 Importing (输入口)
          • 这时候槽的映射关系不做修改,迁移过程中如果出现查询槽 1 的数据,客户端会先去 Node A 上去查询数据,如果对应的数据还在本机上,那么直接返回,如果数据不在 Node A上 则会返回 ASK 转向,如果客户端接收到 ASK 转向, 那么将命令请求的发送对象调整为转向所指定的节点 Node B (先发送一个 ASKING 命令,然后再发送真正的命令请求)
          • 请求完成后不会更新客户端所记录的槽至节点的映射:槽 1 应该仍然映射到 Node A , 而不是 Node B
          • 一旦 Node A 针对槽 1 的迁移工作完成, Node A 在再次收到针对槽 1 的命令请求时,就会向客户端返回 MOVED 转向, 将关于槽 1 的命令请求长期地转向到 Node B
        • Redis如何保证主从数据一致(Master 可以执行读写的命令,slave 只能执行读的命令)
          1. 从节点连接主节点并发送sync命令
          2. 主节点收到sync命令,开始执行bgsave操作,生成rdb文件,将期间的写命令缓存到内存中
          3. 当bgsave操作完成后,主节点将rdb文件和缓存的写命令发送给从节点
          4. 从节点收到rdb文件和写命令后,先载入rdb文件,再执行写命令,以达到与主节点一致的状态
          5. 之后主节点会实时地将新的写命令发送给从节点,以维持主从数据的同步
    • Redis有哪几种数据淘汰策略
      • noeviction(默认策略): 当内存不足以容纳新写入数据时,新写入操作会报错
      • allkeys-lru: 从所有key中选择最近最少使用的数据淘汰
      • volatile-lru: 从设置了过期时间的key中选择最近最少使用的数据淘汰
      • allkeys-random: 从所有key中随机选择数据淘汰
      • volatile-random: 从设置了过期时间的key中随机选择数据淘汰
    • Redis有哪几种过期键删除策略
      • 定期删除:Redis默认每隔100ms会随机抽取一些设置了过期时间的key并检查是否过期,如果过期则删除
      • 惰性删除:当客户端尝读取某个key时,Redis才会检查它是否过期,如果过期则删除
    • Redis怎么取出数据比较大的key
      • bigkeys:String 是使用 STRLEN 方法来计算,List 是 llen 来计算
    • String的底层数据结构
      • 底层数据结构是简单动态字符串SDS(simple dynamic string),它可以在O(1)时间内完成字符串插入和删除等操作
      • 可自动扩展缓冲区大小,性能好、安全性高
  • 缓存穿透:指一个请求的数据在缓存中不存在,导致该请求穿透缓存直接访问数据库
    • 解决方案:缓存空对象;布隆过滤器
  • 缓存雪崩:指大量的缓存在同一时间过期失效,导致大量请求同时访问数据库
    • 解决方案:原有的失效时间基础上增加一个随机值;用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写
  • 缓存击穿:指一个缓存在某个时间点过期失效,恰好在这个时刻有大量并发请求访问该数据,导致缓存无法承受高并发的压力,从而导致请求访问数据库
    • 场景:当前key是一个热点key(例如一个秒杀活动),并发量非常大;重建缓存不能在短时间完成,可能是一个复杂计算
    • 解决方案:分布式互斥锁
      • Redis分布式锁:先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放
  • 高并发场景下,如何保证缓存与数据库一致性
    • 数据不一致:因为写和读是并发的,没法保证顺序,如果删了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,再删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。 如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致
    • 解决方案:
      • 写请求串行化(分布式锁)
        • 写请求更新之前先获取分布式锁,获得之后才能去数据库更新这个数据,获取不到就进行等待,超时后就返回更新失败
        • 更新完之后去刷新缓存,如果刷新失败,放到内存队列中进行重试(重试时取数据库最新数据更新缓存)
  • Redis实现消息队列的两种方式
    • List数据结构:key用来表示队列名称,发消息采用rpush,lpush实现,消费消息采用rpop或lpop实现
    • 发布订阅模式:通过publish命令发布消息,通过subscribe命令订阅消息
  • Redis如何实现延时队列
    • 可以使用zset,将消息内容作为value,消息执行时间作为score,添加到zset中。消费者通过轮询zset获取当前时间之前所有的消息进行处理,如果处理失败,可以重新添加到zset中,重新处理
  • Redis事务(只保证了一致性和隔离性,不满足原子性和持久性):虽然redis是单线程,但是可以同时有多个客户端访问,客户端之间存在竞争
    • 隔离性:开启事务之后,会执行完当前连接的所有命令直到遇到exec命令,才处理其他连接的命令
    • 事务的三个阶段:事务开始:MULTI;命令入队:QUEUED;事务执行:EXEC;
    • 事务提供了一种将多个命令打包,然后一次性、有序地执行的机制(多个命令会被入队到事务队列中,然后按先进先出(FIFO)的顺序执行)

文章作者: 钱不寒
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 钱不寒 !
  目录