面试系列(三):redis

636 阅读25分钟

redis有几个库?如何切库?

Redis默认提供了16个库。

使用select命令切库,比如说select2,就是切换到第三个库,因为redis的库是从0开始排序的。

Redis的数据结构都有哪些?

说到redis的数据结构,主要有String字符串,list列表,hash表,set集合,还有sorted set有序集合。这是redis的基本数据类型。

String字符串就不用说了,就是正常理解上的字符串,底层是简单动态字符串。

然后就是list,这个list和我们java中的list不一样,可以将它理解为链表,它底层的数据结构是双向链表和压缩列表。

再就是hash,hash是redis中最常见的数据结构,底层是hash表,redis中所有的key,以及key对应的数据,全部都是由hash表组成的,但是hash在数据量少的时候,是使用压缩链表构建的,当数据量大到一定的地步的时候,就会使用hash组成数据结构。

还有sorted set和set,一个有序集合,一个普通集合,有序集合底层是跳表,普通集合底层就是数组。

除了这5种基本数据类型,redis还提供了bitMaps,HyperLogLogs,GEO这三种高级数据结构。如果我们记录的数据只有0和1两个值的状态,bigmap会是一个很好的选择,因为bitmap中的一个数据,是只用一个bit记录的,可以节省内存。如果是进行基数统计,就是统计一个集合内不重复的元素的个数,而且集合的元素的数量达到了亿级别,而且不需要精确统计,就可以使用Hypeloglogs。  

Redis里面的跳表数据结构

我们知道,redis的基本数据结构中有一个叫sorted set,有序集合,它的底层就是跳表结构。

在数据量比较稀少的情况下,sorted set的底层是使用压缩链表存储数据的,但是一旦数据量上去了,那么有序链表因为只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。

跳表是在链表的基础上,增加了多级索引,就是说在链表之上,再增加一级索引,如果还是有点慢,那么在这一级的索引上面,再增加一级索引,这样形成的数据结构,就是我们的跳表。

跳表的优点就是查询速度快,要比链表快太多。

redis为什么使用跳表而不使用B+树或二叉树呢?

redis支持多种数据结构,里面有个有序集合,也叫ZSET。内部实现就是跳表。那为什么要用跳表而不用B+树等结构呢?

这个几乎每次面试都要被问一下。

虽然已经很熟了,但每次都要装作之前没想过,现场思考一下才知道答案。

真的,很考验演技。

大家知道,redis 是纯纯的内存数据库。

进行读写数据都是操作内存,跟磁盘没啥关系,因此也不存在磁盘IO了,所以层高就不再是跳表的劣势了。

并且前面也提到B+树是有一系列合并拆分操作的,换成红黑树或者其他AVL树的话也是各种旋转,目的也是为了保持树的平衡。

而跳表插入数据时,只需要随机一下,就知道自己要不要往上加索引,根本不用考虑前后结点的感受,也就少了旋转平衡的开销。

因此,redis选了跳表,而不是B+树。

为什么说redis的性能可以这么高?或者说单线程的redis为什么性能可以这么高?

首先,我们要知道,为什么说redis是单线程的?

Redis的单线程,主要指的是reids的网络IO和键值对读写是由一个线程完成的,这也是redis对提供服务的主要流程,但是redis的其他功能,比如说持久化,异步删除等操作,其实都是由额外的线程执行的。

那么,为什么redis是单线程呢?我们要意识到一个问题,redis里面的所有数据,某种意义上来讲都是共享数据,也就是说,如果使用多线程来操作这些变量,那么必然就带来了并发访问控制问题,而且线程的切换也会带来很大的开销,所以redis最后使用了单线程。

那么,为什么单线程的redis性能如此之高呢?

我觉得,这应该是个综合性的结果,一方面,redis的大部分操作是在内存中完成的,而且它还采用了高效的数据结构,像压缩链表,跳表,哈希表等等,这是它能实现高性能的一个重要原因。另外一方面,即便redis是单线程的,但是它使用了多路复用机制。

我们知道,redis是使用socket实现和客户端之间的通信的,那么就会有监听套接字和已连接套接字,一般的IO操作,一个线程都是会阻塞在一个监听套接字或者是一个已连接套接字上面的。

但是IO多路复用机制不同,它指的是一个线程处理多个IO流,也就是我们经常听到的select/epoll机制,它允许操作系统的内核中,同时存在多个监听套接字和多个已连接套接字,也就是说,redis的单线程不会阻塞在单个的监听套接字或者是已连接套接字上,而是当监听套接字监听到一个请求过来了,会触发响应的事件,然后将这个事件放进一个事件队列中,然后redis对这个队列中事件不断的进行处理。

