面试专题:中间件——缓存

为什么要使用缓存

(一)性能 如下图所示,我们在碰到需要执行耗时特别久,且结果不频繁变动的SQL(比如业务基础数据、假期数据等),就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。
在这里插入图片描述

题外话:忽然想聊一下这个迅速响应的标准。其实根据交互效果的不同, 这个响应时间没有固定标准。不过曾经看到过一篇文章:“在理想状态夏,我们的页面跳转需要在瞬间解决,对于页面内操作则需要在刹那间解决。另外,超过一弹指的耗时操作要有进度提示,并且可以随时中止或取消,这样才能给用户最好的体验。”
那么瞬间、刹那、一弹指具体是多少时间呢?
根据《摩诃僧祗律》记载

一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。
那么,经过周密的计算,一瞬间为0.36秒,一刹那为0.018秒,一弹指长达7.2秒。
(二)并发 如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问Redis,而不是直接访问数据库。
在这里插入图片描述

优秀的缓存系统Redis

Redis是完全开源免费的,用C语言编写的,遵守BSD协议,是一个高性能的分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSQL数据库之一,也被人们称为数据结构服务器。
Redis相比同类的其他产品,具有如下优点:

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list、set、zset、hash等数据结构的存储
  • Redis支持数据的备份,即master-slave模式的数据备份

redis为什么这么快

主要是以下三点

  • 纯内存操作
  • 单线程操作,避免了频繁的上下文切换
  • 采用了非阻塞I/O多路复用机制

题外话:我们现在要仔细的说一说I/O多路复用机制,因为这个说法实在是太通俗了,通俗到一般人都不懂是什么啥意思。博主打一个比方:小曲在深圳开了一家快递店,负责同城快送服务。小曲因为资金限制,雇佣了一批快递员,然后小曲发现资金不够了,只够买一辆车送快递。
经营方式一 客户每送来一份快死,小曲就让一个快递员盯着,然后快递员开车去送快递。慢慢的小曲就发现了这种经营方式存在下述问题

  • 几十个快递员基本上时间都花在抢车上了,大部分快递员都处于闲置状态,谁抢到了车,谁就能去送快递
  • 随着快递的增多,快递员也越来越多,小曲发现快递店里越来越挤,没办法雇佣新的快递员了
  • 快递员之间的协调很花时间

综合上述缺点,小曲痛定思痛,提出了下面的经营模式
经营方式二 小曲只雇佣一个快递员。然后呢,客户送来的快递,小曲按送达地点标注号,然后依次放在一个地方。最后,那个快递员依次的去取快递,一次拿一个,然后开着车去送快递,送好了就回来拿下一个快递。
对比 上述两种经营方式对比,是不是明显觉得第二种,效率更高,更好呢。在上述比喻中:

  • 每个快递员 ——————> 每个线程
  • 每个快递 ——————> 每个socket(I/O流)
  • 快递的送达地点 ——————> socket的不同状态
  • 客户送快递请求 ——————> 客户端请求
  • 小曲的经营方式 ——————> 服务端运行的代码
  • 一辆车 ——————> CPU的核数
    于是我们有如下结论 1、经营方式一就是传统的并发模型,每个I/O流(快递)都有一个新的线程(快递员)管理。2、经营方式二就是I/O多路复用。只有单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。
    下面类比到真实的 redis 线程模型,如图所示:
    I/O多路复用
    参照上图,简单来说,就是我们的 redis-client 在操作的时候,会产生具有不同事件类型的 socket 。在服务端,有一段 I/O 多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。需要说明的是,这个 I/O 多路复用机制,redis 还提供了 select 、epoll 、evport 、kqueue 等多路复用函数库,大家可以自行去了解。

redis 的数据类型,以及每种数据类型的使用场景

(一)String
这个其实没啥好说的,最常规的 set/get 操作,value 可以是String 也可以是数字。一般做一些复杂的计数功能的缓存
(二)hash
这里 value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就用这种数据结构存储用户信息,以 cookieId 作为key,设置30分钟为缓存过期事件,能很好的模拟出类似的效果。
(三)list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用 lrange 命令,做基于 redis的分页功能,性能极佳,用户体验好。
(四)set
因为set堆放的是一堆不重复值得结合。所以可以做全局去重得功能。为什么不用JVM自带得 Set 进行去重?因为我们的系统一般都是集群部署,使用 JVM 自带的 Set,比较麻烦,难道为了做一个全局去重,再起一个公共服务,太麻烦了。另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的特殊喜好等功能。
(五)sorted set
sorted set 多了一个权重参数 score,集合中的元素能够按 score 进行排列,可以做排行榜应用,取 TOP N 操作。另外,参照另一篇《分布式之延时任务方案解析》,该文指出了 sorted set 可以用来做延时任务。最后一个应用就是可以做范围查找

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

分析:这个问题其实箱单重要,到底 redis 有没有用到家,这个问题就可以看出来。比如你 redis 只能存 5G 数据,可以你写了 10G ,那会删除 5G的的数据。怎么删除的,这个问题思考过吗?还有,你的数据已经设置了过期事件,但是时间到了,内存占用率还是比较高,有思考过原因吗?回答: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。
4) volatile-lru:当在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis 即当缓存,又做持久化存储的时候采用。
5)volatile-random:在设置了过期时间的键空间中,随机移除某个key。
6)volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key 优先移除。
ps:如果没有设置 expire 的key,不满足先决条件(prerequistes);那么 volatile-lrn,volatile-random 和volatile-ttl策略的行为,和 noevication(不删除)基本上一致。

渐进式 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 ,系统启动后启动检查线程,在某个热点 key 快要超时时,重新读取数据库数据到缓存,避免直接打穿。
  • 设置热点数据永远不过期

猜你喜欢

转载自blog.csdn.net/lxn1023143182/article/details/114302963