Redis面试知识点汇总

Saturday, May 22, 2021 | 13 | Saturday, May 22, 2021

@

Redis面试知识点汇总 ⑅︎◡̈︎*

 
——> 全文约 5500 字 <——

Redis是什么

Redis是C语言开发的一个开源的(遵从BSD协议)高性能键值对(Key-Value)的内存数据库,可以用作数据库、缓存、消息中间件等。它是一种NoSQL数据库(Not-Only SQL,泛指非关系型数据库。关系型数据库通过二维表实现,安全性高但是 灵活性高并发性能 差)。

Redis的优点

  • 性能优秀,数据在内存中,读写速度非常快,支持并发10W QPS(每秒查询率);
  • 单进程单线程,是线程安全的,采用IO多路复用机制(在同一个线程内可以同时处理多个IO请求);
  • 丰富的数据类型,支持字符串(Strings)、散列(Hashes)、列表(Lists)、集合(Sets)、有序集合(Sorted Sets)等;
  • 支持数据持久化。可以将内存中数据保存在磁盘中,重启时加载;
  • 主从复制,哨兵,高可用;
  • 可以用作分布式锁;
  • 可以作为消息中间件使用,支持发布订阅

Redis应用场景

  • 热点数据的缓存:由于Redis访问速度块、支持的数据类型比较丰富,所以redis很适合用来存储热点数据
  • 限时业务:限时优惠、手机验证码等(可以给key设置过期时间)
  • 计数器(String实现):如电商网站商品的浏览量、视频网站视频的播放数等,并发量高时如果每次都请求数据库操作容易使数据库压力过大。Redis提供的 incrby 命令可以实现计数器功能,也可以限制一个手机号发多少条短信、一个接口一分钟限制多少请求等等
  • 排行榜(Zset实现,有序集):Redis提供的有序集合数据结构能实现各种复杂的排行榜应用
  • 社交网络:点赞数(String实现) / 共同好友(Set实现,无序集) / 关注量,使用关系型数据库储存不方便,Redis的Hash、Set等数据结构可以实现这些功能
  • 延时操作:购物时的付款剩余时长(也是利用key的过期时间)
  • 分布式锁:很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能,因此可以利用Redis的分布式锁

根据各种数据类型的特点判断应用场景:

  • String是字符串,可以用于计数器/分布式锁/存放session
  • List是双向链表,可以按照时间顺序储存数据 / 消息队列
  • Hash是键值对集合,可以用于储存个人信息
  • Set是无序集,可以用于社交网络中取交集操作(共同好友)/ 随机展示 / 存放黑名单
  • Zset是有序集,可以用于排行榜或者与时间线相关的

Redis数据类型及底层实现

Redis支持五种数据类型:

  • String
  • List(底层实现:双向链表)
  • Hash(底层实现:字典)
  • Set(无序集合)
  • Zset(Sorted Set,有序集合,底层实现:跳跃表ZSkipList(实现方式之一))

N.B. 尽管以上数据类型都有各自的底层实现方式,但是还可以使用 ZipList(压缩列表) 进行实现,因为 ZipList 可以节省内存空间,详细内容参见 Redis源码分析-压缩列表ZipList ,Redis的数据类型和底层实现的对应关系如下图所示:

Zset的底层实现:ZipList(压缩列表) / SkipList(跳跃表)

当Zset满足以下两个条件的时候,使用ZipList,否则使用SkipList:

  • 保存的元素少于128个
  • 保存的所有元素大小都小于64字节

SkipList 跳跃表:双链表的基础上增加多级的索引结构,参见 Redis数据结构-跳跃表

Redis的持久化:RDB / AOF

Redis默认使用RDB做持久化,但是建议采用AOF方式

  • RDB(Redis DataBase):在不同的时间点,将Redis存储的数据生成快照并存储到磁盘等介质上
  • AOF(Append-Only File):将执行过的 写指令 记录下来(默认每秒记录一次),在数据恢复时按照从前到后的顺序再将指令都执行一遍
