vlambda博客
学习文章列表

02中间件(缓存) -- 面试手册泰山版

一、为什么要使用缓存

1、性能,需要执行耗时特别久,且结果不频繁变动的SQL,就适合将运行结果放入缓存。这样后面的请求去缓存中读取,使得请求能够迅速响应。

在理想状态下,我们的页面跳转需要在瞬间解决,对于页内操作则需要在刹那间解决。另外,超过一弹指的耗时操作要有进度提示,并且可以随时中止或取消,这样才能给用户最好的体验。

2、并发,在大并发情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问数据库。

二、优秀的缓存系统Redis

Redis是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSQL数据库之一,也被人们称为数据结果服务器。

Redis相比同类的其他产品,具有如下优点:

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以在此加载进行使用。

  • Redis不仅仅支持简单的key-value类型数据,同时还提供list,set,zset,hash等数据结构的存储

  • Redis支持数据的备份,即master-slave模式的数据备份

三、Redis为什么这么快

主要三点:

  • 纯内存操作

  • 单线程,避免了频繁的上下文切换

  • 采用了非阻塞I/O多路复用机制

我们的redis-client在操作的时候,会产生具有不同事件类型的socket。在服务端,有一段I/0多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。需要说明的是,这个I/O多路复用机制,redis还提供了select、epoll、evport、kqueue等多路复用函数库。

四、Redis的数据类型,以及每种数据类型的使用场景

(一)String 这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。

(二)hash 这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。

(三)list 使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。

(四)set 因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?

因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。

(五)sorted set

sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。另外,参照另一篇《分布式之延时任务方案解析》,该文指出了sorted set可以用来做延时任务。最后一个应用就是可以做范围查找。

五、redis的过期策略以及内存淘汰机制

回答: redis采用的是定期删除+惰性删除策略。为什么不用定时删除策略? 定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略. 定期删除+惰性删除是如何

工作的呢? 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。采用定期删除+惰性删除就没其他问题了么? 不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。在redis.conf中有一行配置

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己) 1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用,目前项目在用这种。3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。应该也没人用吧,你不删最少使用Key,去随机删。4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。不推荐 5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。依然不推荐 6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。不推荐 ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。


渐进式ReHash

渐进式rehash的原因

整个rehash过程并不是一步完成的,而是分多次、渐进式的完成。如果哈希表中保存着数量巨大的键值对时,若一次进行rehash,很有可能会导致服务器宕机。

渐进式rehash的步骤

  • 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表

  • 维持索引计数器变量rehashidx,并将它的值设置为0,表示rehash开始

  • 每次对字典执行增删改查时,将ht[0]的rehashidx索引上的所有键值对rehash到ht[1],将rehashidx值+1。

  • 当ht[0]的所有键值对都被rehash到ht[1]中,程序将rehashidx的值设置为-1,表示rehash操作完成

注:渐进式rehash的好处在于它采取分为而治的方式,将rehash键值对的计算均摊到每个字典增删改查操作,避免了集中式rehash的庞大计算量。


缓存穿透

概念访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB会挂掉。

解决方案:

采用布隆过滤器,使用一个足够大的bitmap,用于存储可能访问的key,不存在的key直接被过滤;

访问key未在DB查询到值,也将空值写进缓存,但可以设置较短过期时间。

缓存雪崩

大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。

解决方案

可以给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效;采用限流算法,限制流量;采用分布式锁,加锁访问。




  • 能否说下缓存击穿、穿透、雪崩的区别?

  • 区分:

  • 缓存击穿 (某个热点key缓存失效了)

  • 缓存穿透(查询不存在数据)

  • 缓存雪崩 (多个热点key都过期)


  • 针对这些问题,说下常见的解决方案

    缓存击穿 (某个热点key缓存失效了)

    • 大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩

    • 预防:存数据的过期时间设置随机,防止同一时间大量数据过期现象发生,设置热点数据永远不过期,定时任务定时更新




    缓存穿透(查询不存在数据)

    • 查询一个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id为“-1”不存在的数据

    • 如果从存储层查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在大量查询不存在的数据,可能DB就挂掉了,这也是黑客利用不存在的key频繁攻击应用的一种方式。

    • 预防:接口层增加校验,数据合理性校验,缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置短点的过期时间,防止同个key被一直攻击




    缓存雪崩 (多个热点key都过期)

    • 缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力增大。

    • 和缓存雪崩的区别在于这里针对某一key缓存,后者则是很多key。

    • 预防:设置热点数据不过期,定时任务定时更新缓存,或者设置互斥锁