
缓存
计算机存储的访问级别可以大致分为这几个级别(通过 CPU 访问):
1。 寄存器: CPU 直接访问的存储单元,速度最快,但容量有限。
2。 缓存: L1、L2、L3 缓存,用于缓解内存和 CPU 的速度瓶颈。
3。 内存: 可以直接访问 CPU,用于缓解外存和 CPU 的速度瓶颈。
4。 外存: 磁盘、机械硬盘、固态硬盘。。。,用于存储大量数据。
5。 网络: 网络的访问是最慢的,通常为 20ms ~ 999+ms。
上述的分级中,缓存描述的是 L1、L2、L3 这类高速缓存。但是什么是缓存主要还是其相对于什么来说,其实寄存器、内存、外存都可以看作是缓存。
- 从外存来看,内存就是外存的缓存,用于缓解外存和 CPU 的速度瓶颈。
- 从网络来看,外存就是网络的缓存,用于缓解网络传输带来的速度瓶颈,浏览器通常有缓存机制,对于那些静态资源,通常可以缓存到本地硬盘中,以便加快下次的访问。
关于 "二八定律" :20% 的热点数据,能够应对 80% 的访问场景。因此只需要把这少量的热点数据缓存起来,就可以应对大多数场景,从而在整体上有明显的性能提升。
Redis 就可以作为常见的关系型数据库的缓存(MySQL,PostgreSQL,Oracle 等)来使用。
对于这些关系型数据库,最大缺陷就是性能不高,每进行一次查询操作,就要消耗过多的资源。
为什么说关系型数据库的性能不高?
- 数据库把数据存储在硬盘上,硬盘的 IO 速度并不快。尤其是随机访问。
- 如果查询不能命中索引,就需要进行表的遍历,这就会大大增加硬盘 IO 次数。
- 关系型数据库对于 SQL 的执行会做一系列的解析,校验,优化工作。
- 如果是一些复杂查询,比如联合查询,需要进行笛卡尔积操作,效率更是降低很多。
所以,如果某一时间段内,服务器并发量高了,就很容易造成服务器服务不可用,也就是宕机。
如何让数据库能够承担更大的并发量呢?核心思路主要是两个:
- 开源:引入更多的机器,部署更多的数据库实例,构成数据库集群。(主从复制,分库分表等...)
- 节流:引入缓存,使用其他的方式保存经常访问的热点数据,从而降低直接访问数据库的请求数量。
Redis 的访问速度是比 MySQL 要快的,因此 Redis 可以作为 MySQL 的后盾,对于那些热点数据,不需要通过 MySQL 查询,而是直接从 Redis 中获取。只有当 Redis 中没有需要的数据时,才会去 MySQL 中查询。
Redis 作为缓存,是可以提高读速度的,但是写数据还是要往 MySQL 写入,是不能提高这部分的速度的。
缓存的更新策略
定时生成
每隔一定的周期(比如一天/一周/一个月),对于访问的数据频次进行统计。挑选出访问频次最高的前 N%
的数据。这些数据通常就是这些时间段的热点数据。
这种做法会导致数据的实时性不够高,对于那些短时的热点数据可能会统计不当。
实时生成
先给缓存设定容量上限(可以通过 Redis 配置文件的 maxmemory
参数设定)。接下来把用户每次查询:
- 如果在 Redis 查到了,就直接返回。
- 如果 Redis 中不存在,就从数据库查,把查到的结果同时也写入 Redis。如果缓存已经满了(达到上限),就触发缓存淘汰策略,把一些 "相对不那么热门" 的数据淘汰掉。
按照上述过程,持续一段时间之后 Redis 内部的数据自然就是 "热门数据" 了。
缓存淘汰策略
FIFO(First In First Out,先进先出):
- 将最早进入的数据淘汰,像队列一样。
LRU(Least Recently Used,最近最少未使用):
- 统计每个数据上一次到至今访问的时间。
- 淘汰那个时间最长的数据,该数据就是最近最久未使用的。
LFU(Least Frequently Used,最近最不常用):
- 统计每个数据被访问的次数。
- 淘汰那个访问次数最少的数据。
Random(随机淘汰):
- 随机选择一些数据,进行淘汰。
这些淘汰策略 Redis 都内置了:
volatile-lru
:当内存不足以容纳新写入数据时,从设置了过期时间的key
中使用 LRU(最近最少使用)算法进行淘汰。allkeys-lru
:当内存不足以容纳新写入数据时,从所有key
中使用LRU(最近最少使用)算法进行淘汰。volatile-lfu
:4.0
版本新增,当内存不足以容纳新写入数据时,在过期的key
中,使用 LFU 算法进行删除key
。allkeys-lfu
:4.0
版本新增,当内存不足以容纳新写入数据时,从所有key
中使用 LFU 算法进行淘汰。volatile-random
:当内存不足以容纳新写入数据时,从设置了过期时间的key
中,随机淘汰数据。allkeys-random
:当内存不足以容纳新写入数据时,从所有key
中随机淘汰数据。volatile-ttl
:在设置了过期时间的key
中,根据过期时间进行淘汰,越早过期的优先被淘汰。(相当于 FIFO,只不过是局限于过期的key
)noeviction
:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
Redis 生产问题
缓存预热(Cache Preheating)
在 Redis 首次接入整个服务时或者 Redis 很多 key
都是失效时,里面可能是没有任何数据的。此时依然会有大量的请求直接访问数据库,导致数据库压力过大。只有过了一段时间后,Redis 才会积累一定的数据量。
为了解决整个问题,可以预先将一批热点数据以离线的方式让入 Redis 中,此时这批热点数据就可以缓解数据库的一些请求压力了。这便是缓存预热。
缓存穿透(Cache Penetration)
缓存穿透说简单点就是大量请求的 key
是不合理的,根本不存在于缓存中,也不存在于数据库中。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
说白了就是 Redis 没有数据,MySQL 也没有数据,导致请求直接到了数据库上,Redis 没有发挥其作用。
造成缓存穿透的原因可能有几种:
- 业务设计不合理:比如缺少必要的参数校验环节,导致非法的 key 也被进行查询了。
- 开发/运维误操作:不小心把部分数据从数据库上误删了,而且短时间内还没有发现。
- 黑客恶意攻击:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库上。
如何解决?
- 缓存空值 :对于查询不到的数据,可以设置一个空值(比如
""
),这样下次再查询时,就可以直接返回空值了。 - 参数校验 :对于需要的参数或
key
进行较为严格参数校验,避免非法的key
被查询。 - 布隆过滤器 :
- 布隆过滤器就是 位图 + hash 的一种数据结构,可以用非常小的空间和非常快的速度判断某个数据是否存在于集合中。
- 可以把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程(访问 Redis 或者 MySQL)。
缓存雪崩(Cache Avalanche)
在短时间内,Redis 中的数据大规模失效,大量的请求都直接访问 MySQL,造成 MySQL 服务器压力迅速上升,甚至宕机。这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
也有可能是 Redis 服务故障了,导致所有的请求都落到了 MySQL 上。
如何解决?
- 提高 Redis 的可用性:采用主从模式、哨兵模式、集群模式,保证 Redis 的高可用。
- 随机缓存时间:对于数据的过期时间,可以随机设置(例如在固定过期时间的基础上加上一个随机值)。不要大量数据都设置同一个过期时间,否则很容易造成雪崩。
- 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略。
缓存击穿(Cache Breakdown)
在短时间内,大量热点数据失效,导致大量请求直接访问数据库,导致数据库压力过大,甚至宕机。这是针对热点数据来说的。
如何解决?
- 热点数据永不过期:对于热点数据,可以设置永不过期,这样即使热点数据失效,也不会造成缓存击穿。
- 服务降级:引入分布式锁,限制同时请求数据库的并发数。
缓存穿透和缓存击穿有什么区别?
- 缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。
- 缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)
缓存雪崩和缓存击穿有什么区别?
- 缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在于缓存中(通常是因为缓存中的那份数据已经过期)。