RDBAOF
优点1. 体积小,非常适用于备份,全量复制等场景
2. Redis加载RDB恢复数据远远快于AOF
1. 秒级持久化,每次最多丢失一秒数据
2. 速度快,只进行追加
3. 先执行再记录 @
4. 只要未对AOF进行 rewrite 就可以恢复AOF到误操作前的状态
5. 支持重写
缺点1. 没办法做到实时持久化/秒级持久化(属于重量级操作)
2. Redis不同版本的BDB无法相互兼容(储存格式不同)
1. 体积比RDB大
2. 数据恢复慢(需要把所有指令执行一遍)

@ 先执行再记录的好处是,不需要检查指令的语法,因为只有成功执行才会被记录到日志中,且当前的执行过程不会阻塞当前的AOF操作(当磁盘的写入压力大的时候,写过程可能会影响下一个执行操作)

AOF三种写回策略:Always / Everysec / No

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;

    可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能。

  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;

    采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中

  • No,操作系统控制写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

    只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了。

AOF重写机制

AOF重写机制指的是,对过大的AOF文件进行重写,以此来压缩AOF文件的大小。

  1. Redis根据数据库的现状创建一个新的 AOF 文件
  2. 将多条操作记录变成一条记录保存在新的AOF文件中,原AOF文件继续工作
  3. 重写完后新的指令追加到新的AOF文件中

Redis的主从复制:master / slave

  • 主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。
  • 从数据库一般是只读的,并接受主数据库同步过来的数据。
  • 一个主数据库可以拥有多个从数据库,一个从数据库只能拥有一个主数据库。

作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。
  • 读写分离:可以用于实现读写分离,主库写、从库读,不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量;
  • 负载均衡:配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即Redis数据时连接主节点,Redis数据时连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用的基础:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

Redis Sentinel 哨兵(宕机后如何快速恢复)

Redis Sentinel(哨兵)是一个分布式的架构,它本身也是一个独立的Redis节点,只不过它不存储数据,只支持部分命令,它能够自动完成故障发现和故障转移,并通知应用方,从而实现高可用。

工作原理:

  1. 状态感知:哨兵需要知道每个master对应的slave信息(即知道整个集群完整的拓扑结构)

  2. 心跳检测:每个哨兵节点每隔1秒向 master / slave / 其他哨兵节点 发送ping命令,如果对方能在指定时间内响应,说明节点健康。否则该哨兵节点认为此节点主观下线(为什么叫主观下线?因为ping失败有可能是网络故障而非节点故障,确定某个master节点是否故障需要多个哨兵节点共同确认)。

  3. 选举哨兵领导者:确认某个master节点真正故障后,就需要进入到故障恢复阶段。选择出哨兵领导者后(通过某种选举协商算法选出),之后的故障恢复操作都由这个哨兵领导者进行操作。

  4. 选择新的master:在故障的master节点的slave节点中选取一个节点为新的master。

  5. 提升新的master:

    • 选出新的master节点
    • 给slave节点发送命令,给它们指定新的master节点
    • 把故障节点降为slave节点,若故障修复,自动成为新的master的slave
    • 客户端感知新master:集群中的其余客户端获取最新的master的地址。

