1、Redis 支持的数据类型有哪些?Redis.png

三种特殊的数据类型:

  1. Bitmap:位图,Bitmap想象成一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap中叫做偏移量。使用Bitmap实现统计功能,更省空间。如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用 Bitmap,因为它只用一个 bit 位就能表示 0 或 1。
  2. Hyperloglog:HyperLogLog 是一种用于统计基数的数据集合类型,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。场景:统计网页的UV(即Unique Visitor,不重复访客,一个人访问某个网站多次,但是还是只计算为一次)。要注意,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。
  3. Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如朋友的定位、附 近的人、打车距离计算等。

    2、Redis ⼀般都有哪些使用场景?

    Snipaste_2022-01-06_17-09-40.png
    Redis 适合的场景

  4. 缓存:减轻 MySQL 的查询压力,提升系统性能;

  5. 排行榜:利用 Redis 的 SortSet(有序集合)实现;
  6. 计算器/限速器:利用 Redis 中原子性的自增操作,我们可以统计类似⽤户点赞数、⽤户访问数等。这类操作。如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个 API的频率,常⽤的有抢购时,防⽌⽤户疯狂点击带来不必要的压力;
  7. 好友关系:利⽤集合的⼀些命令,比如求交集、并集、差集等。可以⽅便解决⼀些共同好友、共同爱好之类的功能;
  8. 消息队列:除了 Redis ⾃身的发布/订阅模式,我们也可以利⽤ List 来实现⼀个队列机制,⽐如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的 DB 压⼒,完全可以⽤ List 来完成异步解耦;
  9. Session 共享:Session 是保存在服务器的⽂件中,如果是集群服务,同⼀个⽤户过来可能落在不同机器上,这就会导致⽤户频繁登陆;采用Redis 保存 Session 后,⽆论⽤户落在那台机器上都能够获取到对应的Session 信息。

Redis 不适合的场景
数据量太⼤、数据访问频率⾮常低的业务都不适合使⽤ Redis,数据太⼤会增加成本,访问频率太低,保存在内存中纯属浪费资源。

3、Redis 为什么这么快?

  1. 完全基于内存,绝大部分部请求是纯粹的内存操作,非常快速;
  2. 数据结构简单,对数据操作也简单;
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
    1. 单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理, 即一个线程处理所有网络请求。
  4. 使用多路 I/O 复用模型,非阻塞 IO。Redis使用多路复用IO技术,将 epoll 作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。

Snipaste_2022-01-06_17-42-54.png

4、Redis 为何选择单线程?

在 Redis v6.0 以前,Redis 的核心网络模型选择用单线程来实现。先来看下官方的回答:

It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redisis either memory or network bound. For instance, using pipelining Redisrunning on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.

核心意思就是,对于一个 DB 来说,CPU 通常不会是瓶颈,因为大多数请求不会是 CPU 密集型的,而是
I/O 密集型。具体到 Redis的话,如果不考虑 RDB/AOF 等持久化方案,Redis是完全的纯内存操作,执
行速度是非常快的,因此这部分操作通常不会是性能瓶颈,Redis 真正的性能瓶颈在于网络 I/O,也就是
客户端和服务端之间的网络传输延迟,因此 Redis 选择了单线程的 I/O 多路复用来实现它的核心网络模
型。
实际上更加具体的选择单线程的原因如下:

  1. 避免过多的上下文切换开销:如果是单线程则可以规避进程内频繁的线程切换开销,因为程序始终运行在进程中单个线程内,没有多线程切换的场景。
  2. 避免同步机制的开销:如果 Redis选择多线程模型,又因为 Redis是一个数据库,那么势必涉及到底层数据同步的问题,则必然会引入某些同步机制,比如锁,而我们知道 Redis不仅仅提供了简单的 key-value 数据结构,还有 list、set 和 hash 等等其他丰富的数据结构,而不同的数据结构对同步访问的加锁粒度又不尽相同,可能会导致在操作数据过程中带来很多加锁解锁的开销,增加程序复杂度的同时还会降低性能。
  3. 简单可维护:如果 Redis 使用多线程模式,那么所有的底层数据结构都必须实现成线程安全的,这无疑又使得 Redis 的实现变得更加复杂。

