缓存异常

缓存雪崩

理解:

  • 雪崩,就是某东西蜂拥而至的意思,像雪崩一样。这里指Redis缓存大规模集体失效,在高并发情况下使得key大规模访问MySQL,导致MySQL崩掉。

解决方案:

  • 通常的解决方案是将key的过期时间后面加上一个随机数,让key均匀的失效;
  • 考虑用队列或者锁让程序执行在压力范围之内,当然这种方案可能会影响并发量;
  • 热点数据可以考虑不失效。

缓存穿透

理解:

  • 访问透过Redis直接访问MySQL,通常是一个不存在的key,在MySQL中查询为null,每次请求落在MySQL且高并发。

解决方案:

  • 将查到的null设成该key的缓存对象;
  • 根据明显错误的key在逻辑层就就进行验证;
  • 同时,你也可以分析用户行为,是否为故意请求或者爬虫、攻击者。针对用户访问做限制;
  • 其他等等,比如用布隆过滤器(超大型hashmap)先过滤。

缓存击穿

理解:

  • 指一个key非常热点,大并发集中对这一个点进行访问,这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求MySQL,就像蛮力击穿一样,导致MySQL崩掉。

解决方案:

  • 可以使用互斥锁避免大量请求同时落到db;
  • 布隆过滤器,判断某个容器是否在集合中;
  • 可以将缓存设置永不过期(适合部分情况);
  • 做好熔断、降级,防止系统崩溃。

发生缓存异常如何处理

一般避免以上情况发生我们从三个时间段去分析下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

上面的几点我会在吊打系列Redis篇全部讲一下这个月应该可以吧Redis更完,限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。

好处:

数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。只要数据库不死,就是说,对用户来说,3/5 的请求都是可以被处理的。只要有 3/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。

这个在目前主流的互联网大厂里面是最常见的,你是不是好奇,某明星爆出什么事情,你发现你去微博怎么刷都空白界面,但是有的人又直接进了,你多刷几次也出来了,现在知道了吧,那是做了降级,牺牲部分用户的体验换来服务器的安全,可还行。

如何实现延时队列

使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

持久化

RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会丢失大量数据,所以需要AOF配合来使用。在Redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作来实现完整恢复重启之前的状态。

这里很好理解,把RDB理解为一整个表全量的数据,AOF理解为每次操作的日志就好了,服务器重启的时候先把表的数据全部搞进去,但是他可能不完整,你再回放一下日志,数据不就完整了嘛。不过Redis本身的机制是AOF持久化开启且存在AOF文件时,优先加载AOF文件;AOF关闭或者AOF文件不存在时,加载RDB文件;加载AOF/RDB文件成功后,Redis启动成功;AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

RDB的原理

fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,新写的数据会逐渐和子进程分离开来。

Pipeline

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

Redis的同步机制

Redis可以使用主从同步、从从同步。第一次同步时,主节点做一次bgsve,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接收完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步就行。

Redis集群

Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

Redis5种数据类型的常见使用场景

String

这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。

但是真实的开发环境中,很多仔可能会把很多比较复杂的结构也统一转成String去存储使用,比如有的仔他就喜欢把对象或者List转换为JSONString进行存储,拿出来再反序列话啥的。我在这里就不讨论这样做的对错了,但是我还是希望大家能在最合适的场景使用最合适的数据结构,对象找不到最合适的但是类型可以选最合适的嘛,之后别人接手你的代码一看这么规范,诶这小伙子有点东西呀,看到你啥都是用的String,垃圾!好了,这些都是题外话了,道理还是希望大家记在心里,习惯成自然嘛,小习惯成就你。

String的实际应用场景比较广泛的有:

  • 缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  • 计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  • 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成,大大提高效率。

Hash

这个是类似 Map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作 Hash 里的某个字段。但是这个场景其实还是多少单一了一些,因为现在很多对象都是比较复杂的,比如你的商品对象可能里面就包含了很多属性,其中也有对象。我自己使用的场景用得不是那么多。

List

List 是有序列表,这个还是可以玩儿出很多花样的。比如可以通过 List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 List 实现分页查询,这个是很棒的一个功能,基于 Redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。比如可以搞个简单的消息队列,从 List 头怼进去,从 List 屁股那里弄出来。List本身就是我们在开发过程中比较常用的数据结构了,热点数据更不用说了。

  • 消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的“抢”列表尾部的数据。
  • 文章列表或者数据分页展示的应用。比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能,大大提高查询效率。

