面试-Redis知识点整理


Redis概述

什么是Redis?优缺点?

Redis是Key-Value内存型数据库

优点:

  • 读写性能极高。Redis是内存数据库,相比较于去访问磁盘,访问内存的速度快很多
  • 支持数据持久化
  • 支持事务
  • 支持的数据类型丰富
  • 提供原生集群支持

缺点:

  • 数据库容量受到物理内存的限制。
  • 数据不一致问题

Redis为什么快?(Redis是单线程为什么快?)

  • 基于内存,读写速度快
  • 单线程没有上下文切换带来的消耗
  • 非阻塞IO多路复用模型

Redis为什么用单线程?

  • 不用考虑锁的问题
  • 没有上下文切换带来的消耗
  • 处理逻辑简单
  • CPU不是性能瓶颈

Redis线程模型

Redis线程模型包含4个部分:

  • 多个socket
  • IO多路复用程序
  • 文件事件分派器
  • 文件事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

文件事件处理器使用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理

IO多路复用如何保证读写的顺序?

多个套接字产生的事件,由IO多路复用程序将它们放到一个队列中,然后通过这个队列,有序、同步地传递给文件事件分派器

4.0和6.0引入的多线程

4.0

引入多线程处理异步任务,包括命令如UNLINKFLUSHALL ASYNCFLUSHDB ASYNC

6.0

引入多线程IO,处理网络数据的读写和协议解析,而执行命令依旧是单线程

为什么用Redis做缓存而不是用Map?

用Map实现的缓存,生命周期随JVM的销毁而结束,而且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用Redis的称为分布式缓存,在多实例的情况下,各实例共用一份缓存,缓存具有一致性。缺点是需要保持Redis的高可用性,整个架构比较复杂

Redis和Memcached的区别

  1. Redis支持的数据类型更多
  2. Redis支持数据的持久化,可以将内存中的数据保存到磁盘上,重启的时候可以再次加载,而Memcached把数据全部存放在内存中
  3. Redis原生支持集群
  4. Redis使用单线程的IO多路复用模型,Memcached是多线程的非阻塞IO复用模型

Redis常用的数据结构和使用场景

String

  • 命令:set, get, incr, decr, mget
  • 底层实现:int、embstr、raw(即SDS)
  • 应用场景:常规key-value缓存应用;计数:微博数、粉丝数

List

  • 命令:lpush, lpop, rpush, rpop, lrang
  • 底层实现:ziplist、linkedlist
  • 应用场景:微博的关注列表,粉丝列表,消息列表

Hash

  • 命令:hset, hget, hgetall

  • 底层实现:ziplist、hashtable

  • 应用场景:存储用户信息,商品信息

  • 渐进式rehash的优缺点:

    优点:rehash操作分摊到对字典的增删改查操作上,避免了集中式rehash带来的庞大计算量,避免了Redis阻塞

    缺点:rehash过程中有两张表同时在使用,增加了占用内存

Set

  • 命令:sadd, spop, smembers, scard
  • 底层实现:intset、hashtable
  • 应用场景:共同关注、共同粉丝、共同爱好等

SortedSet(zset)

  • 命令:zadd, zrange, zcard, zrank, zscore

  • 底层实现:ziplist、skiplist(包含一个跳表和一个字典)

  • 应用场景:直播间的在线用户列表,礼物排行版

  • 补充

    • 什么是跳表?

      跳表的插入、删除、查找的平均时间复杂度是O(logN)。它实际上是有序的链表,只是它为每个节点维护多个指向其他节点的指针,每个节点的指针数量可以理解为层数,每层是一个指针,指针会链接同一层的下一个节点,层数是随机出来的,越高的层数随机的概率越低,也就是说同一层中,层数越高节点数量就越少。查找的时候从最高层开始,找到最后一个小于当前元素的位置,跳到下一层,每层可以跳过部分节点,从而实现快速的查找

    • 跳表的其他应用:ConcurrentSkipListMap(对数据没有强一致性要求的高并发场景)、ElasticSearch的倒排索引(对于英文单词,可以通过跳表快速查询)

    • 实现:面试题之跳表