总而言之,Redis选择单线程可以说是多方博弈之后的一种权衡:在保证足够的性能表现之下,使用单线
程保持代码的简单和可维护性。

5、Redis v6.0 为何引入多线程?

很简单,就是 Redis的网络 I/O 瓶颈已经越来越明显了。
随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统
消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向:

  • 优化网络 I/O 模块;
  • 提高机器内存读写的速度

后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向:

  • 零拷贝技术或者 DPDK 技术;
  • 利用多核优势

零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的
消耗和 Linux 零拷贝技术。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需
要内核甚至是硬件的支持。

因此,利用多核优势成为了优化网络 I/O 性价比最高的方案。

6、Redis RDB AOF

Redis持久化机制 RDB、AOF

7、Redis的过期策略

Redis支持如下两种过期策略:

  1. 惰性删除:客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。
  2. 定期删除:Redis会将设置了过期时间的key放到一个独立的字典中,并对该字典进行每秒10次的过期扫描。

定期删除:过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略。
该策略的删除逻辑如下:

  1. 从过期字典中随机选择20个key;
  2. 删除这20个key中已过期的key;
  3. 如果已过期key的比例超过25%,则重复步骤1。

    8、Redis的缓存淘汰策略

    当写入数据将导致超出maxmemory(最大内存)限制时,Redis会采用maxmemory-policy所指定的策略进行数据淘汰,该策略一共包含如下8种选项
策略 描述
noeviction 直接错误返回
volatile-ttl 从设置了过期时间的键中,选择过期时间最小的键,进行淘汰
volatile-random 从设置了过期时间的键中,随机选择键,进行淘汰
volatile-lru 从设置了过期时间的键中,使用LRU算法选择键,进行淘汰
volatile-lfu 从设置了过期时间的键中,使用LFU算法选择键,进行淘汰
allkeys-random 从所有键中,随机选择键,进行淘汰
allkeys-lru 从所有键中,使用LRU算法选择键,进行淘汰
allkeys-lfu 从所有键中,使用LFU算法选择键,进行淘汰

volatile前缀代表从设置了过期时间的键中淘汰数据,allkeys前缀代表从所有的键中淘汰数据。关于后缀,ttl代表选择过期时间最小的键,random代表随机选择键,需要我们额外关注的是lru和lfu后缀,它们分别代表采用lru算法和lfu算法来淘汰数据。
LRU(Least Recently Used)是按照最近最少使用原则来筛选数据,即最不常用的数据会被筛选出来!

  • 标准LRU:把所有的数据组成一个链表,表头和表尾分别表示MRU和LRU端,即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端,而新增的数据也是刚被访问的数据,也会被移动到MRU端。当链表的空间被占满时,它会删除LRU端的数据。
  • 近似LRU:Redis会记录每个数据的最近一次访问的时间戳(LRU)。Redis执行写入操作时,若发现内存超出maxmemory,就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后内存依然超出限制,则继续采样淘汰。可以通过maxmemory_samples配置项,设置近似LRU每次采样的数据个数,该配置项的默认值为5。

LRU算法的不足之处在于,若一个key很少被访问,只是刚刚偶尔被访问了一次,则它就被认为是热点数据,短时间内不会被淘汰。
LFU算法正式用于解决上述问题,LFU(Least Frequently Used)是Redis4新增的淘汰策略,它根据key的最近访问频率进行淘汰。
LFU在LRU的基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出内存。如果两个数据的访问次数相同,LFU再比较这两个数据的访问时间,把访问时间更早的数据淘汰出内存。

9、如何保证缓存与数据库的双写一致性?

如何保证Redis缓存与数据库的一致性?

10、如何实现Redis的高可用?

实现Redis的高可用,主要有哨兵集群两种方式。
哨兵

主从模式下,当主服务器宕机后,需要⼿动把⼀台从服务器切换为主服务器,这就需要人工干预,费时费力,还会造成⼀段时间内服务不可用。这种方式并不推荐,实际⽣产中,我们优先考虑哨兵模式。这种模式下,master 宕机,哨兵会⾃动选举 master 并将其他的 slave 指向新的 master。