Set

Set 是无序集合,会自动去重的那种。直接基于 Set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?对吧。反正这些场景比较多,因为对比很快,操作也简单,两个查询一个Set搞定。

Sorted Set

Sorted Set 是排序的 Set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。有序集合的使用场景与集合类似,但是set集合不是自动有序的,而Sorted Set可以利用分数进行成员间的排序,而且是插入时就排序好。所以当你需要一个有序且不重复的集合列表时,就可以选择Sorted Set数据结构作为选择方案。

  • 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
  • 用Sorted Set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
  • 微博热搜榜,就是有个后面的热度值,前面就是名称。

如何解决多个系统同时操作Redis带来的数据问题

这个问题我以前开发的时候遇到过,其实并发过程中确实会有这样的问题,比如下面这样的情况

系统A、B、C三个系统,分别去操作Redis的同一个Key,本来顺序是1,2,3是正常的,但是因为系统A网络突然抖动了一下,B,C在他前面操作了Redis,这样数据不就错了么。

就好比下单,支付,退款三个顺序你变了,你先退款,再下单,再支付,那流程就会失败,那数据不就乱了?你订单还没生成你却支付,退款了?明显走不通了,这在线上是很恐怖的事情。

我们可以找个管家帮我们管理好数据的嘛!

某个时刻,多个系统实例都去更新某个 key。可以基于 Zookeeper 实现分布式锁。每个系统通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。你要写入缓存的数据,都是从 MySQL 里查出来的,都得写入 MySQL 中,写入 MySQL 中的时候必须保存一个时间戳,从 MySQL 查出来的时候,时间戳也查出来。每次要写之前,先判断一下当前这个 Value 的时间戳是否比缓存里的 Value 的时间戳要大。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。

如何保证缓存和数据库的双写一致性

一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。把一些列的操作都放到队列里面,顺序肯定不会乱,但是并发高了,这队列很容易阻塞,反而会成为整个系统的弱点、瓶颈。

最经典的KV、DB读写模式是什么?

最经典的KV+DB读写模式,就是Cache Aside Pattern

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据放入缓存,同时返回响应
  • 更新的时候,先更新数据库,然后再删除缓存

为什么是删除缓存,而不是更新缓存?

原因很简单,因为很多时候,在复杂点的缓存场景中,缓存不单单是数据库中直接取出来的值。比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?举个栗子:一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

其实删除缓存,而不是更新缓存,就是一个 Lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 Mybatis,Hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 List,没有必要说每次查询部门,里面的 1000 个员工的数据也同时查出来啊。80%的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。

Redis和Memcached的区别

  • Redis 支持复杂的数据结构:

Redis 相比 Memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, Redis 会是不错的选择。

  • Redis 原生支持集群模式:

在 redis3.x 版本中,便能支持 Cluster 模式,而 Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。

  • 性能对比:

由于 Redis 只使用单核,而 Memcached 可以使用多核,所以平均每一个核上 Redis 在存储小数据时比 Memcached 性能更高。而在 100k 以上的数据中,Memcached 性能要高于 Redis,虽然 Redis 最近也在存储大数据的性能上进行优化,但是比起 Memcached,还是稍有逊色。

说说Redis的线程模型

Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用IO多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。文件事件处理器的结构包含 4 个部分:

  • 多个 Socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

常见面试题

使用Redis有哪些好处?

  1. 存储在内存中,有更快的响应
  2. 为数据库减压,提高整体的性能
  3. 丰富的数据类型,适应更多的场景

Redis相比Memcached有哪些优势?

  1. 拥有更多的数据结构,支持更丰富的数据操作
  2. 在Redis3.x版本中,便能支持cluster模式,而memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据
  3. Redis只是使用单核,memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比memcached性能更高

Redis常见性能问题和解决方案

  • MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?
  • Memcache与Redis的区别都有哪些?
  • 在什么样的场景下可以充分的利用Redis的特性,大大提高Redis的效率?
  1. 秒杀库存扣减
  2. APP首页的流量高峰

Redis的缓存雪崩、穿透、击穿了解么?有什么异同点?分别怎么解决?