这样的话,redis的性能就会很好了。

Redis的单线程处理IO请求的性能瓶颈有哪些?

一 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
1.1 操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
1.2 使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
1.3 大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
1.4 淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
1.5 AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
1.6 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
二 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。

针对问题1,一方面需要业务人员去规避,一方面Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
针对问题2,Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。

请大概讲一下redis的持久化方案。

Redis的持久化策略主要是两种,AOF日志和RDB快照。

首先我们来讲一下AOF日志,AOF日志写后日志,也就是说redis要先执行命令,把数据写入到内存后,才去记录一条日志,这一点和mysql是有区别的,mysql是写前日志。那么,为什么AOF是写后日志呢,原因主要有两点,第一点,如果是写前日志,我们在写入一条日志的时候,必然是要校验这条日志是否是正确的,那么写后日志就不需要了,这样是省了一笔开销了。第二点就是,写后日志不会去阻塞当前的写操作,这样的话也不会影响到redis的性能。当然写后日志是有风险的,如果在执行命令的时候redis宕机了,还来不及写入AOF日志,那么我们就会永久的丢失这条操作了。而且写后日志虽然不会阻塞当前的写操作,但是有可能会阻塞下一条写操作,这也是会对redis的性能有一定的影响。

Redis提供了AOF的三条持久化策略,第一种是每一次命令执行完,就直接将日志写入磁盘。第二种则是命令执行完后,先将日志放到内存中,每隔一秒才将日志写入磁盘。第三种则是命令执行完后,先将日志写入内存,具体什么时候写入到磁盘,要看操作系统的决定。

我们知道,AOF是以文件的形式在记录接受到的命令,那么随着时间的推移,我们的AOF文件肯定会越来越大的,这个时候我们要注意AOF文件过大带来的问题。

讲完了AOF,我们来讲一下RDB。

我们知道AOF是保存的命令,也就意味着如果我们的redis宕机了,那么我们使用AOF文件恢复的时候,必须要一条一条的执行命令,耗费时间太长,还会影响到我们的redis的正常使用,所以redis还提供RDB快照方式来实现持久化。

RDB是内存快照,它会将redis某个时刻的数据状态都记录下来,保存到磁盘。我们知道,在redis6.0之前,redis使用的是单线程模型,但是RDB是将redis的全部数据保存到磁盘,那么必然会带来线程的阻塞,那么为了不阻塞主线程,在实现RDB的时候,我们的redis都是重现fork了一个子进程(注意是进程),专门用于写入RDB文件,从而避免了主线程的阻塞。

面对RDB这种持久化策略,我们要注意的一点是,在对内存数据做快照的时候,redis内存里面的数据是可以修改的吗?如果不可以修改,那就意味着redis的性能下降了,但是如果可以修改,如何实现这个快照的话就非常麻烦了。所以这个时候,redis会借助操作系统提供的写时复制技术,当redis有命令是在执行修改操作的时候,主线程会将这块修改的数据复制一份,生成该数据的一个副本,然后主线程在这个副本上进行修改,当我们的RDB结束了后,再将这块数据写入redis数据库中。

那么,多久进行一次RDB呢?其实这个时间不是很好把握,如果时间太短,频繁的进行磁盘操作和fork子进程出来,势必会影响到redis的性能,但是时间太长,那么如果redis宕机了,丢失的数据就太多了,无法接受。

在redis4.0中提出了一个混合使用AOF日志和内存快照的方法,简单来说,RDB以一定的频率执行,而在两次RDB之间,使用AOF日志记录这期间的所有命令操作,在第二次RDB的时候,会将AOF日志清空。这样的好处是不用频繁的进行RDB操作和fork子进程操作,而且AOF文件也不会变的很大。  

AOF的文件太大,导致的性能问题,有什么解决方案呢?

首先我们要知道,AOF文件过大,会导致什么样的性能问题呢?主要问题是三个方面,第一是文件系统本身对文件大小有限制,无法保存过大的文件。二是如果文件太大,之后再往里面追加命令记录的话,效率也会变低。三是如果redis宕机了,AOF的记录的命令需要一个一个的执行,但是AOF的文件太大,导致的恢复费时间就会很长,会影响到redis的正常使用。

解决方案,就是AOF的重写机制。我们知道,AOF文件是以追加的形式,每当数据库操作一个键值的时候,就会生成一条AOF日志,这样的话,每一个键值的相关命令生成的AOF日志就会特别多。那么解决方案就是和这个相关了,redis会根据自己数据库的现状,创建一个新的AOF文件,然后读取数据库中的所有键值对,然后每一条键值就用一条命令记录它的当前值的写入,这样,就可以把以前所有的关于这个键值的旧的AOF日志,合并为一条AOF日志,这样,AOF就会变小很多。