底层

  • SDS:free + len + buf。String的长度大于39字节时使用
  • embstr:free + len + buf。和SDS的不同是buf不是指针而是一块连续的空间。对embstr做修改后会变成SDS
  • ziplist:zlbytes+zltail+zllen+节点+zlend,zlbytes表示整个列表占用的字节数,zltail表示尾节点距离起始地址的字节数,zllen表示节点数量,zlend表示末端(0xFF特殊值)。节点结构为:前一个节点的长度+编码+实际数据
  • linkedlist:head+tail+len
  • hashtable:size+mask+used+table
  • intset:encoding(int或者long)+len+整数数组

Redis过期键删除策略

惰性删除+定期删除

  • 惰性删除策略:所有的读写操作都会对键检查,如果键已失效被删除或者键不存在,则执行键不存在的操作;否则执行键存在的操作
  • 定期删除策略:默认每隔100ms随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。它会在规定的时间内,分多次遍历所有的数据库。

内存淘汰机制

在两种策略下仍然可能出现内存中堆积大量过期的key,或者说堆积大量没有设置过期但不是热点数据的key

  1. volatile-lru:从已设置过期的时间数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. allkeys-lru:在键空间中,移除最近最少使用的key(常用)
  3. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  4. allkeys-lfu:在键空间中,移除最近最不经常使用的key(常用)
  5. volatile-random:从已设置过期的时间数据集(server.db[i].expires)中随机挑选数据淘汰
  6. allkeys-random:在键空间中,随机移除key
  7. volatile-ttl:从已设置过期的时间数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  8. no-eviction:拒绝移除任何数据,报错

Redis持久化

RDB

默认的持久化方式。在指定时间间隔内生成数据库的快照。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。

三种情况下会保存RDB:

  1. save 900 1:900秒之后,如果至少一个key发生变化,Redis自动触发BGSAVE
  2. save 300 10:300秒之后,如果至少有10个key发生变化,Redis自动触发BGSAVE
  3. save 60 10000:60秒之后,如果至少有10000个key发生变化,Redis自动触发BGSAVE

记录的内容:二进制形式的键值对

AOF

持久化记录服务器执行的所有写操作命令

三种持久化方式:

  1. appendfsync always:每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
  2. appendfsync everysec:每秒钟同步一次,显式地将多个写命令同步到硬盘
  3. appendfsync no:让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

记录的内容:操作

AOF重写

如何解决AOF体积过大、还原时间长的问题?

使用BGREWRITEAOF命令,实现原理:AOF重写不需要读取旧的AOF文件,而是读取数据库状态,将键值对的关系用命令保存下来(若某个键有多个值,多个值会合并成一条命令,默认是每64个元素变成一条命令)。在AOF重写期间,新的写命令会保存到AOF重写缓冲区内;当AOF重写完成后,AOF重写缓冲区内的操作写入到新的AOF文件中,新的AOF文件原子地覆盖旧AOF文件(该过程会阻塞父进程)

混合持久化

Redis4.0支持RDB和AOF的混合持久化。持久化开始的时刻,生成数据库的RDB快照,在生成过程中的修改操作记录到AOF缓冲区,之后将RDB快照内容和AOF增量内容写入到新的AOF文件中,最后原子地覆盖旧AOF文件

优点:数据恢复快

缺点:AOF里面的RDB部分是压缩格式而不是AOF格式,可读性较差

RDB和AOF的优缺点

RDB

优点:

  1. 保存的是二进制文件,占用的空间小
  2. 做数据恢复时速度快

缺点:

  1. 可读性差
  2. 服务器出现故障时,可能丢失较多数据
  3. 数据量大时,每次备份可能比较耗时

AOF

优点:

  1. 存放的是操作,可读性好
  2. 通过配置持久化方式,最多丢失一秒的数据
  3. 操作会记录在文件末尾,写入性能高

缺点:

  1. 占用的空间比RDB文件大
  2. 恢复数据时速度相对较慢

缓存异常

缓存雪崩

定义:缓存在同一时间大面积失效,导致请求全部落到数据库上,造成数据库短时间接受大量请求而崩掉

产生原因:

  • 大量的key设置了相同的过期时间
  • Redis宕机

解决办法:

  1. 在原有的过期时间上再加一个随机值。避免了采用相同的过期时间导致的缓存雪崩
  2. 使用熔断机制。当流量达到一定阈值,直接返回提示或者默认值,防止过多的请求打在数据库上,至少保证一部分用户是可以正常使用的,其他用户多刷新几次也能得到结果
  3. 保证Redis集群的高可用性
  4. 提高数据库的容灾能力,使用分库分表、读写分离策略