Redis Sentinel(哨兵)是一个分布式架构,它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点和其余的哨兵节点进行监控,当发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它就会与其他的哨兵节点进行协商,当多数哨兵节点都认为主节点不可达时,它们便会选举出一个哨兵节点来完成自动故障转移的工作,同时还会将这个变化实时地通知给应用方。整个过程是自动的,不需要人工介入,有效地解决了Redis的高可用问题。
一组哨兵可以监控一个主节点,也可以同时监控多个主节点,两种情况的拓扑结构如下图:
image.png
哨兵节点包含如下的特征:

  1. 哨兵节点会定期监控数据节点,其他哨兵节点是否可达;
  2. 哨兵节点会将故障转移的结果通知给应用方;
  3. 哨兵节点可以将从节点晋升为主节点,并维护后续正确的主从关系;
  4. 哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点信息;
  5. 节点的故障判断是由多个哨兵节点共同完成的,可有效地防止误判;
  6. 哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用,整个集合依然是健壮的;
  7. 哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令。

优点:

  1. Redis Sentinel集群部署简单;
  2. 能够解决Redis主从模式下的高可用问题;
  3. 很方便实现Redis数据节点的线形扩展,轻松突破Redis自身单线程瓶颈,可极极大满足Redis大容量或高性能的业务需求;
  4. 可以实现⼀套Sentinel监控⼀组Redis数据节点或多组数据节点。

缺点:

  1. 部署相对Redis主从模式要复杂⼀些,原理理解更繁琐;
  2. 资源浪费,Redis数据节点中slave节点作为备份节点不提供服务;
  3. Redis Sentinel主要是针对Redis数据节点中的主节点的⾼可⽤切换,对Redis的数据节点做失败判定分为主观下线和客观下线两种,对于Redis的从节点有对节点做主观下线操作,并不执行故障转移。
  4. 不能解决读写分离问题,实现起来相对复杂。

集群
Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:

  1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;
  2. 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
  3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

Redis集群中数据的分片逻辑如下图:
image.png
Redis集群的功能限制:

  1. key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mset、mget等操作可能存在于多个节点上所以不被支持。
  2. key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
  3. key作为数据分区的最小粒度,因此不能将一个大的键值对象(如hash、list等)映射到不同的节点。
  4. 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即DB0。
  5. 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

Redis集群的通信方案:
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。
Redis集群采用P2P的Gossip(流言)协议,Gossip协议的工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。通信的大致过程如下:

  1. 集群中每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口号上加10000;
  2. 每个节点再固定周期内通过特定规则选择几个节点发送ping消息;
  3. 接收ping消息的节点用pong消息作为响应。

其中,Gossip协议的主要职责就是信息交换,而信息交换的载体就是节点彼此发送的Gossip消息,Gossip消息分为:meet消息、ping消息、pong消息、fail消息等。

  • meet消息:用于通知新节点加入,消息发送者通知接受者加入到当前集群。meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换。
  • ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息封装了自身节点和一部分其他节点的状态数据。
  • pong消息:当接收到meet、ping消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内封装了自身状态数据,节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
  • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。

虽然Gossip协议的信息交换机制具有天然的分布式特性,但它是有成本的。因为Redis集群内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。所以,Redis集群的Gossip协议需要兼顾信息交换的实时性和成本的开销。

  • 集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。
  • 如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的超时选项设置时长的一半(cluster-node-timeout/2),那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
  • 每个消息主要的数据占用:slots槽数组(2KB)和整个集群1/10的状态数据(10个节点状态数据约1KB)。

    11、什么是缓存穿透?怎么解决?

    缓存穿透是指客户端查询⼀个⼀定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
    解决方案:

  • 缓存空对象:存储层未命中后,仍然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回 空值; (空值做了缓存,意味着缓存中存了更多的键,需要更多的空间,针对这些数据可以设置一个较短的过期时间,让其自动删除)

  • 布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回 空值;

    12、说一说你对布隆过滤器的理解

    【布隆过滤器详解】,Redis缓存穿透解决方案
    布隆过滤器可以用很低的代价,估算出数据是否真实存在。例如:给用户推荐新闻时,要去掉重复的新闻,就可以利用布隆过滤器,判断该新闻是否已经推荐过。
    布隆过滤器的核心包括两部分:
  1. 一个大型的位数组;一窜二进制数组组成(插入和查询的速度非常快)
  2. 若干个不一样的哈希函数,每个哈希函数都能将哈希值算的比较均匀。

    为什么需要多个Hash函数? 误差率越小,它所占用的空间越大,所需要的哈希函数越多。还有是因为相同的数据算出来的哈希值可能是一样的,相同率很大。为了减少算出的哈希值相同的概率,所以增加了不同的哈希函数,每个哈希函数的所使用的哈希算法不同,算出来的哈希位置也越多,进而数组中的位置也越多。 为什么所占的空间越大? 因为它所需的哈希函数越多,计算出来的哈希值也越多,对应的二进制数据也越多

