Redis 面试题总结

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

Redis是什么

Redis 是一个 key-value 存储系统它支持存储的 value 类型相对更多包括 string、list、set、zsetsorted set --有序集合和 hash。这些数据结构都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作而且这些操作都是原子性的。在此基础上Redis 支持各种不同方式的排序。为了保证效率数据都是缓存在内存中Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件并且在此基础上实现了master-slave主从同步。


Redis 都有哪些使用场景

  • 缓存这应该是 Redis 最主要的功能了也是大型网站必备机制合理地使用缓存不仅可以加 快数据的访问速度而且能够有效地降低后端数据源的压力。
  • 共享Session对于一些依赖 session 功能的服务来说如果需要从单机变成集群的话可以选择 redis 来统一管理 session。
  • 消息队列系统消息队列系统可以说是一个大型网站的必备基础组件因为其具有业务 解耦、非实时业务削峰等特性。Redis提供了发布订阅功能和阻塞队列的功 能虽然和专业的消息队列比还不够足够强大但是对于一般的消息队列功 能基本可以满足。比如在分布式爬虫系统中使用 redis 来统一管理 url队列。
  • 分布式锁在分布式服务中。可以利用Redis的setnx功能来编写分布式的锁虽然这个可能不是太常用。 当然还有诸如排行榜、点赞功能都可以使用 Redis 来实现但是 Redis 也不是什么都可以做比如数据量特别大时不适合 Redis我们知道 Redis 是基于内存的虽然内存很便宜但是如果你每天的数据量特别大比如几亿条的用户行为日志数据用 Redis 来存储的话成本相当的高。

Redis为什么这么快

在这里插入图片描述

  1. 基于内存存储实现
    我们都知道内存读写是比在磁盘快很多的Redis基于内存存储实现的数据库相对于数据存在磁盘的MySQL数据库省去磁盘I/O的消耗。

  2. 高效的数据结构
    我们知道MySQL 索引为了提高效率选择了 B+ 树的数据结构。其实合理的数据结构就是可以让你的应用/程序更快。先看下 Redis 的数据结构&内部编码图
    在这里插入图片描述
    SDS简单动态字符串
    在这里插入图片描述

  • 字符串长度处理Redis获取字符串长度时间复杂度为O(1)而C语言中需要从头开始遍历复杂度为On;
  • 空间预分配字符串修改越频繁的话内存分配越频繁就会消耗性能而SDS修改和空间扩充会额外分配未使用的空间减少性能损耗。
  • 惰性空间释放SDS 缩短时不是回收多余的内存空间而是free记录下多余的空间后续有变更直接使用free中记录的空间减少分配。
  • 二进制安全Redis可以存储一些二进制数据在C语言中字符串遇到’\0’会结束而 SDS中标志字符串结束的是len属性。
  1. 合理的数据编码
    Redis 支持多种数据数据类型每种基本类型可能对多种数据结构。什么时候,使用什么样数据结构使用什么样编码是 redis 设计者总结优化的结果。
  • String如果存储数字的话是用int类型的编码;如果存储非数字小于等于39字节的字符串是embstr大于 39 个字节则是 raw 编码。
  • List如果列表的元素个数小于 512 个列表每个元素的值都小于 64 字节默认使用 ziplist 编码否则使用 linkedlist 编码
  • Hash哈希类型元素个数小于512 个所有值小于 64 字节的话使用ziplist编码,否则使用 hashtable 编码。
  • Set如果集合中的元素都是整数且元素个数小于 512 个使用 intset 编码否则使用 hashtable 编码。
  • Zset当有序集合的元素个数小于 128 个每个元素的值小于 64 字节时使用 ziplist 编码否则使用skiplist跳跃表编码

3.4 合理的线程模型
I/O 多路复用
在这里插入图片描述
多路I/O复用技术可以让单个线程高效的处理多个连接请求而Redis使用用epoll作为I/O多路复用技术的实现。并且Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件不在网络I/O上浪费过多的时间。

什么是I/O多路复用
I/O 网络 I/O
多路 多个网络连接
复用复用同一个线程。
IO多路复用其实就是一种同步 IO 模型它实现了一个线程可以监视多个文件句柄一旦某个文件句柄就绪就能够通知应用程序进行相应的读写操作而没有文件句柄就绪时,就会阻塞应用程序交出 cpu。