但是,重写这个过程,是要将整个数据库的最新数据的操作日志都要写到磁盘中的,如果是通过主线程来操作,就会耗时很久,阻塞了主线程,所以redis将这个重写的过程,通过后台子进程bgrewriteaof(注意,这里是进程,不是线程。)来完成的,避免了对主线程的阻塞。

什么时候会触发重写呢?在redis中是有配置项可以进行处理的,一个设置当AOF文件的大小的最小大小的时候的值,默认是64MB。另外还有一个配置是当前AOF文件的大小和上次重写后的AOF文件的大小的差值,再除以上一次重写后文件的大小的值。当AOF的日志大小,同时超过这两个配置的时候,会触发重写。  

redis主从复制数据的最终一致性是怎么实现的?

Redis提供了主从库的模式,为了保证数据副本之间的一致性,主从库之间采用的是读写分离的方式。

首先,redis让主库保持了可读可写,但是从库就只能读不能写,原因是,当客户端对同一个key的数据进行了多次的修改操作,那么每一次操作都给到了不同的库,那么就会导致数据的不一致,redis在读取的时候,可能会读取到旧的值,即便是库与库之间进行数据同步,这种同步的方式也会非常的复杂,实现起来也会影响到redis的性能。

既然redis采用了主库从库的方式,那么主库从库之间的数据是怎么同步的呢?首先,当我们启动多个redis实例的时候,它们相互之间就可以通过relicaof命令形成主库从库的关系,然后从库会告知主库开始进行全量同步,也就是说,主库会将它的所有数据,生成RDB快照,同步到从库,从库就会加载这个RDB文件,将数据生成在内存中。在这个全量同步过程中,主库还在正常运行,所以它会将它收到的写命令,放到一个专属的内存区域replication buffer中,等到从库的全量同步结束,主库会将这个replication buffer区域的命令通知给从库,进行增量同步,后续的访问主库的写命令也是同样如此操作。

而且我们的redis为了缓解主库同时同步好几个从库带来的压力,还设置了主-从-从的模式,也就说,当一个主库同步给一个从库之后,这个从库会将数据同步给另外一个从库,这样避免了主库同时同步几个从库带来的压力,从而带来了redis的性能提升。

但是也要注意的一个问题是,当我们的主库和从库完全全量同步之后,主库从库之间会保持一个长连接,进行增量同步。但是如果因为网络问题链接断掉了,那么主库的一些数据的变更从库肯定就没有同步到。

Redis的解决方案是,将链接断掉后的写操作命令,放到一个repl_backlog_buffer缓冲区中,当链接恢复后,主库会将这个缓冲区的命令同步给从库。但是要注意的是,repl_backlog_buffer是一个环形缓冲区,当链接断开的时间太长,导致这个缓冲区被重写覆盖了,那么当链接重新连接上的时候,主库和从库之间会进行全量同步。

请大概讲一下redis的缓存淘汰机制?

Redis为我们提供了三类缓存淘汰机制,

第一类是就是当redis满了以后,还有访问过来的时候,redis会直接报错,不能再进行操作了。

第二类就是和过期时间有关的,包含的策略大概有volatile-lru,volatile-lfu等等,这些策略的前提是要在设置了过期时间的键值对,进行策略上的删除。

第三类就是在全部数据的基础上,选择数据进行删除,比如说我们经常使用的allkey-lru,allkey-lfu等策略。

一般来说,我们使用的策略是第三类策略中allkey-lru策略,这种策略是将最近最少使用的可以删除。

那么是如何实现这种策略的呢?redis在底层维护了一个链表,链表头是最近使用的数据,链表尾是最近不常用的数据,当链表其中有一条数据刚刚被使用的时候,redis会将这条数据移动到链表头,而其他的数据都会相应的往后移动一个位置。

但是这种实现方案有一个问题,当redis的数量很多的时候,因为需要用链表管理所有的缓存数据,而且有数据被访问的时候,需要移动数据,如果redis的访问很频繁,那么会带来很大的性能损耗。

于是redis对allkey-lru的实现方案进行了优化,redis会在每一条数据中记录他们自己的最近访问时间,然后redis在决定淘汰数据的时候,第一次会随机选择出N条数据,这个数据量是由我们自己配置,redis提供了配置项,接下来,redis会比较这个N个数据的时间,把时间最小的数据从缓存中删除。