Redis缓存问题

  1. 缓存穿透:大面积请求的数据(多个数据)不存在于数据库和缓存中,就会一直查询数据库,导致数据库访问压力激增而崩溃。解决方案:

    • 接口校验:在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤,或者用户ID小于0直接过滤等等

    • 缓存空对象:当请求一个缓存和数据库中都不存在的数据,第一次请求就会跳过缓存进行数据库的访问,并且访问数据库后返回为空,此时也将该空对象进行缓存。若是再次进行访问该空对象的时候,就会直接访问缓存,而不是数据库(实现简单,但是缓存中存在很多空对象,占用内存,解决方案是给这些空对象设置较短的过期时间)

    • 布隆过滤器:一种基于概率的数据结构,用来判断某个元素是否在集合内。用布隆过滤器存储所有数据库中可能访问的key,不存在的key直接被过滤,存在的key则进一步查询

      优点缺点
      1. 运行速度快(时间效率)
      2. 占用内存小(空间效率)
      3. 不会漏报(判断不存在则一定不存在)
      1. 可能误报(判断存在有可能实际不存在,解决办法是扩大布隆过滤器长度或者增加哈希函数的数量)
      2. 不能删除元素
  2. 缓存击穿单个数据承受大并发的集中访问。当key失效时,大并发直接穿过缓存请求数据库导致数据库访问压力激增而崩溃。其原因有两个:

    • 冷门数据:该数据没有人查询过 ,缓存中不存在,第一次就大并发的访问
    • 热点数据:添加到了缓存,但是大并发访问的时候key失效了

    解决方案:

    • 查询缓存/数据库过程中加互斥锁(分布式环境使用分布式锁),只能第一个进来的请求执行,当第一个请求把该数据放进缓存中,接下来的访问就集中访问缓存
    • 热点数据不设置失效时间
  3. 缓存雪崩:某一个时间段,缓存集中过期失效,数据请求绕开缓存集中访问数据库。原因有两个:

    • Redis宕机
    • 数据集中失效(key在同一个时间点失效,比如天猫双11,马上就要到双11零点,很快就会迎来一波抢购,这波商品在23点集中的放入了缓存,假设缓存一个小时,那么到了凌晨24点的时候,这批商品的缓存就都过期了)

    解决方案:

    • 搭建高可用集群,防止Redis宕机
    • 把每个Key的失效时间都加个随机值,保证数据不会大面积同时失效。

Redis分布式锁

分布式锁的目的,就是为了保证多台服务器在执行某一段代码时只有一台服务器执行

  • 分布式锁的实现需要同时满足以下几点:

    1. 互斥性。在任何时刻,保证只有一个客户端持有锁。
    2. 不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
    3. 保证上锁和解锁都是同一个客户端
  • 实现分布式锁的方式:

    • 使用MySQL,基于唯一索引。
    • 使用ZooKeeper,基于临时有序节点。
    • 使用Redis,基于 setnx 命令(SET if not exists)。

企业的生产环境中用分布式锁的时候,一定会使用开源的类/库,Redis分布式锁一般使用Redisson框架。

  • 实现思路(通过 lua 脚本实现,复杂的业务逻辑可以封装在lua脚本中发送给Redis,保证执行过程的原子性):

    1. 加锁setnx key value ,其中value用于标识加锁的客户端(为了防止持有锁的客户端崩溃了陷入死锁,加锁时还要设置有效时间,超时锁自动失效)。
      • 如果key不存在,则设置value,加锁成功;
      • 如果key存在,说明有客户端加锁了,此时加锁失败。
    2. 解锁:校验value值 → 释放锁 → 删除Redis中的键值对(value能保证加锁和解锁都是同一个客户端)。
    3. 锁的续期:加锁过程中我们设置了有效时长,但是有时业务代码执行时间会大于有效时间,则需要对锁进行续期,保证业务代码正常执行。使用Redisson框架时,默认加锁时长是30秒,加锁成功时同时激活一个定时任务(Watchdog ,看门狗),它会定期检查业务执行状态,如果发现未执行完成,则在第10秒时进行续期,把有效时长重置为30秒(如果该客户端崩溃宕机了,那么定时任务跑不动,无法进行续期,时间一到自然解锁了,详细内容请 跳转浏览 )。
    4. 可重入锁:在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法A,该方法中又调用了另一个需要相同锁的方法B,则该线程可以直接执行调用的方法,而无需重新获得锁。
      • 在Redisson中,加锁成功后会记录重入次数,每次重入锁该次数+1;
      • 每释放一次分布式锁,重入次数-1,当重入次数为0说明该客户端不持有锁,删除Redis中键值对,从而释放该资源。
    5. 订阅消息:如果加锁失败的情况下,不可能一直轮询尝试加锁,直到加锁成功为止,这样太过耗费性能,所以需要利用订阅的机制进行优化。
      • 当加锁失败后,订阅锁释放的消息,自身进入阻塞状态;
      • 当持有锁的客户端释放锁的时候,发布锁释放的消息;
      • 当进入阻塞等待的其他客户端收到锁释放的消息后,解除阻塞等待状态,再次尝试加锁。
  • 缺点:客户端A在对某个主服务器master加锁的时候,数据会异步复制给对应的从服务器slave,假设在还没有复制过去的时候master宕机,slave选举成为新的master,客户端B可以进行加锁。此时两个客户端分别在两个服务器对同一个数据进行加锁,这时候就会产生脏数据(简而言之就是,Redis的master宕机时,由于主从异步复制,可能导致多个客户端同时完成加锁)。