单线程模型
Redis是单线程模型的而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。也正因为是单线程如果某个命令执行过长如hgetall命令会造成阻塞。
Redis是面向快速执行场景的数据库所以要慎用如 smembers 和 lrange、hgetall 等命令。
Redis 6.0 引入了多线程提速它的执行命令操作内存的仍然是个单线程。

  1. 虚拟内存机制
    Redis直接自己构建了VM机制 不会像一般的系统会调用系统函数处理会浪费一定的时间去移动和请求。

Redis的虚拟内存机制是啥呢

虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过 VM 功能可以实现冷热数据分离使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。


Redis支持的数据类型

String字符串

格式: set key value、get key

string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型一个键最大能存储512MB。
应用场景共享session、分布式锁计数器、限流。
内部编码有3种int8字节长整型/embstr小于等于39字节字符串/raw大于39个字节字符串
C语言的字符串是char[]实现的而Redis使用SDSsimple dynamic string 封装sds源码如下

struct sdshdr{ unsigned int len; // 标记buf的长度 unsigned int free; //标记buf中未使用的元素个数 char buf[]; // 存放元素的坑 }

SDS 结构图如下
在这里插入图片描述
Redis为什么选择SDS结构而C语言原生的char[]不香吗
举例其中一点SDS中O(1) 时间复杂度就可以获取字符串长度而 C 字符串需要遍历整个字符串时间复杂度为 O(n)

Hash哈希

格式: hset key field value 、hget key field

Redis hash 是一个键值 ( key=>value ) 对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表hash 特别适合用于存储对象。

List列表

Redis 列表是简单的字符串列表按照插入顺序排序。你可以添加一个元素到列表的头部左边或者尾部右边

格式: lpush name value
在 key 对应 list 的头部添加字符串元素

格式: rpush name value
在 key 对应 list 的尾部添加字符串元素

格式: lrem name index
key 对应 list 中删除 count 个和 value 相同的元素

格式: llen name
返回 key 对应 list 的长度

Set集合

格式: sadd name value

Redis的Set是string类型的无序集合。
集合是通过哈希表实现的所以添加删除查找的复杂度都是 O(1)。

zset (sorted set有序集合)
格式: zadd name score value

zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。

zset的成员是唯一的,但分数(score)却可以重复。

在这里插入图片描述


Redis为什么是单线程的

  • 代码更清晰处理逻辑更简单
  • 不用考虑各种锁的问题不存在加锁和释放锁的操作没有因为可能出现死锁而导致的性能问题
  • 不存在多线程切换而消耗 CPU
  • 无法发挥多核 CPU 的优势但可以采用多开几个 Redis 实例来完善

Redis真的是单线程的吗

Redis6.0 之前是单线程的Redis6.0 之后开始支持多线程
redis 内部使用了基于 epoll 的多路服用也可以多部署几个 redis 服务器解决单线程的问题
redis 主要的性能瓶颈是内存和网络
内存好说加内存条就行了而网络才是大麻烦所以 redis6 引入了多线程的概念
redis6.0在网络 IO 处理方面引入了多线程如网络数据的读写和协议解析等需要注意的是执行命令的核心模块还是单线程的。


Redis 持久化

在这里插入图片描述

redis提供了两种持久化的方式分别是 RDBRedis DataBase和 AOFAppend Only File。

  • RDB简而言之就是在不同的时间点将 redis 存储的数据生成快照并存储到磁盘等介质上
  • AOF则是换了一个角度来实现持久化那就是将redis执行过的所有写指令记录下来在下次 redis 重新启动时只要把这些写指令从前到后再重复执行一遍就可以实现数据恢复了。

RDB 持久化

是指在指定的时间间隔内执行指定次数的写操作将内存中的数据集快照写入磁盘中它是Redis默认的持久化方式。执行完操作后在指定目录下会生成一个dump.rdb文件Redis 重启的时候通过加载dump.rdb文件来恢复数据。RDB触发机制主要有以下几种
在这里插入图片描述

RDB 的优点

  • 适合大规模的数据恢复场景如备份全量复制等

RDB缺点

  • 没办法做到实时持久化/秒级持久化。
  • 新老版本存在RDB格式兼容问题

在生成 RDB期间Redis 可以同时处理写请求么
可以的Redis提供两个指令生成RDB分别是 save 和 bgsave