布隆过滤器的工作原理:

  1. 添加key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置,并将位数组中这个位置的值设置为1。
  2. 询问key时,每个哈希函数都利用这个key计算出一个哈希值,再根据哈希值计算一个位置。然后对比这些哈希函数在位数组中对应位置的数值:

    • 如果这几个位置中,有一个位置的值是0,就说明这个布隆过滤器中,不存在这个key。
    • 如果这几个位置中,所有位置的值都是1,就说明这个布隆过滤器中,极有可能存在这个key。之所以不是百分之百确定,是因为也可能是其他的key运算导致该位置为1。

      13、什么是缓存雪崩?该如何解决?

      在某一时刻,是缓存中有大量数据同时过期,导致大量请求无法得到处理,发生大量的缓存穿透,所有的查询都落在数据库上。
      解决方案:
  3. 避免数据同时过期,设置过期时间时,附加一个随机数,避免大量的key同时过期;

  4. 启用降级和熔断措施,在发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息;在发生雪崩时, 对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回;
  5. 构建高可用的缓存服务,采用哨兵或集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用。
  6. 加锁排队:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许⼀个线程查询数据和写缓存,其他线程等待;
  7. 数据预热:可以通过缓存 reload 机制,预先去更新缓存,再即将发生大并发访问前⼿动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀;
  8. 做⼆级缓存,或者双缓存策略:Cache1 为原始缓存,Cache2 为拷贝缓存,Cache1 失效时,可以访问Cache2,Cache1 缓存失效时间设置为短期,Cache2 设置为⻓期。

    14、什么是缓存击穿?该如何解决?

    一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
    解决方案:

  9. 永不过期:热点数据不设置过期时间,所以不会出现上述问题,这是“物理”上的永不过期; 为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存;

  10. 加互斥锁:对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。

    15、Redis 怎么实现分布式锁?

    如何利用Redis实现一个分布式锁?

    16、说一下 Redis 中的 watch 命令

    很多时候,要确保事务中的数据没有被其他客户端修改才执行该事务。
    Redis 提供了 「watch」 命令来解决这类问题,这是一种乐观锁的机制客户端通过 watch 命令,要求服务器对一个或多个 key 进行监视,如果在客户端执行事务之前,这些 key 发生了变化,则服务器将拒绝执行客户端提交的事务,并向它返回一个空值。

    17、Redis的主从同步是如何实现的?

    从2.8版本开始,Redis使用psync命令完成主从数据同步,同步过程分为全量复制和部分复制。全量复制一般用于初次复制的场景,部分复制则用于处理因网络中断等原因造成数据丢失的场景。psync命令需要以下参数的支持:

  11. 复制偏移量:主节点处理写命令后,会把命令长度做累加记录,从节点在接收到写命令后,也会做累加记录;从节点会每秒钟上报一次自身的复制偏移量给主节点,而主节点则会保存从节点的复制偏移量。

  12. 积压缓冲区:保存在主节点上的一个固定长度的队列,默认大小为1M,当主节点有连接的从节点时被创建;主节点处理写命令时,不但会把命令发送给从节点,还会写入积压缓冲区;缓冲区是先进先出的队列,可以保存最近已复制的数据,用于部分复制和命令丢失的数据补救。
  13. 主节点运行ID:每个Redis节点启动后,都会动态分配一个40位的十六进制字符串作为运行ID;如果使用IP和端口的方式标识主节点,那么主节点重启变更了数据集(RDB/AOF),从节点再基于复制偏移量复制数据将是不安全的,因此当主节点的运行ID变化后,从节点将做全量复制。

psync命令的执行过程以及返回结果,如下图:
image.png
全量复制的过程,如下图:
image.png
部分复制的过程,如下图:
image.png