更多相关内容可以点击 面试官想要你回答的分布式锁实现原理怎样实现Redis分布式锁

Redis集群

(暂不了解,有空补上)

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。

记住三主三从,节点间用Gossip协议以meet形式通信

Redis与MySQL(缓存与数据库)一致性

写入数据的时候,缓存和数据库都需要进行修改,但是两者必然存在先后顺序,过程中可能导致缓存与数据库的不一致,此时需要考虑两个问题:

  1. 顺序问题:先操作数据库还是缓存
  2. 策略问题:更新缓存还是删除缓存
    • 更新缓存:把更新操作放到缓存中执行,不会出现 cache miss 的情况,但是相对于删除缓存的运算消耗更大(且有可能造成数据不一致
    • 删除缓存:直接删除缓存的数据,但是下一次查询无法在缓存中读取数据,会有一次 cache miss (但是最多也只会有一次读取失败,因此选择删除缓存更好

因此可以分为两种方案(两种方案都有可能出现暂时的数据不一致):

  1. 先删除缓存,再更新数据库
    • A线程删除缓存,并且写入数据库;
    • B线程发现缓存无数据,从数据库读数据,<并且把旧数据写入缓存>;
    • <旧数据写入缓存>后,在缓存过期之前Redis储存的一直都是脏数据。
      • 解决方法是 延时双删 ,A线程写入数据库之后经过一段休眠时间再次删除缓存(该方法优化的方式是 异步淘汰 ,把休眠时间和再次删除缓存交给C线程执行);
      • 如果再次删除失败,还可以引入 重试机制 ,报错并不断重试直到成功执行。
  2. 先更新数据库,再删除缓存
    • A线程写入数据库,并且删除缓存;
    • B现场在A线程写入数据库过程中读了旧数据,出现暂时的数据不一致。

参阅 如何保证缓存(Redis)与数据库(MySQL)的一致性

相关链接

  1. Redis面试题

本文结束。

© 2020 - 2026 Kays Blog ⑅︎◡̈︎*

🌱 Powered by Hugo with theme Dream.

憨批の自我介绍
🍺 Kayman' Blog 🏎️

这个博客用来记录一些生活&学习上的事情 ⑅︎◡̈︎*

一名憨憨CRUD专家,现就职于Shopee ,数据鸡架的 Flink 方向

业余时间只剩下 羽毛球🏸️|篮球🏀|德州♠️|打游戏🎮|肥宅🥤 了……

-- 2022 年 02 月 08 日更新 --

一名憨憨CRUD专家,现即将从就读于 🏫 北大青鸟毕业

业余时间会做开源和一些别的项目啥也不会做

目前准备去1075了,顺便捣鼓些Geek Proj,养老的同时培养点兴趣驱使方向。


在北大最好的朋友是 江栽花 ,我们一个负责吹牛皮一个负责去 GayHub 偷别人的优秀代码,完成过很多烂番茄项目,搞过一段时间的磕盐。

尽管也没人认识我们,但是还是客套地说一下

-- 2021 年 06 月 18 日更新 --
技术栈 & Project

主要的技术栈是:

  • 目前工作的方向是 Flink ˙Ꙫ˙

  • Java ˙Ꙫ˙

  • Python ˙Ꙫ˙

  • SQL ˙Ꙫ˙

  • 一点点的 html + css + js 🤏

  • 一点点的 shell 🤏


做过啥:

(其实啥也没做过 ⑅︎◡̈︎*)

友链 :💬 I will put some of my friends' and technical Dalaos' blogs HERE so as to encourage myself (umm though it may not work hahah~) 📣 😤
⑅︎◡̈︎*

My BEST FRIEND in PKU, so l list her at the first row.

I modify my front-end framework from him. List him here so as to thx him~
加载每日一言中...