  • 如果是 save 指令会阻塞因为是主线程执行的。
  • 如果是 bgsave 指令是 fork 一个子进程来写入 RDB 文件的快照持久化完全交给子进程来处理父进程则可以继续处理客户端的请求。

AOFappend only file 持久化

采用日志的形式来记录每个写操作追加到文件中重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。

AOF的工作流程如下
在这里插入图片描述
AOF的优点

数据的一致性和完整性更高
AOF的缺点

AOF记录的内容越多文件越大数据恢复变慢。

其实 RDB 和 AOF 两种方式也可以同时使用在这种情况下如果 redis 重启的话则会优先采用 AOF 方式来进行数据恢复这是因为 AOF 方式的数据恢复完整度更高。

如果你没有数据持久化的需求也完全可以关闭 RDB 和 AOF 方式这样的话redis 将变成一个纯内存数据库。


缓存穿透/缓存雪崩如何解决

一个常见的缓存使用方式读请求来了先查下缓存缓存有值命中就直接返回缓存没命中就去查数据库然后把数据库的值更新到缓存再返回。

缓存穿透

一般的缓存系统都是按照 key 去缓存查询如果不存在对应的 value就应该去后端系统查找比如DB 数据库。一些恶意的请求会故意查询不存在的 key请求量很大就会对后端系统造成很大的压力。这就叫做缓存穿透。

缓存穿透一般都是这几种情况产生的

业务不合理的设计比如大多数用户都没开守护但是你的每个请求都去缓存查询某个userid查询有没有守护。
业务/运维/开发失误的操作比如缓存和数据库的数据都被误删除了。
黑客非法请求攻击比如黑客故意捏造大量非法请求以读取不存在的业务数据。

如何解决

  • 如果是非法请求我们在API入口对参数进行校验过滤非法值。
  • 对查询结果为空的情况也进行缓存缓存时间设置短一点或者该 key 对应的数据 insert 之后清理缓存。
  • 对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中查询时通过该Bitmap 过滤。

缓存雪崩

当缓存服务器重启或者大量缓存集中在某一时间段失效这样在失效的时候会给后端系统带来很大的压力导致系统崩溃。

如何解决

  • 在缓存失效后通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存其它线程等待
  • 做二级缓存A1 为原始缓存A2 为拷贝缓存A1 失效时可以访问 A2A1 缓存失效时间设置为短期A2 设置为长期
  • 不同的 key设置不同的过期时间让缓存失效的时间尽量均匀

缓存击穿

指热点 key 在某个时间点过期的时候而恰好在这个时间点对这个 Key 有大量的并发请求过来从而大量的请求打到 DB。

缓存击穿和缓存雪崩看着有点像其实它两区别是缓存雪奔是指数据库压力过大甚至 down 机缓存击穿只是大量并发请求到了 DB 数据库层面。可以认为击穿是缓存雪崩的一个子集吧

如何解决

