Redis面试
一、redis简介
1. 缓存数据库:实现了对热点技术的高速缓存,提高了应用的响应速度,减少后端压力。
2. 主流应用架构
为了提高性能,在客户端和存储层之间添加一个缓存层。当客户端向后端发送请求时,会先去缓存层查,如果有相关数据就直接返回,如果没有就进行穿透查询,如果存储层有相关数据,就将该数据回写进缓存层,那么当再次请求同样的数据时,可以快速响应。(熔断)当存储层无法提供服务时,可以将客户端的请求,直接送到缓存层,无论有没有获得数据都直接返回,实现在有损情况下提供服务。
3. 缓存中间件--Memcache和Redis的区别
(1)Memcache:代码层类似Hash
a. 支持简单数据类型
b. 不支持数据持久化存储,当服务器无法工作是,数据是无法保存下来的;
c. 不支持主从同步
d. 不支持分片
(2)Redis
a. 数据类型丰富:String,Set,List,Hash,Sorted Set
b. 支持数据磁盘持久化存储;
c. 支持主从
d. 支持分片
4. 为什么Redis能这么快:10w+的QPS(每秒查询次数)
(1)完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高
(2)数据结构简单,对数据操作也简单--HashMap
(3)采用单线程,单线程也能处理高并发请求,想多核也可启动多实例
(4)使用多路I/O复用模型,NIO
5. 多路I/O复用模型
(1)FD:File Descriptor,文件描述符
一个打开的文件通过唯一的描述符进行引用,该描述符是打开文件的元数据到文件本身的映射
(2)select系统调用:selector方法返回可读可写的FD个数;selector是负责监听文件是可读还是可写的
(3)Redis采用的I/O多路复用函数:epoll/kqueue/evport/select?
a. Redis会根据平台的不同,选择合适的I/O多路复用函数
b. 优先选择时间复杂度是O(1)的I/O多路复用函数作为底层实现
c. 以时间复杂度为O(n)的select作为保底
d. 基于react设计模式监听I/O事件
二、redis常用数据类型和底层数据结构
1. 供用户使用的数据类型
(1)String:最基本的数据类型,二进制安全
(2)Hash:String元素组成的字典,适合用于存储对象
(3)List:链表,按照String元素插入顺序排序
(4)Set:String元素组成的无序集合,通过hash表实现,不允许重复
(5)Sorted Set:通过分数来为集合中的成员进行从小到大的排序,通过hash表或者skiplist实现
(6)用于计数的HyperLogLog,用于支持存储地理位置信息的Geo
2. 数据类型底层数据结构
(1)SDS - simple synamic string - 支持自动动态扩容的字节数组
(2)list - 链表
(3)dict - 使用双哈希表实现的, 支持平滑扩容的字典
(4)zskiplist - 附加了后向指针的跳跃表
(5)intset - 用于存储整数数值集合的自有结构
(6)ziplist - 一种实现上类似于TLV, 但比TLV复杂的, 用于存储任意数据的有序序列的数据结构
(7)quicklist - 一种以ziplist作为结点的双链表结构,
(8)zipmap - 一种用于在小规模场合使用的轻量级字典结构
三、从海量key里查询出某一固定前缀的key
1. 查询前需要摸清数据规模,即问清楚边界
2. KEYS pattern:查找所有符合给定模式pattern的key
缺点:KEYS指令需要一次性返回所有匹配的key,如果key的数量太大会使服务卡顿。
3. SCAN cursor [MATCH pattern][COUNT count]:可以无阻塞的提取出指定模式的key列表。
基于游标cursor的迭代器,需要基于上一次的游标延续之前的迭代过程;以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历;不保证每次执行都返回某个给定数量的元素,支持模糊查询;一次返回的数量不可控,只能是大概率符合count参数
四、redis中的hot key和big key
1. Hot key
(1)热点键值,即某一个key在特定的一段时间内被频繁访问,导致大量的访问请求落在一个redis片上;
(2)解决方案
a. 使用客户端本地缓存;(本地缓存过大,数据一致性)
b. 为hot key设置前缀ID,使得同一个ID落在不同的片上;
2. Big key
(1)数据量大的key,某个key的实例内存使用量远大于其他实例,造成内存不足,拖慢整个集群的使用;
(2)解决方法:对大数据量的key进行拆分
3. 如何发现hot key和big key
(1)事前
在业务开发阶段,就要对可能变成 hot key ,big key 的数据进行预判,提前处理,这需要的是对产品业务的理解,对运营节奏的把握,对数据设计的经验。
(2)事中
a. 监控:在proxy层,对每一个 redis 请求进行收集上报;
b. 自动处理:
五、通过Redis实现分布式锁
1. 分布式锁需要解决的问题
(1)互斥性:任一时刻都只有一个客户端能够获得锁;
(2)安全性:锁只能有持有该锁的客户端删除,不能由其他客户端删除
(3)死锁:避免死锁
(4)容错:
2. SETNX key value:如果key不存在,则创建并赋值。判断当前是否有线程在操作key,但会一直占用线程
(1)时间复杂度:O(1)
(2)设置成功,返回1;设置失败,返回0
3. EXPIRE key seconds
(1)设置key的生存时间,当key过期时(生存时间为0),会被自动删除
(2)原子性得不到满足
4. SET key value [EX seconds][PX milliseconds][NX|XX]
(1)EX seconds:设置key的过期时间
(2)PX millisecond:设置key的过期时间为millisecond;
(3)NX:只有键不存在时,才对key进行设置操作;
(4)XX:只有键已经存在时,才对键进行设置操作
(5)SET操作成功完成时,返回OK,否则返回nil;
5. 大量的key同时过期的注意事项
(1)清除大量的key会很耗时,会出现短暂的卡顿的现象;
(2)解决方案:在设置key的过期时间的时候,给每个key加上随机值;
6. Redis设置过期时间后,删除过期key
(1)定期删除:redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,过期就删;
(2)惰性删除:通过查询key,删除那些没有被定期删除的key;若大量key堆积,则需要利用redis内存淘汰机制。
7. 基于redis的分布式锁的实现
(1)基于REDIS的SETNX()、EXPIRE()方法做分布式锁
a. setnx(lockkey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功
b. expire() 命令对 lockkey 设置超时时间,为的是避免死锁问题。
c. 执行完业务代码后,可以通过 delete 命令删除 key。
(2)基于 REDIS 的 SETNX()、GET()、GETSET()方法做分布式锁
这个方案的背景主要是在setnx()和expire()的方案上针对可能存在的死锁问题,做了一些优化。
a. setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2。
b. get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。
c. 计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。
d. 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
e. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
(3)基于REDLOCK做分布式锁
使用 Redlock 算法,可以保证在挂掉最多 2 个节点的时候,分布式锁服务仍然能工作,这相比之前的数据库锁和缓存锁大大提高了可用性,由于 redis 的高效性能,分布式缓存锁性能并不比数据库锁差。
a. 客户端获取当前时间,以毫秒为单位。
b. 客户端尝试获取 N (一般N=5)个节点的锁,(每个节点获取锁的方式和前面说的缓存锁一样),N 个节点以相同的 key 和 value 获取锁。客户端需要设置接口访问超时,接口超时时间需要远远小于锁超时时间,比如锁自动释放的时间是 10s,那么接口超时大概设置 5-50ms。这样可以在有 redis 节点宕机后,访问该节点时能尽快超时,而减小锁的正常使用。
c. 客户端计算在获得锁的时候花费了多少时间,方法就是用当前时间减去在步骤a获取的时间,只有客户端获得了超过3(ceil(N/2+1))个节点的锁,而且获取锁的时间小于锁的超时时间,客户端才获得了分布式锁。
d. 客户端获取的锁的有效时间为设置的锁超时时间减去步骤c计算出的获取锁花费时间。
e. 如果客户端获取锁失败了,客户端会依次删除所有redis实例上的锁。
六、Redis内存淘汰机制
1. Volatile-LRU:从已设置过期时间的数据中挑选最近最少使用的数据淘汰;
2. Volatile-TTL:从已设置过期时间的数据集中挑选将要过期的数据淘汰;
3. Volatile-random:从已设置过期时间的数据集中任意选择数据淘汰;
4. Allkeys-LRU:当内存不足以容纳新的数据时,在key空间中,移除最近最少使用的key;
5. Allkeys-random:从数据集中任意挑选数据淘汰;
6. No-eviction:进制驱逐数据,当内存不足以容纳新的数据时,新写入数据会报错
7. Redis中的LRU实现
基于HashMap和双向链表实现 LRU。整体的设计思路是,可以使用HashMap 存储key,这样可以做到save和get key的时间都是O(1),而HashMap的Value指向双向链表实现的LRU的Node节点。
七、如何使用Redis做异步队列
1. 使用List作为队列,RPUSH右端生产消息,LPOP左端消费信息
(1) 缺点:LPOP没有等待队列有值就直接消费;
(2)可以通过在应用层引入Sleep机制去调用LPOP重试
2. BLPOP key[key ...]timeout:阻塞直到队列有消息或者超时
(1)缺点:只能供一个消费者消费
3. 使用Redis中的pub/sub主题订阅者模式
(1)发送者pub发送消息,订阅者sub接受消息
(2)订阅者可以订阅任意数量的频道
(3)缺点是消息发布是无状态的,无法保证可达
八、Redis如何持久化
1. RDB快照持久化:保存某个时间点的全量数据快照。快照:关于指定数据集合的一个完全可用拷贝。RDB配置,在redis.con中,“save seconds write-instructions”SAVE:阻塞Redis的服务器进程,直到RDB文件被创建完毕;
BGSAVE:Fork出一个子进程来创建RDB文件,不阻塞服务器进程
2. 自动化触发RDB持久化的方式
(1)根据redis.conf配置里的SAVE m n定时触发(用的是BGSAVE)
(2)主从复制时,主节点自动触发
(3)执行Debug Reload
(4)执行Shutdown且没有开启AOF持久化
3. BGSAVE原理
系统调用fork():创建进程,实现了Copy-on-right。Copy-on-Write就是一种优化策略,如果有多个调用者同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。
4. RDB持久化缺点:
(1)内存数据的全量同步,数据量如果很大,会由于I/O而严重影响性能
(2)可能会因为Redis挂掉而丢失从当前至最近一次快照期间的数据
5. AOF持久化:保存Redis服务器所执行的写状态来记录数据库的。备份数据库接收到的指令。
(1)记录下除了查询以外的所有变更数据库状态的指令
(2)以append的形式追加保存到appendonly.aof文件中(增量)
(3)配置:在redis.conf中,appendonly no→yes
(4)随着写操作的不断增加,这些操作也会被记录下来,使得AOF文件不断增大,日志重写解决AOF文件大小不断增大的问题,原理如下:
a. 调用fork(),创建一个子进程;
b. 子进程会进行AOF重写,从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录该键值对的多个命令,即去除冗余写;
c. 主进程持续将新的变动同时写到AOF重写缓存中和原来的AOF里;
d. 主进程获取子进程重写AOF的完成信号,将AOF重写缓存中的内容同步到新的AOF文件中;
e. 使用新的AOF文件替换旧的AOF文件
6. AOF持久化方式
(1)Always:每次数据修改时,都会写入AOF文件中,但是这样会严重降低redis的速度;
(2)Everysec:服务器每一秒重调用一次fdatasync,将缓冲区里面的命令写入到磁盘里面,在这种模式写,服务器即使遭遇意外停机时,最多只丢失一秒钟内执行的命令数据;(默认模式)
(3)No:让操作系统来决定什么时候同步,这种模式写,服务器遭遇意外停机时,丢失命令的数据是不确定的
7. RDB和AOF文件共存情况下的回复流程
8. RDB和AOF的优缺点
(1)RDB优点:全量数据快照,文件小,恢复快
(2)RDB缺点:无法保存最近一次快照之后的数据
(3)AOF优点:可读性高,适合保存增量数据,数据不易丢失
(4)AOF缺点:文件体积大,恢复时间长
9. RDB-AOF混合持久化方式
(1)BGSAVE做镜像全量持久化(主从初始化时使用),AOF做增量持久化;
(2)Redis主从同步时,会先开辟一个后端子进程,用来复制当前数据的快照;与此同时,主进程也会记录写操作;当快照复制完成后,会将数据同步到从数据库上,并将这段时间的增量在之后也同步过去,之后同步就全是增量同步方式了;
10. Pipeline和主从同步
(1)使用pipeline的好处
a. Pipeline和Linux的管道类似
b. Redis基于请求/响应模式(TCP请求),单个请求处理需要一一应答。
c. Pipeline批量执行命令,节省多次IO往返的时间。Pipeline将请求打包发送,默认是53条,再将结果统一到一个报文中返回;
d. 有顺序依赖的指令建议分批发送
(2)Redis的同步机制(主从,哨兵,集群)
a. 全同步过程:Salve发送sync命令到Master;Master启动一个后台进程,将Redis中的数据快照保存到文件中;Master将保存数据快照期间接受到的写命令缓存起来;Master完成写操作后,将该文件发送给Salve;使用新的AOF文件替换掉旧的AOF文件;Master将这期间收集的增量写命令发送给Salve端
b. 增量同步过程:Master接收到用户的操作指令,判断是否需要传播到Salve;将操作记录追加到AOF文件;将操作传播到其他Slave:对齐主从库和往响应缓存写入指令;将缓存中的数据发送给Slave
c. 哨兵:哨兵的作用就是监控redis主、从数据库是否正常运行,主出现故障自动将从数据库转换为主数据库。
(10)解决主从同步Master宕机后的主从切换问题:
a. 监控:检查主从服务器是否运行正常
b. 提醒:通过API向管理员或者其他应用程序发送故障通知;
c. 自动故障迁移:主从切换
(11)留言协议Gossip
a. 每个节点都随机地与对方通信,最终所有节点的状态达成一致
b. 种子节点定期随机想其他节点发送节点列表以及需要传播的消息
c. 不保证信息一定会传递给所有的节点,但是最终会趋于一致
11. Redis集群原理
(1)从海量数据里快速找到所需
a. 分片:按照某种规则去划分数据,分散存储在多个节点上
b. 常规的按照hash划分无法实现节点的动态增减
(2)一致性hash算法:
a. 对2^32取模,将hash值空间组织成虚拟圆环
b. 将数据key使用相同的函数Hash计算出hash值
(3)Hash环的数据倾斜
a. 造成资源分配不均匀,某一节点的资源过度集中;
b. 解决:引入虚拟节点解决数据倾斜问题
九、Redis与Mysql双写一致性方案解析
1. 缓存更新策略
(1)先更新数据库,再更新缓存;
(2)先删除缓存,再更新数据库;
(3)先更新数据库,再删除缓存
2. 先删缓存,再更新数据库
(1)该方案数据导致不一致的原因:同时有一个请求A进行更新操作,另一个请求B进行查询操作,则
a. 请求A进行写操作,删除缓存
b. 请求B查询发现缓存不存在
c. 请求B去数据库查询得到旧值
d. 请求B将旧值写入缓存
e. 请求A将新值写入数据库 上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。
(2)解决方案:延时双删策略
a. 先淘汰缓存
b. 再写数据库
c. 休眠1秒,再次淘汰缓存 这么做,可以将1秒内所造成的缓存脏数据,再次删除。
(3)如何确定休眠时间
写数据的休眠时间在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
(4)使用MySQL读写分离的情况下,数据不一致的原因:
a. 请求A进行写操作,删除缓存
b. 请求A将数据写入数据库了
c. 请求B查询缓存发现,缓存没有值
d. 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
e. 请求B将旧值写入缓存
f. 数据库完成主从同步,从库变为新值 上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
(5)第二次删除失败?单库环境下
提供一个保障重试机制。
a. 更新数据库数据;
b. 缓存因为种种问题删除失败;
c. 将需要删除的key发送至消息队列;
d. 自己消费消息,获得需要删除的key;
e. 继续重试删除操作,直到成功 然而,该方案有一个缺点,对业务线代码造成大量的侵入。
3. 先更新数据库,在删除缓存
a. 更新数据库数据
b. 数据库会将操作信息写入binlog日志当中
c. 订阅程序提取出所需要的数据以及key
d. 另起一段非业务代码,获得该信息
e. 尝试删除缓存操作,发现删除失败
f. 将这些信息发送至消息队列
g. 重新从消息队列中获得该数据,重试操作。
十、缓存雪崩,缓存穿透和缓存击穿
1. 缓存雪崩
(1)缓存统一时间大量失效,比如设置的TTL同时到期或者redis服务器宕机等。后面的请求会直接落到数据库上,而数据库无法承受海量的数据请求而宕机。
(2)解决
a. 事前:尽量保证整个redis集群的高可用性,发现机器宕机需要自动补上,并且要选择合适的内存淘汰机制;
b. 事中:本地缓存+hystrix限流&服务降级,避免MySQL宕机;
c. 事后:利用redis持久化保存的数据尽快恢复缓存;
2. 缓存穿透
(1)查询缓存中不存在的数据,导致所有的请求都落在数据库上,造成数据库崩溃;
(2)解决
a. 数据库中若不含有某个值,可以直接在缓存中设为null;
b. 布隆过滤器(过滤非法请求)