Redis面试知识点汇总 ⑅︎◡̈︎*
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):将执行过的
写指令记录下来(默认每秒记录一次),在数据恢复时按照从前到后的顺序再将指令都执行一遍
| RDB | AOF | |
|---|---|---|
| 优点 | 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文件的大小。
- Redis根据数据库的现状创建一个新的 AOF 文件
- 将多条操作记录变成一条记录保存在新的AOF文件中,原AOF文件继续工作
- 重写完后新的指令追加到新的AOF文件中
Redis的主从复制:master / slave
- 主数据库可以进行读写操作,当写操作导致数据变化时会自动将数据同步给从数据库。
- 从数据库一般是只读的,并接受主数据库同步过来的数据。
- 一个主数据库可以拥有多个从数据库,一个从数据库只能拥有一个主数据库。
作用:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。
- 读写分离:可以用于实现读写分离,主库写、从库读,不仅可以提高服务器的负载能力,同时可根据需求的变化,改变从库的数量;
- 负载均衡:配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时连接主节点,读Redis数据时连接从节点),分担服务器负载。尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用的基础:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
Redis Sentinel 哨兵(宕机后如何快速恢复)
Redis Sentinel(哨兵)是一个分布式的架构,它本身也是一个独立的Redis节点,只不过它不存储数据,只支持部分命令,它能够自动完成故障发现和故障转移,并通知应用方,从而实现高可用。
工作原理:
状态感知:哨兵需要知道每个master对应的slave信息(即知道整个集群完整的拓扑结构)
心跳检测:每个哨兵节点每隔1秒向
master / slave / 其他哨兵节点发送ping命令,如果对方能在指定时间内响应,说明节点健康。否则该哨兵节点认为此节点主观下线(为什么叫主观下线?因为ping失败有可能是网络故障而非节点故障,确定某个master节点是否故障需要多个哨兵节点共同确认)。选举哨兵领导者:确认某个master节点真正故障后,就需要进入到故障恢复阶段。选择出哨兵领导者后(通过某种选举协商算法选出),之后的故障恢复操作都由这个哨兵领导者进行操作。
选择新的master:在故障的master节点的slave节点中选取一个节点为新的master。
提升新的master:
- 选出新的master节点
- 给slave节点发送命令,给它们指定新的master节点
- 把故障节点降为slave节点,若故障修复,自动成为新的master的slave
- 客户端感知新master:集群中的其余客户端获取最新的master的地址。
Redis缓存问题
缓存穿透:大面积请求的数据(多个数据)不存在于数据库和缓存中,就会一直查询数据库,导致数据库访问压力激增而崩溃。解决方案:接口校验:在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤,或者用户ID小于0直接过滤等等
缓存空对象:当请求一个缓存和数据库中都不存在的数据,第一次请求就会跳过缓存进行数据库的访问,并且访问数据库后返回为空,此时也将该空对象进行缓存。若是再次进行访问该空对象的时候,就会直接访问缓存,而不是数据库(实现简单,但是缓存中存在很多空对象,占用内存,解决方案是给这些空对象设置较短的过期时间)
布隆过滤器:一种基于概率的数据结构,用来判断某个元素是否在集合内。用布隆过滤器存储所有数据库中可能访问的key,不存在的key直接被过滤,存在的key则进一步查询
优点 缺点 1. 运行速度快(时间效率)
2. 占用内存小(空间效率)
3. 不会漏报(判断不存在则一定不存在)1. 可能误报(判断存在有可能实际不存在,解决办法是扩大布隆过滤器长度或者增加哈希函数的数量)
2. 不能删除元素
缓存击穿:单个数据承受大并发的集中访问。当key失效时,大并发直接穿过缓存请求数据库导致数据库访问压力激增而崩溃。其原因有两个:- 冷门数据:该数据没有人查询过 ,缓存中不存在,第一次就大并发的访问
- 热点数据:添加到了缓存,但是大并发访问的时候key失效了
解决方案:
- 查询缓存/数据库过程中加互斥锁(分布式环境使用分布式锁),只能第一个进来的请求执行,当第一个请求把该数据放进缓存中,接下来的访问就集中访问缓存
- 热点数据不设置失效时间
缓存雪崩:某一个时间段,缓存集中过期失效,数据请求绕开缓存集中访问数据库。原因有两个:- Redis宕机
- 数据集中失效(key在同一个时间点失效,比如天猫双11,马上就要到双11零点,很快就会迎来一波抢购,这波商品在23点集中的放入了缓存,假设缓存一个小时,那么到了凌晨24点的时候,这批商品的缓存就都过期了)
解决方案:
- 搭建高可用集群,防止Redis宕机
- 把每个Key的失效时间都加个随机值,保证数据不会大面积同时失效。
Redis分布式锁
分布式锁的目的,就是为了保证多台服务器在执行某一段代码时只有一台服务器执行。
分布式锁的实现需要同时满足以下几点:
- 互斥性。在任何时刻,保证只有一个客户端持有锁。
- 不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
- 保证上锁和解锁都是同一个客户端。
实现分布式锁的方式:
- 使用MySQL,基于唯一索引。
- 使用ZooKeeper,基于临时有序节点。
- 使用Redis,基于
setnx命令(SET if not exists)。
企业的生产环境中用分布式锁的时候,一定会使用开源的类/库,Redis分布式锁一般使用Redisson框架。
实现思路(通过
lua脚本实现,复杂的业务逻辑可以封装在lua脚本中发送给Redis,保证执行过程的原子性):- 加锁:
setnx key value,其中value用于标识加锁的客户端(为了防止持有锁的客户端崩溃了陷入死锁,加锁时还要设置有效时间,超时锁自动失效)。- 如果key不存在,则设置value,加锁成功;
- 如果key存在,说明有客户端加锁了,此时加锁失败。
- 解锁:校验value值 → 释放锁 → 删除Redis中的键值对(value能保证加锁和解锁都是同一个客户端)。
- 锁的续期:加锁过程中我们设置了有效时长,但是有时业务代码执行时间会大于有效时间,则需要对锁进行续期,保证业务代码正常执行。使用Redisson框架时,默认加锁时长是30秒,加锁成功时同时激活一个定时任务(
Watchdog,看门狗),它会定期检查业务执行状态,如果发现未执行完成,则在第10秒时进行续期,把有效时长重置为30秒(如果该客户端崩溃宕机了,那么定时任务跑不动,无法进行续期,时间一到自然解锁了,详细内容请 跳转浏览 )。 - 可重入锁:在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法A,该方法中又调用了另一个需要相同锁的方法B,则该线程可以直接执行调用的方法,而无需重新获得锁。
- 在Redisson中,加锁成功后会记录重入次数,每次重入锁该次数+1;
- 每释放一次分布式锁,重入次数-1,当重入次数为0说明该客户端不持有锁,删除Redis中键值对,从而释放该资源。
- 订阅消息:如果加锁失败的情况下,不可能一直轮询尝试加锁,直到加锁成功为止,这样太过耗费性能,所以需要利用订阅的机制进行优化。
- 当加锁失败后,订阅锁释放的消息,自身进入阻塞状态;
- 当持有锁的客户端释放锁的时候,发布锁释放的消息;
- 当进入阻塞等待的其他客户端收到锁释放的消息后,解除阻塞等待状态,再次尝试加锁。
- 加锁:
缺点:客户端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(缓存与数据库)一致性
写入数据的时候,缓存和数据库都需要进行修改,但是两者必然存在先后顺序,过程中可能导致缓存与数据库的不一致,此时需要考虑两个问题:
- 顺序问题:先操作数据库还是缓存
- 策略问题:更新缓存还是删除缓存
- 更新缓存:把更新操作放到缓存中执行,不会出现
cache miss的情况,但是相对于删除缓存的运算消耗更大(且有可能造成数据不一致 ) - 删除缓存:直接删除缓存的数据,但是下一次查询无法在缓存中读取数据,会有一次
cache miss(但是最多也只会有一次读取失败,因此选择删除缓存更好)
- 更新缓存:把更新操作放到缓存中执行,不会出现
因此可以分为两种方案(两种方案都有可能出现暂时的数据不一致):
- 先删除缓存,再更新数据库
- A线程删除缓存,并且写入数据库;
- B线程发现缓存无数据,从数据库读数据,<并且把旧数据写入缓存>;
- <旧数据写入缓存>后,在缓存过期之前Redis储存的一直都是脏数据。
- 解决方法是
延时双删,A线程写入数据库之后经过一段休眠时间再次删除缓存(该方法优化的方式是异步淘汰,把休眠时间和再次删除缓存交给C线程执行); - 如果再次删除失败,还可以引入
重试机制,报错并不断重试直到成功执行。
- 解决方法是
- 先更新数据库,再删除缓存
- A线程写入数据库,并且删除缓存;
- B现场在A线程写入数据库过程中读了旧数据,出现暂时的数据不一致。
参阅 如何保证缓存(Redis)与数据库(MySQL)的一致性 。