  • 使用互斥锁方案。缓存失效时不是立即去加载 DB 数据而是先使用某些带成功返回的原子操作命令如(Redis的setnx去操作成功的时候再去加载 DB 数据库数据和设置缓存。否则就去重试获取缓存。
  • “永不过期”是指没有设置过期时间但是热点数据快要过期时异步线程去更新和设置过期时间。

怎么保证缓存和数据库数据的一致性

1、淘汰缓存

数据如果比较复杂进行缓存的更新操作就会变得异常复杂因此一般推荐选择淘汰缓存而不是更新缓存。

2、选择先淘汰缓存再更新数据库

假如先更新数据库再淘汰缓存如果淘汰缓存失败那么后面的请求都会得到脏数据直至缓存过期。

假如先淘汰缓存再更新数据库如果更新数据库失败只会产生一次缓存穿透相比较而言后者对业务则没有本质上的影响。

3、延时双删策略

如下场景同时有一个请求 A 进行更新操作另一个请求 B 进行查询操作。

  1. 请求 A 进行写操作删除缓存
  2. 请求 B 查询发现缓存不存在
  3. 请求 B 去数据库查询得到旧值
  4. 请求 B 将旧值写入缓存
  5. 请求 A 将新值写入数据库
public void write(String key,Object data){
    redisUtils.del(key);
    db.update(data);
    Thread.Sleep(100);
    redisUtils.del(key);
}

这么做可以将1秒内所造成的缓存脏数据再次删除。这个时间设定可根据俄业务场景进行一个调节。

4、数据库读写分离的场景

两个请求一个请求 A 进行更新操作另一个请求B进行查询操作。

  1. 请求 A 进行写操作删除缓存
  2. 请求 A 将数据写入数据库了
  3. 请求 B 查询缓存发现缓存没有值
  4. 请求 B 去从库查询这时还没有完成主从同步因此查询到的是旧值
  5. 请求 B 将旧值写入缓存
  6. 数据库完成主从同步从库变为新值
    依旧采用延时双删策略解决此问题。

Redis 有哪些架构模式

单机版

特点

  • 简单

问题

  • 内存容量有限
  • 处理能力有限
  • 无法高可用。

主从模式

Redis 的复制replication功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品其中被复制的服务器为主服务器master而通过复制创建出来的服务器复制品则为从服务器slave。

只要主从服务器之间的网络连接正常主从服务器两者会具有相同的数据主服务器就会一直将发生在自己身上的数据更新同步给 从服务器从而一直保证主从服务器的数据相同。

特点

  • master/slave 角色
  • master/slave 数据相同
  • 降低 master 读压力在转交从库

问题

  • 无法保证高可用
  • 没有解决 master 写的压力

哨兵模式

Redis sentinel 是一个分布式系统中监控 redis 主从服务器并在主服务器下线时自动进行故障转移。其中三个特性

  • 监控MonitoringSentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒Notification当被监控的某个 Redis 服务器出现问题时 Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移Automatic failover当一个主服务器不能正常工作时 Sentinel 会开始一次自动故障迁移操作。

特点

  • 保证高可用
  • 监控各个节点
  • 自动故障迁移

缺点

  • 主从模式切换需要时间丢数据
  • 没有解决 master 写的压力

集群代理型

Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器Twemproxy 是一个快速的单线程代理程序支持 Memcached ASCII 协议和 redis 协议。

特点

  • 多种 hash 算法MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
  • 支持失败节点自动删除
  • 后端 Sharding 分片逻辑对业务透明业务方的读写方式和操作单个 Redis 一致

缺点

  • 增加了新的 proxy需要维护其高可用。
  • failover 逻辑需要自己实现其本身不能支持故障的自动转移可扩展性差进行扩缩容都需要手动干预

集群直连型## 标题

从 redis 3.0 之后版本支持 redis-cluster 集群Redis-Cluster 采用无中心结构每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

特点

  • 无中心架构不存在哪个节点影响性能瓶颈少了 proxy 层。
  • 数据按照 slot 存储分布在多个节点节点间数据共享可动态调整数据分布。
  • 可扩展性可线性扩展到 1000 个节点节点可动态添加或删除。
  • 高可用性部分节点不可用时集群仍可用。通过增加 Slave 做备份数据副本
  • 实现故障自动 failover节点之间通过 gossip 协议交换状态信息用投票机制完成 Slave到 Master 的角色提升。

缺点

  • 资源隔离性较差容易出现相互影响的情况。
  • 数据通过异步复制,不保证数据的强一致性
  • 高可用Redis架构分析搭建可以参考

Redis 过期键的删除策略

1.定时删除:在设置键的过期时间的同时创建一个定时器 timer). 让定时器在键的过期时间来临时 立即执行对键的删除操作。

2.惰性删除:放任键过期不管但是每次从键空间中获取键时都检查取得的键是 否过期 如果过期的话 就删除该键;如果没有过期 就返回该键。

3.定期删除:每隔一段时间程序就对数据库进行一次检查删除里面的过期键。至 于要删除多少过期键 以及要检查多少个数据库 则由算法决定。

Redis 有哪几种数据淘汰策略

  1. volatile-lru当内存不足以容纳新写入数据时从设置了过期时间的key中使用LRU最近最少使用算法进行淘汰
  2. allkeys-lru当内存不足以容纳新写入数据时从所有key中使用LRU最近最少使用算法进行淘汰。
  3. volatile-lfu4.0版本新增当内存不足以容纳新写入数据时在过期的key中使用LFU算法进行删除key。
  4. allkeys-lfu4.0版本新增当内存不足以容纳新写入数据时从所有key中使用LFU算法进行淘汰
  5. volatile-random当内存不足以容纳新写入数据时从设置了过期时间的key中随机淘汰数据。
  6. allkeys-random当内存不足以容纳新写入数据时从所有key中随机淘汰数据。
  7. volatile-ttl当内存不足以容纳新写入数据时在设置了过期时间的key中根据过期时间进行淘汰越早过期的优先被淘汰
  8. noeviction默认策略当内存不足以容纳新写入数据时新写入操作会报错。

热 Key 问题如何解决

什么是热Key呢在Redis中我们把访问频率高的key称为热点key。

**热点Key是怎么产生的呢**主要原因有两个

