0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何设计一个缓存系统?

数据分析与开发 来源:CSDN 作者:zeb_perfect 2021-02-08 11:40 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

设计一个缓存系统,不得不要考虑的问题就是:缓存穿透、缓存击穿与失效时的雪崩效应。

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

1.使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间,所以这里给出两种版本代码参考:

//2.6.1前单机版本锁 Stringget(Stringkey){ Stringvalue=redis.get(key); if(value==null){ if(redis.setnx(key_mutex,"1")){ //3mintimeouttoavoidmutexholdercrash redis.expire(key_mutex,3*60) value=db.get(key); redis.set(key,value); redis.delete(key_mutex); }else{ //其他线程休息50毫秒后重试 Thread.sleep(50); get(key); } } }

最新版本代码:

publicStringget(key){ Stringvalue=redis.get(key); if(value==null){//代表缓存值过期 //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能loaddb if(redis.setnx(key_mutex,1,3*60)==1){//代表设置成功 value=db.get(key); redis.set(key,value,expire_secs); redis.del(key_mutex); }else{//这个时候代表同时候的其他线程已经loaddb并回设到缓存了,这时候重试获取缓存值即可 sleep(50); get(key);//重试 } }else{ returnvalue; } }

memcache代码:

if(memcache.get(key)==null){ //3mintimeouttoavoidmutexholdercrash if(memcache.add(key_mutex,3*60*1000)==true){ value=db.get(key); memcache.set(key,value); memcache.delete(key_mutex); }else{ sleep(50); retry(); } }

2. "提前"使用互斥锁(mutex key):

在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:

v=memcache.get(key); if(v==null){ if(memcache.add(key_mutex,3*60*1000)==true){ value=db.get(key); memcache.set(key,value); memcache.delete(key_mutex); }else{ sleep(50); retry(); } }else{ if(v.timeout<= now()) {           if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {               // extend the timeout for other threads               v.timeout += 3 * 60 * 1000;               memcache.set(key, v, KEY_TIMEOUT * 2);                  // load the latest value from db               v = db.get(key);               v.timeout = KEY_TIMEOUT;               memcache.set(key, value, KEY_TIMEOUT * 2);               memcache.delete(key_mutex);           } else {               sleep(50);               retry();           }       }   } 

3. "永远不过期":

这里的“永远不过期”包含两层意思:

(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

Stringget(finalStringkey){ Vv=redis.get(key); Stringvalue=v.getValue(); longtimeout=v.getTimeout(); if(v.timeout<= System.currentTimeMillis()) {               // 异步更新后台异常执行               threadPool.execute(new Runnable() {                   public void run() {                       String keyMutex = "mutex:" + key;                       if (redis.setnx(keyMutex, "1")) {                           // 3 min timeout to avoid mutex holder crash                           redis.expire(keyMutex, 3 * 60);                           String dbValue = db.get(key);                           redis.set(key, dbValue);                           redis.delete(keyMutex);                       }                   }               });           }           return value;   }

4. 资源保护:

采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。

没有最佳只有最合适

解决方案 优点 缺点
简单分布式互斥锁(mutex key) 1. 思路简单
2. 保证一致性
1. 代码复杂度增大
2. 存在死锁的风险
3. 存在线程池阻塞的风险
“提前”使用互斥锁 1. 保证一致性 同上
不过期(本文) 1. 异步构建缓存,不会阻塞线程池 1. 不保证一致性。
2. 代码复杂度增大(每个value都要维护一个timekey)。
3. 占用一定的内存空间(每个value都要维护一个timekey)。
资源隔离组件hystrix(本文) 1. hystrix技术成熟,有效保证后端。
2. hystrix监控强大。
1. 部分访问存在降级策略。

四种方案来源网络,详文链接:http://carlosfu.iteye.com/blog/2269687

总结

针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。

最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。

原文标题:如何设计缓存系统:缓存穿透,缓存击穿,缓存雪崩解决方案分析

文章出处:【微信公众号:数据分析与开发】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 存储
    +关注

    关注

    13

    文章

    4883

    浏览量

    90251
  • 缓存
    +关注

    关注

    1

    文章

    248

    浏览量

    27807

原文标题:如何设计缓存系统:缓存穿透,缓存击穿,缓存雪崩解决方案分析

文章出处:【微信号:DBDevs,微信公众号:数据分析与开发】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    京东缓存中间件架构与缓存内核优化

    、京东缓存中间件架构 1、背景 在当今高并发、分布式的系统架构中,缓存已成为提升应用性能、降低数据库负载的核心组件。随着业务规模的扩大与系统
    的头像 发表于 04-03 16:18 1803次阅读
    京东<b class='flag-5'>缓存</b>中间件架构与<b class='flag-5'>缓存</b>内核优化

    KeepAlive:组件缓存实现深度解析

    我们学习了 Suspense 如何处理异步组件加载。今天,我们将探索Vue3中另一个强大的特性:KeepAlive。它允许我们在组件切换时缓存组件实例,避免重复渲染,极大地提升了用户体验和性能
    发表于 03-05 19:17

    C语言的缓冲区(缓存)详解

    缓冲区又称为缓存,它是内存空间的部分。也就是说,在内存空间中预留了定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。   缓冲区根据其对应的是输入设备还是输出设备
    发表于 01-14 07:30

    ESP32 编译过程中 bootloader 配置阶段的 CMake 缓存冲突错误,记录

    你遇到的是 ESP32 编译过程中 bootloader 配置阶段的 CMake 缓存冲突错误,核心原因是系统中混合了 ESP-IDF v5.5.1 和 v5.4.3 两版本的路径,导致
    发表于 12-23 07:07

    SSD为何需要DRAM缓存?天硕工业级SSD带来深度解析!

    在当今数字化转型的浪潮中,工业存储设备的选择直接关系到整个系统的稳定性和效率。天硕工业级SSD固态硬盘凭借其卓越的DRAM缓存技术,在众多应用场景中展现出独特优势。本文将采用问答形式,深入探讨这
    的头像 发表于 10-20 17:59 1035次阅读
    SSD为何需要DRAM<b class='flag-5'>缓存</b>?天硕工业级SSD带来深度解析!

    串口DMA发送有缓存吗?

    串口DMA发送有缓存吗, 我是从ringbuffer取出来,放到申请的缓存里,启动串口DMA发送,然后就释放了。暂时没发现什么问题。 用的drv_usart.c是这个版本
    发表于 10-10 06:14

    Redis缓存的经典问题和解决方案

    用户疯狂查询数据库中不存在的数据,每次查询都绕过缓存直接打到数据库,导致数据库压力骤增。
    的头像 发表于 08-20 16:24 887次阅读

    缓存之美:万文详解 Caffeine 实现原理(上)

    文章将采用“总-分-总”的结构对配置固定大小元素驱逐策略的 Caffeine 缓存进行介绍,首先会讲解它的实现原理,在大家对它有概念之后再深入具体源码的细节之中,理解它的设计理念,从中能学习到
    的头像 发表于 08-05 14:49 801次阅读
    <b class='flag-5'>缓存</b>之美:万文详解 Caffeine 实现原理(上)

    本地缓存 Caffeine 中的时间轮(TimeWheel)是什么?

    我们详细介绍了 Caffeine 缓存添加元素和读取元素的流程,并详细解析了配置固定元素数量驱逐策略的实现原理。在本文中我们将主要介绍 配置元素过期时间策略的实现原理 ,补全 Caffeine
    的头像 发表于 08-05 14:48 691次阅读
    本地<b class='flag-5'>缓存</b> Caffeine 中的时间轮(TimeWheel)是什么?

    harmony-utils之CacheUtil,缓存工具类

    harmony-utils之CacheUtil,缓存工具类
    的头像 发表于 07-04 16:36 588次阅读

    高性能缓存设计:如何解决缓存伪共享问题

    在多核高并发场景下, 缓存伪共享(False Sharing) 是导致性能骤降的“隐形杀手”。当不同线程频繁修改同缓存行(Cache Line)中的独立变量时,CPU缓存
    的头像 发表于 07-01 15:01 877次阅读
    高性能<b class='flag-5'>缓存</b>设计:如何解决<b class='flag-5'>缓存</b>伪共享问题

    请问如何增大usb3.0从设备fifo接口固件中的写dma缓存大小?

    现有的固件是默认的,分别配置了21KB的缓存给读和写的dma。我想要多分配缓存给写dma,比如分配4kB给写dma。请教下该如何修改
    发表于 05-14 08:13

    cyusb3014 slave fifo模式In和Out缓存大小不样时,显示错误怎么解决?

    cyusb3014 slave fifo 模式 In 和 Out 缓存大小设置不样时(比如:U2P DMA缓存16K,P2U DMA缓存1K),可以测出来实际就是设置值,但在USB
    发表于 05-13 06:55

    MCU缓存设计

    MCU 设计通过优化指令与数据的访问效率,显著提升系统性能并降低功耗,其核心架构与实现策略如下: 缓存类型与结构 指令缓存(I-Cache)与数据
    的头像 发表于 05-07 15:29 1266次阅读

    Nginx缓存配置详解

    Nginx 是功能强大的 Web 服务器和反向代理服务器,它可以用于实现静态内容的缓存缓存可以分为客户端缓存和服务端
    的头像 发表于 05-07 14:03 1377次阅读
    Nginx<b class='flag-5'>缓存</b>配置详解