缓存击穿

定义:大量请求的热点key突然失效,导致请求直接落在数据库上

解法办法:

  1. 过期时间设置在系统低流量的时间段,比如凌晨三四点
  2. 使用互斥锁。只有拿到锁才可以查询数据库
  3. 使用限流。根据数据库的承受能力限制进入数据库查询的请求数量

缓存穿透

定义:大量请求的key不存在于缓存中,导致请求直接落在数据库上

产生原因:黑客大量制造不存在于缓存中key

解决办法:使用布隆过滤器。若布隆过滤器判断某个key不存在,则一定不存在,否则可能存在

缓存和数据库一致性

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

    不一致场景:缓存刚好失效,线程A去数据库里读数据,另一个线程B更新数据库并删除缓存,然后A将旧的数据写入到了缓存中。不过由于读的速度快于写的速度,出现的概率很小。

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

    不一致场景:线程A删除缓存,另一个线程查询缓存未命中,查询数据库并更新到缓存中,A更新数据库。由于读的速度快于写的速度,出现的概率高于第一种。

  3. 先更新数据库,再更新缓存。

    不一致场景:线程A更新数据库,另一个线程B更新数据库并更新缓存,然后A更新缓存。从缓存层面看,出现了线程安全问题,因为缓存中的数据是先更新了数据库的那个线程的数据。此外,从业务上看,如果写操作多,频繁的更新缓存浪费性能。

  4. 先更新缓存,再更新数据库。

    不一致场景:线程A更新缓存,另一个线程B更新缓存并更新数据库,然后A更新数据库。问题和情况3相同

事务

原子性:事务队列中的命令要么全部执行,要么全部不执行。但是在执行过程中发生的错误不会让事务回滚(比如string类型的数据用了LPUSH,没有执行的时候服务器无法判断是否正确的)

一致性:对于入队错误,Redis拒绝执行此事务;对于执行错误,Redis不会中断事务,其他命令不会被错误的命令影响

隔离性:Redis使用单线程执行事务,并且保证在执行事务期间不会中断事务

Lua脚本如何保证原子性的?Lua脚本在执行过程中,不会有其他的脚本和命令同时执行,因为Redis是单线程的

Redis高可用方案

主从复制

一个master,多个slave

拓扑结构

  • 星形结构
  • 链式结构

复制模式(同步策略)

  • 全量复制:master全部同步到slave,用于初次复制
  • 增量复制:slave断线重连后只复制丢失的数据(通过复制积压缓冲区实现)

存在的问题

  • 同步故障
    • 复制数据延迟(master和slave数据不一致)
    • 读取过期数据(slave不能主动删除过期的数据)
  • 避免全量复制
    • 增大复制积压缓冲区(默认为1MB)
    • 如果节点运行id不匹配(如master重启、master运行id发生变化),此时执行全量复制,应该配合哨兵和集群解决

哨兵集群

是Redis的高可用性解决方案

节点下线

  • 主观下线

    Sentinel(哨兵)节点对Redis节点失败的偏见,超出超时时间认为节点宕机

    哨兵集群的每个Sentinel节点会定时(默认1s)对Redis集群的所有节点发送心跳包检测节点是否正常。如果一个节点在down-after-milliseconds时间内没有回复Sentinel节点的心跳包,则该Sentinel将该Redis节点标记为主观下线

  • 客观下线

    当节点被一个Sentinel节点标记为主观下线,并不意味着该节点一定故障了,还需要Sentinel集群里的其他Sentinel节点共同判断为主观下线才行。该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过quorum个Sentinel节点认为master主观下线,则该Sentinel节点认为master客观下线

Leader选举

  • 当一个master被判断为客观下线时,由Leader Sentinel 对客观下线的master执行故障转移

  • 选举一个Sentinel作为Leader的前提:集群中至少有三个Sentinel节点

  • 选举流程

    1. 每个发现master客观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者.
    2. 收到命令的Sentinel节点如果没有同意过其他Sentinel节点发送的命令,则同意请求,否则拒绝
    3. 如该Sentinel节点发现自己的票数超过Sentinel集群一半的数量,则它成为Leader
    4. 如果此过程没有选出Leader,则等待一段时间再重新进行选举。

故障转移

转移流程

  1. Sentinel选出一个合适的slave作为新的master(slaveof no one命令)
  2. 向其余slave发出通知,让他们成为新master的slave
  3. 更新新master的slave信息,待旧master复活,让他成为新master的slave
  4. 向客户端通知master变化

