查看“关于:缓存穿透、缓存击穿、缓存雪崩、热点数据失效”的源代码
←
关于:缓存穿透、缓存击穿、缓存雪崩、热点数据失效
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您请求的操作仅限属于该用户组的用户执行:
用户
您可以查看和复制此页面的源代码。
[[category:Redis]] == 关于 == 项目通常会引入NoSQL技术(即:基于内存的数据库,如redis),来应对大数据量的需求,以规避磁盘读/写速度比较慢的问题而存在的性能弊端;<br/> 但是,引入redis等又有可能出现缓存穿透,缓存击穿,缓存雪崩等问题,并需要开发对这些问题进行考量并作出应对。 == 缓存穿透(请求一个“一定不存在的数据”) == 缓存穿透:'''请求一个不存在的数据''',缓存和数据库都查不到这个数据,每次都会去数据库查询。 : 比如:用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。 === 解决方案 === 有很多种方法可以有效地解决缓存穿透问题: # '''BloomFilter''':(“布隆过滤器”)将所有可能存在的数据哈希到一个足够大的bitmap中:每次查询的时候都先去BloomFilter判断,如果没有就直接返回null;如果存在再进行查缓存、DB查询; #* 注意 BloomFilter 没有删除操作,对于删除的 key,可以在缓存中缓存 null; #* 见:“'''[[什么是布隆过滤器(BloomFilter)?]]'''” # '''缓存空值''':如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然'''为这些key在缓存中设置对应的值为null'''。 #* 要对这些key设置过期时间,以防止真的有数据; #: <syntaxhighlight lang="java"> //伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) { return cacheValue; } else { //数据库查询不到,为空 cacheValue = GetProductListFromDB(); if (cacheValue == null) { //如果发现为空,设置个默认值,也缓存起来 cacheValue = string.Empty; } CacheHelper.Add(cacheKey, cacheValue, cacheTime); return cacheValue; } } </syntaxhighlight> * BloomFilter可以结合缓存空值用; * 针对于一些恶意攻击,攻击带过来的大量key是不存在的,可以考虑使用BloomFilter进行过滤。 == 缓存击穿(被大量请求的“单个热点数据”过期) == 缓存击穿:'''大量的请求同时查询同一个key'''时,此时这个'''key正好失效'''了,就会导致同一时间,这些请求都会去查询数据库,这样的现象我们称为缓存击穿。 === 解决方案 === 针对“热点数据过期”: # 设置热点数据永不过期: #* 物理不过期,针对热点key不设置过期时间; #* 逻辑过期,把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建; 针对“大量请求”: # 采用'''分布式锁''',只有'''拿到锁的第一个线程去请求数据库,然后插入缓存''',当然每次拿到锁的时候都要去查询一下缓存有没有; #: <syntaxhighlight lang="java"> //伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; String lockKey = cacheKey; String cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { synchronized(lockKey) { cacheValue = CacheHelper.get(cacheKey); if (cacheValue != null) { return cacheValue; } else { // 这里一般是sql查询数据 cacheValue = GetProductListFromDB(); CacheHelper.Add(cacheKey, cacheValue, cacheTime); } } return cacheValue; } } </syntaxhighlight> == 缓存雪崩(缓存数据“集体过期”) == 缓存雪崩:当某一时刻发生'''大规模的缓存失效'''的情况。 可能出现的情况: # 数据过期时间相同, # 缓存服务宕机。 === 解决方案 === 针对服务器宕机: # 采用集群,降低服务宕机的概率; # ehcache本地缓存 + Hystrix限流&降级;【???】 #* ehcache 本地缓存:在 Redis Cluster 完全不可用的时候,ehcache 本地缓存做临时支持; #* Hystrix进行限流 & 降级 ,比如一秒来了5000个请求,我们可以设置假设只能有一秒 2000个请求能通过这个组件,那么其他剩余的 3000 请求就会走限流逻辑; 针对过期时间相同: * 事前: *# 均匀过期:设置不同的过期时间。 *# 分级缓存:第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。 *# 热点数据缓存永远不过期。 *#* 物理不过期,针对热点key不设置过期时间 *#* 逻辑过期,把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建 * 事中: *# '''互斥锁'''(或分布式锁):在缓存失效后,通过互斥锁或者队列来控制读数据写缓存的线程数量。降低系统的吞吐量,以减小数据库压力。 *# 使用'''熔断机制''',限流降级:当流量达到一定的阈值,直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上将数据库击垮,至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。 == 热点数据失效 == 热点数据失效:缓存的“'''热点数据'''”“'''集体过期'''”;【击穿 + 雪崩???】 === 解决方案 === 针对缓存过期: # 为缓存'''设置不同的失效时间'''(原有的失效时间基础上增加一个随机值),避免集体失效; # 采用加锁或队列,来保证不会有大量的线程对数据库一次性进行读写;(缓存击穿的解决办法) #* 加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。甚至可能带来分布式锁的问题,因此,在真正的高并发场景下很少使用! # 永不失效,就是采用定时任务对快要失效的缓存进行更新缓存和失效时间; # (设置过期标志更新缓存:) #: <syntaxhighlight lang="java"> //伪代码 public object GetProductListNew() { int cacheTime = 30; String cacheKey = "product_list"; //缓存标记 String cacheSign = cacheKey + "_sign"; String sign = CacheHelper.Get(cacheSign); //获取缓存值 String cacheValue = CacheHelper.Get(cacheKey); if (sign != null) { return cacheValue; //未过期,直接返回 } else { CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) -> { //这里一般是 sql查询数据 cacheValue = GetProductListFromDB(); //日期设缓存时间的2倍,用于脏读 CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2); }); return cacheValue; } } </syntaxhighlight> #* 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存; #* 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。 == 其他缓存概念 == === 缓存预热 === 缓存预热是指'''系统上线后,提前将相关的缓存数据加载到缓存系统'''。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。 * 如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。 缓存预热解决方案: # 数据量不大的时候,工程启动的时候进行加载缓存动作; # 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新; # 数据量太大的时候,优先保证热点数据进行提前加载到缓存。 === 缓存降级 === 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 降级的最终目的是保证核心服务可用,即使是有损的。 * 降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。 * 而且有些服务是无法降级的(如加入购物车、结算)。 以参考日志级别设置预案: # 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; # 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; # 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级; # 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略。 : 例如一个比较常见的做法就是,'''Redis出现问题,不去数据库查询,而是直接返回默认数据(或访问服务的内存数据)给用户'''。 * 在项目实战中通常会将部分热点数据缓存到服务的'''内存'''中,这样一旦缓存出现异常,可以直接使用服务的内存数据,从而避免数据库遭受巨大压力。
返回至“
关于:缓存穿透、缓存击穿、缓存雪崩、热点数据失效
”。
导航菜单
个人工具
登录
命名空间
页面
讨论
大陆简体
已展开
已折叠
查看
阅读
查看源代码
查看历史
更多
已展开
已折叠
搜索
导航
首页
最近更改
随机页面
MediaWiki帮助
笔记
服务器
数据库
后端
前端
工具
《To do list》
日常
阅读
电影
摄影
其他
Software
Windows
WIKIOE
所有分类
所有页面
侧边栏
站点日志
工具
链入页面
相关更改
特殊页面
页面信息