缓存(4):常见缓存 概念、问题、现象 及 预防问题
常见缓存概念
缓存特征: 命中率、最大元素、清空策略
命中率:命中率=返回正确结果数/请求缓存次数
它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。
最大元素(最大空间):缓存中可以存放的最大元素的数量
,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间),那么将会触发缓存启动清空策略
根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的时候缓存。
清空策略
:缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳定服务的同时有效提升命中率?这就由缓存清空策略来处理,设计适合自身数据特征的清空策略能有效提升命中率。
缓存介质
从硬件介质区分:内存、硬盘
从技术上区分:内存、硬盘文件、数据库
内存
:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原
。硬盘
:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的
。数据库
:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力
。像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。
缓存分类
根据缓存和应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存)
本地缓存:指的是在应用中的缓存组件
,- 优点:应用**和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,**在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;
- 缺点:缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。
分布式缓存:指的是与应用分离的缓存组件或服务
,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。
本地缓存实现
有两种方式做到本地缓存的实现:
- 自己实现
- 使用已有的框架
直接实现缓存
只需要简单的缓存数据的功能,而无需关注更多存取、清空策略等深入的特性时,直接编程实现缓存则是最便捷和高效的
成员变量或局部变量实现
:以局部变量map结构缓存部分业务数据,减少频繁的重复数据库I/O操作。缺点仅限于类的自身作用域内,类间无法共享缓存
。静态变量实现:通过静态变量一次获取缓存内存中,减少频繁的I/O读取
,静态变量实现类间可共享,进程内可共享,缓存的实时性稍差
。
这类缓存实现,优点是能直接在heap区内读写,最快也最方便
;
缺点同样是受heap区域影响,缓存的数据量非常有限,同时缓存时间受GC影响
。
主要满足单机场景下的小数据量缓存需求,同时对缓存数据的变更无需太敏感感知
,如上一般配置管理、基础静态数据等场景。
缓存框架
Ehcache、Guava Cache(架构设计灵感来源于ConcurrentHashMap)、
Caffeine、java集合缓存(concurrentMap)、redis缓存
Caffeine :适用于单机应用,数据量较小且要求极低延迟的场景
。可以设定删除时间等删除条件,Caffeine 的读写能力显著高于ConcurrentHashMap 。支持更多的过期/回收策略
ConcurrentHashMap :适用于简单的本地缓存需求,读多写少的场景
。只能动态添加保存,除非显示的删除(有可能内存溢出)
Redis :适用于大规模分布式应用,要求高可用性和数据持久化的场景
。
支持分布式缓存是redis,其他的都是应用中的 基于本地应用 的缓存,即本地缓存。
缓存穿透
什么是缓存穿透
缓存穿透是**指用户查询数据,在数据库没有,自然在缓存中也不会有
**。
这样就导致用户查询的时候,在缓存中找不到对应key的value,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)
。这样请求就绕过缓存直接查数据库
缓存穿透是指:用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在 缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请 求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题
。
预防缓存穿透
方法1:最常见的则是采用布隆过滤器
,将所有可能存在的数据哈 希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存 储系统的查询压力。
方法2:如果一个查询返回的数据为空(不管是数据不 存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
方法3:直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库
。
方法4:拦截无效请求
总结:
-
缓存空值
:如果一个查询返回的数据为空(不管是数据不 存在,还是系统故障)- 我们把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
- 通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库;
-
采用布隆过滤器BloomFilter
:优势占用内存空间很小,bit存储。性能特别高。- 将所有可能存在的数据哈希到一个足够大的 bitmap 中,
一个一定不存在的数据会被这个bitmap 拦截掉,从而避免了对底层存储系统的查询压力
;
-
参数校验
- 原理:在接收请求参数时,进行严格的校验,确保请求参数的合法性。
- 实现:对请求参数进行格式、范围等校验,过滤掉不合法的请求,减少无效查询。
-
限流
- 原理:对请求进行限流,控制单位时间内的请求数量,避免瞬间大量请求打到数据库。
- 实现:使用令牌桶、漏斗等算法实现限流,保护数据库的稳定性。
-
使用默认值
- 原理:
对于某些常见的请求,可以设置默认值,避免每次都查询数据库
。 - 实现:在应用层中,针对特定的请求返回默认值,减少对数据库的访问。
- 原理:
-
监控与报警
- 原理:实时监控请求的情况,及时发现异常请求。
- 实现:设置监控指标,如请求频率、数据库负载等,发现异常时及时报警,进行处理。
-
使用 API 网关
- 原理:通过 API 网关对请求进行统一管理和控制。
- 实现:在 API 网关层面进行请求的过滤、校验和限流,减少无效请求直接到达后端服务。
缓存击穿
什么是缓存击穿
缓存击穿与 缓存穿透的简单区别
- 缓存击穿是指
数据库中有数据,但是缓存中没有
,大量的请求打到数据库; - 缓存穿透是指
缓存和数据库中都没有的数据
,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
二级缓存缓存击穿解决方案
方法1:设置热点数据永远不过期
。
方法2: 如果过期则或者在快过期之前更新,如有变化,主动刷新缓存数据,同时也能保障数据一致性
方法3: 加互斥锁,保障缓存中的数据,被第一次请求回填
。此方案不适用于超高并发场景
缓存雪崩
什么是缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩
。
由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU 和内存造成巨大压力,严重的会造成数据库宕机!
缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间所有原本应该访问缓存的请求都 去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列 连锁反应,造成整个系统崩溃
。
预防缓存雪崩
一般有三种处理办法:
- 一般
并发量不是特别多
的时候,使用最多的解决方案是加锁排队
。 - 给
每一个缓存数据增加相应的缓存标记
,记录缓存的是否失效,如果缓存标记失效,则更新数据缓 存
。 - 为
key 设置不同的缓存失效时间
。
总结:
-
设置
合理的缓存过期时间
随机过期时间
:为不同的缓存数据设置不同的过期时间,避免同一时间大量缓存失效。滑动过期
:在每次访问时,更新缓存的过期时间,确保热点数据不会在短时间内失效。
-
使用
互斥锁
加锁机制:在缓存失效时,使用分布式锁(如 Redis 的 SETNX)来控制只有一个请求能去加载数据,其他请求等待,避免同时访问数据库
。
-
预热缓存
提前加载:在系统启动时或在特定时间段内,提前将热点数据加载到缓存中,减少高并发时的数据库压力
-
使用多级缓存
多层缓存:在应用层和数据库之间引入多级缓存(如本地缓存 + 分布式缓存),减少对数据库的直接访问
。
-
监控与报警
- 实时监控:监控缓存命中率、数据库负载等指标,及时发现异常情况并进行处理。
- 报警机制:设置报警机制,当数据库负载过高时,及时通知运维人员进行处理。
-
流量控制
- 限流:对请求进行限流,避免瞬间大量请求涌入数据库,保护数据库的稳定性。
-
使用消息队列
- 异步处理:将请求放入消息队列中,异步处理数据加载,减少对数据库的直接压力。
-
降级处理
- 备用方案:在缓存失效或数据库压力过大时,提供降级服务,比如返回默认值或缓存中的旧数据,确保系统的可用性。
缓存污染
缓存污染,指留存在缓存中的数据,实际不会被再次访问了,但又占据了缓存空间
。
由于缓存空间有限,热点数据被置换或者驱逐出去了,而一些后面不用到的数据却反而被留下来,从而缓存数据命中率急剧下降
解决缓存污染的关键点是能识别出热点数据,或者未来更有可能被访问到的数据
换句话说: 是要提升 缓存数据命中率
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统
。
作用:避免在用户请求的时候, 先查询数据库,然后再将数据缓存的问题
!用户直接查询事先被预热的缓存数据!
缓存更新策略
缓存更新除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以 根据具体的业务需求进行自定义的缓存淘汰
,
常见的策略有两种:
(1)定时去清理过期的缓存
;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数 据并更新缓存
。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然 需要保证服务还是可用的,即使是有损服务。
系统可以根据一些关键数据进行自动降级,也可以配置开 关实现人工降级
。
降级的目的
核心服务可用,即使是有损的。而且有些服务是无法降级的 (如加入购物车、结算)
在缓存数据不可用的情况下,依然能够提供服务,避免系统崩溃或响应时间过长
。
实现方式:
直接访问数据库
:在缓存失效时,直接从数据库中读取数据。返回默认值
:在无法获取数据时,返回一个预设的默认值。异步更新
:在降级的同时,异步更新缓存,以便在下次请求时能够快速响应。