  • 用户消费的数据远大于生产的数据如秒杀、热点新闻等读多写少的场景。
  • 请求分片集中超过单 Redis 服务器的性能比如固定名称 keyHash 落入同一台服务器瞬间访问量极大超过机器瓶颈产生热点 Key 问题。

那么在日常开发中如何识别到热点key呢

  • 凭经验判断哪些是热Key
  • 客户端统计上报
  • 服务代理层上报

如何解决热key问题

  • Redis 集群扩容增加分片副本均衡读流量
  • 将热 key 分散到不同的服务器中
  • 使用二级缓存即 JVM 本地缓存,减少 Redis 的读请求。

MySQL 与 Redis 如何保证双写一致性

  • 缓存延时双删
  • 删除缓存重试机制
  • 读取 biglog 异步删除缓存

延时双删

什么是延时双删呢流程图如下
在这里插入图片描述
延时双删流程

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会比如1秒再次删除缓存。
    这个休眠一会一般多久呢都是1秒

这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束写请求可以删除读请求可能带来的缓存脏数据。

这种方案还算可以只有休眠那一会比如就那1秒可能有脏数据一般业务也会接受的。但是如果第二次删除缓存失败呢缓存和数据库的数据还是可能不一致对吧给 Key 设置一个自然的 expire 过期时间让它自动过期怎样那业务要接受过期时间内数据的不一致咯还是有其他更佳方案呢

删除缓存重试机制

因为延时双删可能会存在第二步的删除缓存失败导致的数据不一致问题。可以使用这个方案优化删除失败就多删除几次呀,保证删除缓存成功就可以了呀~ 所以可以引入删除缓存重试机制
在这里插入图片描述
删除缓存重试流程

  1. 写请求更新数据库
  2. 缓存因为某些原因删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息获取要删除的key
  5. 重试删除缓存操作

读取biglog异步删除缓存

重试删除缓存机制还可以吧就是会造成好多业务代码入侵。其实还可以这样优化通过数据库的binlog 来异步淘汰 key。
在这里插入图片描述
以mysql为例吧

  • 可以使用阿里的canal将binlog日志采集发送到MQ队列里面
  • 然后通过ACK机制确认处理这条更新消息删除缓存保证数据缓存一致性

Redis 事务机制

Redis 通过 MULTI、EXEC、WATCH 等一组命令集合来实现事务机制。事务支持一次执行多个命令一个事务中所有命令都会被序列化。在事务执行过程会按照顺序串行化执行队列中的命令其他客户端提交的命令请求不会插入到事务执行命令序列中。

简言之Redis事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。

Redis执行事务的流程如下

  1. 开始事务MULTI
  2. 命令入队
  3. 执行事务EXEC、撤销事务DISCARD

Redis 的 Hash 冲突

Redis 作为一个 K-V 的内存数据库它使用用一张全局的哈希来保存所有的键值对。这张哈希表有多个哈希桶组成哈希桶中的 entry 元素保存了 key 和 value 指针其中 key 指向了实际的键value 指向了实际的值。
在这里插入图片描述
哈希表查找速率很快的有点类似于Java中的HashMap它让我们在O(1) 的时间复杂度快速找到键值对。首先通过key计算哈希值找到对应的哈希桶位置然后定位到entry在entry找到对应的数据。

什么是哈希冲突

哈希冲突通过不同的key计算出一样的哈希值导致落在同一个哈希桶中。

Redis为了解决哈希冲突采用了链式哈希。链式哈希是指同一个哈希桶中多个元素用一个链表来保存它们之间依次用指针连接。
在这里插入图片描述
有些读者可能还会有疑问哈希冲突链上的元素只能通过指针逐一查找再操作。当往哈希表插入数据很多冲突也会越多冲突链表就会越长那查询效率就会降低了。

为了保持高效Redis 会对哈希表做rehash操作也就是增加哈希桶减少冲突。为了rehash更高效Redis还默认使用了两个全局哈希表一个用于当前使用称为主哈希表一个用于扩容称为备用哈希表。


Redis底层使用的什么通讯协议?

RESP英文全称是Redis Serialization Protocol,它是专门为 redis 设计的一套序列化协议. 这个协议其实在 redis 的 1.2 版本时就已经出现了,但是到了redis2.0 才最终成为 redis 通讯协议的标准。

RESP主要有实现简单、解析速度快、可读性好等优点。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: redis

“Redis 面试题总结” 的相关文章