缓存雪崩:
是指在同一时间,大面积的key失效,那一瞬间Redis和没有一样,这个数据级别的请求直接打到数据库上市灾难性的,试想一下,若打崩的是用户服务,那么其他依赖用户服务的接口几乎都会报错,若没有做熔断等策略,基本上都是瞬间挂一大片的节奏

解决办法:
1.批量往Redis存数据的时候,把每个key的失效时间都加一个随机值,这样可以保证数据不会在同一时间大面积失效;
2.Redis集群部署,就热点数据均匀地分布在不同的Redis库中,也能避免全部失效的问题;
3.设置热点数据永不过期,有更新操作就更新缓存就好。

缓存穿透:
用户不断地发起缓存和数据库中都不存在的数据的请求,导致数据库压力过大,严重时会打崩数据库。

解决办法:
1.增加参数校验;
2.从网关层NGINX增加配置项,都单个IP每秒访问次数超过阈值的IP进行拉黑操作;
3.BloomFilter也能很好地防止缓存穿透的发生,它的原理很简单,就是利用高效的数据结构与算法快速地判断出你的这个key在数据库中是否存在,若不存在,直接return;若存在,就去查db,刷新KV,再return

缓存击穿:一个key非常热点,不停地扛着大并发,大并发集中对这一个key进行访问,在这个key失效的瞬间,持续的大并发就穿过缓存,直接请求数据库,就好像在完好无损的桶上凿了一个洞

解决办法:
1.设置热点数据永不过期;
2.增加互斥锁

Redis的基本类型有哪些?他们的使用场景了解么?比较高级的用法你使用过么?

基本的数据类型有:string、list、hash、set、Sorted Set

string的应用场景:

  1. 缓存功能
  2. 计数器 – 快速实现计数和查询的功能
  3. 共享用户session

hash的应用场景:

  1. 存储一些对象型的数据结构

list的应用场景:

  1. 存储一些列表型的数据结构 – 如粉丝列表、文章列表等
  2. 实现分页查询 – 可以使用lrange命令,读取某个闭区间内的元素
  3. 实现消息队列 – 比如:数据的生产者可以通过lpush命令从左边插入数据,多个数据消费者,可以使用brpop命令阻塞地“抢”列表尾部的数据

set的应用场景:

  1. 实现 共同(交集)、合并(并集)、相差(差集) 等功能 – 比如获取两个人的好友列表的交集,即可获取共同好友

Sorted Set的应用场景:

  1. 排行榜 – 比如视频网站的播放量排行榜(按照时间、按照播放量、按照获得的赞数等
  2. 带权重的数据

高级用法:Bitmap、HyperLogLog、Geo、pub/sub、Pipeline、Lua、事务

Redis主从怎么同步数据的?集群的高可用怎么保证?持久化机制了解么?

主从同步数据:启动一台slave的时候,它会发送一个psync命令给master,若这个slave第一次连接到master,它会触发一个全量复制,master机会启动一个线程,生成RDB快照,还会把新的写请求都缓存到内存中,RDB文件生成后,master会将这个RDB文件发送给slave的,slave拿到之后做个第一件事情就是写入本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命令都发送给slave。

集群的高可用:Redis sentinel着眼于高可用,在master宕机会自动将slave提升为master,继续提供服务;Redis cluster着眼于扩展性,在单个Redis内存不足时,使用cluster进行分片存储

持久化机制:RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机时会导致丢失大量的数据,所以需要AOF来配合使用,在Redis实例重启时,会使用RDB持久化文件重建内存,再使用AOF文件重放近期的操作指令来实现完整恢复重启之前的状态。Redis本身的机制是,AOF持久化开启且存在AOF文件时,优先加载AOF文件,AOF 关闭或AOF文件不存在时,加载RDB文件。加载RDB/AOF文件之后,Redis启动成功,RDB/AOF文件存在错误时,Redis启动失败并打印错误信息

为什么 redis 单线程却能支撑高并发?

Redis 采用单线程模式处理请求。这样做的原因有 2 个:一个是因为采用了非阻塞的异步事件处理机制;另一个是缓存数据都是内存操作 IO 时间不会太长,单线程可以避免线程上下文切换产生的代价。

Redis的线程模型:

Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

  • 多个 Socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

如何保证缓存和数据库数据的一致性?

一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。把一些列的操作都放到队列里面,顺序肯定不会乱,但是并发高了,这队列很容易阻塞,反而会成为整个系统的弱点,瓶颈。