当下一次有需要淘汰数据的时候,redis会重新挑选N条数据,但是要注意的是,现在的这N条数据的时间,必须要小于上一次的集合中的最小时间,意思就是说,要选择出更早的没有使用的数据来进行缓存淘汰。

如何保持redis和mysql中的数据一致性?

首先,我们要知道的是,redis和mysql是没法保证强一致性的,只能保证两者的最终一致性。

一般来说,操作都是通过先写mysql,然后去删除redis。当然,如果担心缓存失效导致的性能问题,那么可以先写mysql,然后通过监控binlog的形式,去更新redis。

在查询资料的过程中,看到一个博主的文章,值得参考。 blog.csdn.net/weixin_7073…

缓存穿透,缓存击穿还有缓存雪崩是什么意思?

以下是 Redis 缓存穿透、击穿、雪崩的核心解析及解决方案归纳:

一、缓存穿透(Cache Penetration)

  1. 定义
    缓存与数据库均不存在的数据‌被高频请求(如恶意伪造ID),导致请求穿透缓存层直达数据库,引发压力激增13。

  2. 典型场景

    • 攻击者批量请求非法ID(如负数值、随机字符串)39;
    • 业务逻辑缺陷导致无效查询(如已删除商品ID被反复请求)4。
  3. 解决方案

    • 缓存空值‌:对不存在的数据缓存短时空值(如5分钟),拦截重复请求15;
    • 布隆过滤器‌:前置拦截非法请求(判定数据“可能存在”或“一定不存在”)110;
    • 请求校验‌:业务层拦截非法参数(如ID≤0)510。

二、缓存击穿(Cache Breakdown)

  1. 定义
    热点数据缓存突然失效‌(如过期),瞬时高并发请求穿透至数据库67。

  2. 典型场景

    • 微博热搜缓存过期,海量用户同时刷新8;
    • 秒杀商品缓存失效,抢购请求压垮数据库610。
  3. 解决方案

    • 互斥锁(Mutex Lock) ‌:单线程重建缓存,其他请求阻塞等待1112;
    • 逻辑过期‌:缓存永不过期,异步更新数据(需容忍短暂脏数据)12;
    • 热点数据续期‌:提前刷新TTL,避免集中失效7。

三、缓存雪崩(Cache Avalanche)

  1. 定义
    大规模缓存同时失效‌或‌Redis集群宕机‌,请求洪峰压垮数据库67。

  2. 典型场景

    • 电商首页数据午夜批量过期713;
    • Redis主从切换导致服务不可用14。
  3. 解决方案

    • 分散过期时间‌:基础TTL + 随机偏移值(如30s±10s)1314;
    • 高可用架构‌:Redis Cluster分片部署 + 哨兵故障转移714;
    • 熔断降级‌:数据库压力激增时返回默认值(如Hystrix熔断)14。

什么是布隆过滤器

布隆过滤器是一种‌空间效率极高‌的概率型数据结构,用于判断一个元素是否可能存在于一个集合中。它由Burton Howard Bloom于1970年提出,在缓存系统、数据库等领域有广泛应用。

一 数据结构组成

  1. 位数组‌:长度为m的二进制向量(初始所有位为0)
  2. 哈希函数集合‌:k个相互独立且均匀分布的哈希函数(h₁, h₂, ..., hₖ)

操作流程

操作过程描述示例(假设k=3)
添加元素1. 对元素进行k次哈希计算 2. 将位数组中对应位置设为1添加"A":h₁(A)=3, h₂(A)=7, h₃(A)=12 ⇒ 位数组[3][7][12]=1
查询元素1. 对元素进行k次哈希计算 2. 检查所有对应位置是否均为1 全部为1 → ‌可能存在任一为0 → ‌肯定不存在查询"B":h₁(B)=3, h₂(B)=8, h₃(B)=12 ⇒ 位[8]为0 → B不存在

优势

  1. 空间效率极高‌:存储1亿元素仅需约100MB(传统哈希表需GB级)
  2. 查询速度快‌:时间复杂度O(k),与数据量无关
  3. 绝对准确性(不存在时) ‌:若判定元素不存在,则100%不存在

局限

  1. 误判率(假阳性) ‌:

    • 可能错误判定不存在元素为存在
    • 误判率公式:(1-e^(-kn/m))^k
  2. 不支持删除‌:传统布隆过滤器删除元素会破坏其他元素的判断

  3. 无法获取实际数据‌:仅能判断存在性,不能获取元素内容

误判率控制因素

参数与误判率关系优化建议
位数组大小(m)↑ m增大 → ↓误判率降低根据数据规模n和容忍误判率p计算: m = - (n * ln p) / (ln 2)^2
哈希函数数(k)最优值 ≈ (m/n)*ln2公式:k = (m/n) * ln 2
数据规模(n)↑ n增大 → ↑误判率升高预估最大数据量设计容量

