中间件 (一) --------- 缓存

末蓝、 2024-04-01 09:36 203阅读 0赞

目录

  • 一、为什么要使用缓存
  • 二、优秀的缓存系统Redis
  • 三、redis 为什么这么快
  • 四、redis 的数据类型,以及每种数据类型的使用场景
  • 五、redis的过期策略以及内存淘汰机制
  • 六、渐进式 ReHash
  • 七、缓存穿透
  • 八、缓存雪崩

一、为什么要使用缓存

A、性能

如下图所示,我们在碰到需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。

在这里插入图片描述

题外话:忽然想聊一下这个迅速响应的标准。其实根据交互效果的不同,这个响应时间没有固定标准。

不过曾经有人这么告诉我:

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

那么瞬间、刹那、一弹指具体是多少时间呢?

根据《摩诃僧祗律》记载

一刹那者为一念,二十念为一瞬,二十瞬为一弹指,二十弹指为一罗预,二十罗预为一须臾,一日一夜有三十须臾。

那么,经过周密的计算,一瞬间为0.36 秒,一刹那有 0.018 秒,一弹指长达 7.2 秒。

B、并发

如下图所示,在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用 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多路复用机制

题外话:我们现在要仔细的说一说 I/O多路复用机制,因为这个说法实在是太通俗了,通俗到一般人都不懂是什么意思。博主打一个比方:小曲在S城开了一家快递店,负责同城快送服务。小曲因为资金限制,雇佣了一批快递员,然后小曲发现资金不够了,只够买一辆车送快递。

经营方式一

客户每送来一份快递,小曲就让一个快递员盯着,然后快递员开车去送快递。慢慢的小曲就发现了这种经营方式存在下述问题

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

综合上述缺点,小曲痛定思痛,提出了下面的经营方式。

经营方式二

小曲只雇佣一个快递员。然后呢,客户送来的快递,小曲按送达地点标注好,然后依次放在一个地方。最后,那个快递员依次的去取快递,一次拿一个,然后开着车去送快递,送好了就回来拿下一个快递。

对比

上述两种经营方式对比,是不是明显觉得第二种,效率更高,更好呢。在上述比喻中:

  • 每个快递员 —————————> 每个线程
  • 每个快递 ——————————> 每个 socket (I/O流)
  • 快递的送达地点 ———————> socket 的不同状态
  • 客户送快递请求 ———————> 来自客户端的请求
  • 小曲的经营方式 ———————> 服务端运行的代码
  • 一辆车 ———————————> CPU的核数

于是我们有如下结论

1、经营方式一就是传统的并发模型,每个I/O流(快递)都有一个新的线程(快递员)管理。
2、经营方式二就是 I/O 多路复用。只有单个线程(一个快递员),通过跟踪每个I/O流的状态(每个快递的送达地点),来管理多个I/O流。

下面类比到真实的 redis 线程模型,如图所示

在这里插入图片描述

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

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

A、String

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

B、hash

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

**C、list **

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

D、set

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

E、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 中有一行配置

  1. maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的

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

六、渐进式 ReHash

A、渐进式 rehash 的原因

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

B、渐进式 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 的过期时间分布开来,不会集中在同一时刻失效。
  • 采用限流算法,限制流量。
  • 采用分布式锁,加锁访问。

发表评论

表情:
评论列表 (有 0 条评论,203人围观)

还没有评论,来说两句吧...

相关阅读

    相关 中间-消息中间

    jms是java ee 的消息规范,其实现是hornetq和activemq等,小型系统直接使用jms是可以的但是大型系统不太适用。 消息中间件主要保证消息的顺序保证,扩展性