选择slave的规则

  1. 选择slave-priority最高的节点
  2. 选择复制偏移量最大的节点(同步数据最多)
  3. 选择runId最小的节点

Sentinel 集群运行过程中故障转移完成,所有 Sentinel 又会恢复平等。Leader 仅仅是故障转移操作出现的角色。

定时任务

  • 每1s每个Sentinel对所有节点执行ping,进行心跳检测

  • 每2s每个Sentinel向master和slave的命令连接发送信息(channel为__sentinel__:hello

    Sentinel通过订阅连接接收master和slave的信息

    Sentinel之间创建的是命令连接,因为Sentinel是通过master和slave的订阅连接来发现Sentinel,不需要创建订阅连接

  • 每10s每个Sentinel对master和slave执行info,目的是发现slave节点,确定主从关系

补充

  1. Sentinel如何发现master?不存在这个问题,只能一开始就配置好!

  2. Sentinel如何发现slave?

    每10秒Sentinel通过命令连接向master发送INFO,通过master回复的消息不仅可以得到master的信息,也可以得到slave的地址信息

  3. slave如何成为master的slave?在slave上执行命令SLAVEOF

  4. Sentinel如何发现Sentinel?

    Sentinel与服务器建立订阅连接后,会发送hello消息,订阅了服务器的Sentinel能收到消息后就能更新自身对其他Sentinel的认识

  5. 故障转移流程中,如何当旧的master上线时通知它?(因为此时Leader已经不存在了)

    新的master的slave信息会更新,旧的master上线后,Sentinel发送SLAVEOF命令让它成为新master的slave

优缺点

优点:基于主从复制,解决主从复制master宕机时需要手动切换的问题

缺点:只有一个Redis主机接收处理写请求,容易成为瓶颈

分布式集群(cluster)

Gossip协议:某个节点要传播消息时,会随机选择周围的几个节点传播消息,收到消息的节点也重复该过程。

寻址分片

hash取模

最朴素的方法,hash(key) % 机器数量

存在的问题:当新增一台机器或者机器宕机时,导致hash的结果发生变化,使得对所有服务器的缓存请求全部失效,大量请求打到数据库上

一致性hash
  1. 一致性hash对2^32取模,将整个空间组织成hash环
  2. 所有服务器先用哈希函数算hash,从而确定在hash环上的位置
  3. 数据key用相同的哈希函数计算hash,确定数据在环上的位置,从该位置顺时针遍历遇到的第一个服务器就是其应该定位的服务器

容错性和可扩展性

容错性:当服务器C宕机后,只会让所有以C为第一次遇到服务器的数据查询缓存失败

扩展性:在B和C之间增加一台服务器后,查询C上缓存的数据部分失败

存在的问题

由于节点不均匀会导致被缓存的对象大部分缓存在某一台服务器上。(数据倾斜)

解决办法:引入虚拟节点机制,即每个服务器计算多个哈希函数,分布在hash环上,然后再映射虚拟节点到实际节点的关系。实际应用有Dubbo一致性hash负载均衡。取模操作通过TreeMap解决(若计算的hash值大于最大值,会得到null,因此取第一个元素就相当于取模操作了)

Redis中的实现:hash槽

hash -> 槽 -> 实际节点

CRC16(key) & 16383:return crc16(key,keylen) & 0x3FFF;

每个master持有部分slot,增加一个master,就将其他master的hash slot部分迁移过去;减少一个master,就将它的slot迁移到其他master上。

每个master知道所有槽的负责对象,以及自己负责的槽

  • 当请求算出来的槽不由当前master负责,则它返回MOVED让请求到对应的服务器上再次请求一次
  • 当请求算出来的槽正在迁移阶段,则它返回ASK错误,让请求到迁移目的服务器上再次请求一次

节点下线

  • 疑似下线

    每个节点定时向其他节点发送PING消息,超时未收到PONG消息则将接收PING命令的节点标记为疑似下线

    如果主节点A通过消息知道主节点B将主节点C标记为疑似下线,则A会将B的下线报告添加到自己维护的C的结构

    疑似下线报告是有时效性的,如果超过cluster-node-timeout * 2的时间,这个下线报告会被忽略,C会恢复成正常状态

  • 下线

    一个集群里,半数以上负责处理槽的master将某个主节点x标记为疑似下线,则x被标记为下线,将x标记为下线的节点(该节点通过疑似下线报告的数量超过了一半从而确定了x下线)会向集群广播(所有节点立即知道x下线)

故障转移

  1. 从下线master的slave中选择一个合适的节点
  2. 被选中的slave执行SLAVEOF no one成为新的master
  3. 新的master撤销所有指派给旧master的槽指派,让这些槽全部指派给自己
  4. 新的master广播一条PONG消息,集群中其他节点立即知道这个节点成为了master,并且它已经接管了槽
  5. 新的master开始接收和处理指派给自己槽的命令

master选举

类似于Sentinel集群的Leader选举

  1. slave发现自己正在复制的master是下线状态,则向集群广播消息,要求设置它为master
  2. 每个负责槽的master有一次投票机会,如果它未投过票,则同意
  3. 如果slave收到超过半数的同意,则它成为新的master
  4. 如果此过程没有选出master,则集群进入下一个配置纪元(currentEpoch),再次进行选举直到选出新的master

最终一致性

master处理完客户端的写请求后就返回,它的slave是异步复制的

https://redis.io/topics/cluster-tutorial

READONLY

默认情况下slave会将客户端的请求重定向到master,使用READONLY允许客户端的读请求由slave处理

https://redis.io/commands/readonly

Redis使用场景

分布式锁

单个Redis实例

SET命令设置key,并指定过期时间,即使获得锁的线程挂了,超时后锁也能被释放。当用完后,主动删除key

SET key_name random_value NX PX 3000 # NX表示不存在key则设置,否则不设置;PX表示过期时间用毫秒级

存在的问题

  • 线程拿到锁后,被挂起,在挂起期间锁过期了,其他线程拿到了锁
  • 线程拿到锁后,执行的时间大于锁租约的时间,其他线程拿到了锁
  • 线程执行的时间大于锁租约的时间后,主动删除key把其他线程的锁删除了
  • 线程如何实现可重入

解决方案

锁续租,在线程拿到锁之后,再开另一个线程,在该锁要过期之前为它续租

SET的值用唯一ID标识线程,线程主动删除key的时候要判断是否是自己持有锁

ThreadLocal记录重入次数

多个Redis实例(Redlock)

防止单点故障。

原理

5个master节点

  1. client得到当前的时间,微秒单位
  2. 尝试顺序地在5个节点上申请锁,client需要设置与master沟通的timeout大小,避免长时间和一个fail的节点浪费时间
  3. 当client在大于等于3个节点上成功申请到锁时,计算一个申请锁的流逝时间(当前时间减去第一步的时间),如果锁的持续时间(lock validity time)多于流逝时间,那么就真正获得锁
  4. 锁的有效时间是lock validity time - 流逝时间
  5. 如果client申请锁失败,则它在成功申请到锁的master上释放锁
  6. 等待随机的时间后,重新尝试获得锁

存在的问题

使用时间来解决一致性,但是它依赖于时间假设:

  • 时间本身是正确的(不会有时钟突然提前或延后的情况)
  • 跟锁的过期时间相比,网络延迟很小
  • 跟锁的过期时间相比,进程停顿很短

消息队列

  1. 用list做队列,生产者用rpush放入消息,消费者用blpop消费消息,当没有消息的时候会阻塞,直到有消息
  2. 一次生产多次消费如何实现?用发布订阅模型
  3. 发布订阅模型有什么缺点?消费者掉线后生产的消息会丢失
  4. 如何实现延时队列?用zset,时间戳作为score,消息作为内容。消费者通过zrangebyscore获取 N 秒之前的数据

https://blog.csdn.net/flyingwzb/article/details/83866215

注册中心

原理

Key存服务名,Value可以用Hash类型,它的键是URL地址,值是过期时间

  1. 服务提供方在指定的Key下添加自己的信息,同时向指定通道发送register事件(也就是消息)
  2. 服务消费方订阅指定通道的registerunregister事件
  3. 服务消费方收到registerunregister事件后,获得指定Key的服务提供者列表
  4. 服务监控中心可以查看服务提供方的信息,清理掉线的服务提供方

缺点

  1. 注册中心的可靠性依赖于Redis的可靠性

  2. 要求服务器时间必须同步,用于心跳检测过期数据,对服务器有一定压力

[Dubbo:Redis 注册中心](