示例配置‌:存储1亿元素(p=1%)需约958MB空间(k=7),传统哈希表需约3.2GB

缓存系统防护

mermaidCopy Code
graph LR
A[客户端请求] --> B{布隆过滤器}
B -- 判断存在? -->|是| C[查询缓存]
B -- 判断不存在 -->|否| D[直接返回空]
C --> E{缓存命中?}
E -- 是 --> F[返回缓存数据]
E -- 否 --> G[查询数据库]

其他典型场景

  1. 爬虫URL去重‌:判断URL是否已爬取(容忍少量重复)

  2. 邮件系统垃圾过滤‌:快速识别已知垃圾邮件地址

  3. 分布式系统‌:

    • Cassandra:判断SSTable中是否包含特定key
    • Google Bigtable:避免查找不存在的行/列
  4. 区块链‌:以太坊使用布隆过滤器快速筛选交易

变种与改进

类型解决痛点实现方式
计数布隆过滤器不支持删除每个桶使用计数器而非二进制位
布谷鸟过滤器更高空间利用率结合两个哈希表和布谷鸟散列机制
动态扩容布隆过滤器固定容量限制分层设计,溢出时新增布隆过滤器层

最佳实践建议

  1. 适用场景‌:海量数据存在性判断 + 允许可控误判率

  2. 避免场景‌:

    • 要求100%准确性的关键业务
    • 需要获取元素实际数据的场景
  3. 参数调优‌:根据公式计算最佳m和k值

  4. 组合策略‌:结合白名单(确保关键数据0误判)

Redis实现‌:可通过Redis的BITFIELD命令构建分布式布隆过滤器,或使用Redisson等库内置的实现。

Redis 集群数据倾斜怎么处理?

redis的数据倾斜的原因有三种。

第一种是有由于bigkey导致的数据量倾斜问题。由于bigkey本身的数据量很大,如果存储到一个实例中,那么就会导致这个实例的数据比其他实例要大很多。

因此我们最好在使用redis的时候,如果有bigkey的情况,那么就将bigkey拆分为多条数据插入,这样就能避免数据倾斜的产生。

第二种是Slot槽的分配不均匀。比如使用Redis Cluster方案进行集群方案,那么Redis Cluster中有16384个Slot,存储到redis的数据会被映射到这些Slot上面,进而存储到和这些Slot绑定的实例上。也因此,我们的redis在创建之初,就应该注意Slot的绑定要均匀。

第三种就是Hash Tag导致的。有的时候我们需要将一些数据,都放到同一个实例上,方便我们进行范围查询和事务操作,这个时候,我们一般通过Hash Tag的方案做这个事情,也就是说,我们会在key上使用花括号{},花括号{}里面的值是一样的,那么就会映射到同一个Slot上,数据会存储到同一个实例上。这种数据倾斜产生的原因,是人为特地造成的,需要我们自己去做取舍。

使用redis实现消息队列

1. List实现方案

核心命令

  • 生产者:LPUSH queue_name message
  • 消费者:BRPOP queue_name timeout

特性

  • 单消费者模式
  • 阻塞式读取(避免轮询)
  • 消息持久化支持
  • 典型TPS:约10万/秒(单机)

2. Pub/Sub实现方案

核心命令

  • 发布:PUBLISH channel message
  • 订阅:SUBSCRIBE channel1 channel2

特性

  • 实时广播机制
  • 无消息堆积能力
  • 客户端断线即丢失消息
  • 适合在线状态通知

3. Stream实现方案(推荐)

核心命令

# 生产
XADD mystream * field1 value1 field2 value2
# 消费组
XGROUP CREATE mystream mygroup $
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >

核心特性

  1. 消息回溯:XREAD STREAMS mystream 0
  2. 消费确认:XACK mystream mygroup id
  3. 死信处理:XPENDING mystream mygroup
  4. 典型TPS:约8万/秒(单机)

二、生产环境最佳实践

1. 高可用配置

  • 启用Redis持久化(AOF+RDB)
  • 部署Redis Cluster避免单点故障
  • 设置合理的内存淘汰策略

2. 监控指标

# 关键监控项
queue_length = LLEN(queue_name)
pending_count = XLEN(stream_name)
consumer_lag = XINFO GROUPS stream_name

3. 常见问题解决方案

  • 消息丢失:启用AOF持久化+定期RDB快照
  • 重复消费:使用Stream的message ID去重
  • 积压处理:动态增加消费者数量
  • 延迟消息:Sorted Set+定时扫描