【 Redis | 完结篇 缓存优化 】

article/2025/6/4 5:47:32

前言:本节包含常见redis缓存问题,包含缓存一致性问题,缓存雪崩,缓存穿透,缓存击穿问题及其解决方案

1. 缓存一致性

我们先看下目前企业用的最多的缓存模型。缓存的通用模型有三种:

缓存模型解释
Cache Aside

由缓存调用者自己维护数据库与缓存的一致性

查询时:命中则直接返回,未命中则查询数据库并写入缓存

更新时:更新数据库并删除缓存,查询时自然会更新缓存

Read/Write Through

数据库自己维护一份缓存,底层实现对调用者透明

查询时:命中则直接返回,未命中则查询数据库并写入缓存

判断缓存是否存在,不存在直接更新数据库。存在则更新缓存,同步更新数据库

Write Behind Caching读写操作都直接操作缓存,由线程异步的将缓存数据同步到数据库

目前项目中中使用最多的是Cache Aside模式,因为实现起来非常简单。

在Cache Aside模式中,以下有两点需要注意:

1.在对数据库进行增删改操作时,需要加入清理缓存逻辑

在数据库进行增删改等操作时,数据库中数据会发生变化,但是Redis中缓存的数据未发生变化从而导致数据库和Redis中的缓存的数据不一致,故而在对数据库进行操作时,需要加入清理缓存逻辑来清理Redis中对应的未同步缓存数据

2.先更新数据库再删除缓存的方案

异常情况说明:

  • 线程1查询缓存未命中,于是去查询数据库,查询到旧数据

  • 线程1将数据写入缓存之前,线程2来了,更新数据库,删除缓存

  • 线程1执行写入缓存的操作,写入旧数据

       可以发现,异常状态发生的概率极为苛刻,线程1必须是查询数据库已经完成,但是缓存尚未写入之前。线程2要完成更新数据库同时删除缓存的两个操作。要知道线程1执行写缓存的速度在毫秒之间,速度非常快,在这么短的时间要完成数据库和缓存的操作,概率非常之低。

面试题如何保证缓存的双写一致性

:缓存的双写一致性很难保证强一致,只能尽可能降低不一致的概率,确保最终一致。我们项目中采用的是Cache Aside模式。简单来说,就是在更新数据库之后删除缓存;在查询时先查询缓存,如果未命中则查询数据库并写入缓存。同时我们会给缓存设置过期时间作为兜底方案,如果真的出现了不一致的情况,也可以通过缓存过期来保证最终一致。

追问:为什么不采用延迟双删机制?

:延迟双删的第一次删除并没有实际意义,第二次采用延迟删除主要是解决数据库主从同步的延迟问题,我认为这是数据库主从的一致性问题,与缓存同步无关。既然主节点数据已经更新,Redis的缓存理应更新。而且延迟双删会增加缓存业务复杂度,也没能完全避免缓存一致性问题,投入回报比太低。


2. 缓存穿透

什么是缓存穿透呢?

我们知道,当请求查询缓存未命中时,需要查询数据库以加载缓存。但是大家思考一下这样的场景:

如果我访问一个数据库中也不存在的数据。会出现什么现象?

        由于数据库中不存在该数据,那么缓存中肯定也不存在。因此不管请求该数据多少次,缓存永远不可能建立,请求永远会直达数据库。

        假如有不怀好意的人,开启很多线程频繁的访问一个数据库中也不存在的数据。由于缓存不可能生效,那么所有的请求都访问数据库,可能就会导致数据库因过高的压力而宕机。


2.1 缓存空值

简单来说,就是当我们发现请求的数据即不存在与缓存,也不存在与数据库时,将空值缓存到Redis,避免频繁查询数据库。实现思路如下:

核心思路如下:

在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的

现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。


2.2 布隆过滤器

布隆过滤是一种数据统计的算法,用于检索一个元素是否存在一个集合中。

一般我们判断集合中是否存在元素,都会先把元素保存到类似于树、哈希表等数据结构中,然后利用这些结构查询效率高的特点来快速匹配判断。但是随着元素数量越来越多,这种模式对内存的占用也越来越大,检索的速度也会越来越慢。而布隆过滤的内存占用小,查询效率却很高。

此时,我们要判断元素是否存在,只需要再次基于Khash函数做运算, 得到K个角标,判断每个角标的位置是不是1:

  • 只要全是1,就证明元素存在

  • 任意位置为0,就证明元素一定不存在

假如某个元素本身并不存在,也没添加到布隆过滤器过。但是由于存在hash碰撞的可能性,这就会出现这个元素计算出的角标已经被其它元素置为1的情况。那么这个元素也会被误判为已经存在。

因此,布隆过滤器的判断存在误差:

  • 当布隆过滤器认为元素不存在时,它肯定不存在

  • 当布隆过滤器认为元素存在时,它可能存在,也可能不存在

我们可以把数据库中的数据利用布隆过滤器标记出来,当用户请求缓存未命中时,先基于布隆过滤器判断。如果不存在则直接拒绝请求,存在则去查询数据库。尽管布隆过滤存在误差,但一般都在0.01%左右,可以大大减少数据库压力。

面试题如何解决缓存穿透问题

:缓存穿透也可以说是穿透攻击,具体来说是因为请求访问到了数据库不存在的值,这样缓存无法命中,必然访问数据库。如果高并发的访问这样的接口,会给数据库带来巨大压力。

我们项目中都是基于布隆过滤器来解决缓存穿透问题的,当缓存未命中时基于布隆过滤器判断数据是否存在。如果不存在则不去访问数据库。

当然,也可以使用缓存空值的方式解决,不过这种方案比较浪费内存。


3. 缓存雪崩

面试题如何解决缓存雪崩问题

:缓存雪崩的常见原因有两个,第一是因为大量key同时过期。针对问这个题我们可以可以给缓存key设置不同的TTL值,避免key同时过期。

第二个原因是Redis宕机导致缓存不可用。针对这个问题我们可以利用集群提高Redis的可用性。也可以添加多级缓存,当Redis宕机时还有本地缓存可用。


4.缓存击穿

4.1 互斥锁

核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询。

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿。

操作锁的代码:

核心思路就是利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。


private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);
}private void unlock(String key) {stringRedisTemplate.delete(key);
}

操作代码:

 public Shop queryWithMutex(Long id)  {String key = CACHE_SHOP_KEY + id;// 1、从redis中查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get("key");// 2、判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判断命中的值是否是空值if (shopJson != null) {//返回一个错误信息return null;}// 4.实现缓存重构//4.1 获取互斥锁String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 4.2 判断否获取成功if(!isLock){//4.3 失败,则休眠重试Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根据id查询数据库shop = getById(id);// 5.不存在,返回错误if(shop == null){//将空值写入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回错误信息return null;}//6.写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.释放互斥锁unlock(lockKey);}return shop;}

4.2 逻辑过期

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return shop;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建缓存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回过期的商铺信息return shop;
}

面试题如何解决缓存击穿问题

:缓存击穿往往是由热点Key引起的,当热点Key过期时,大量请求涌入同时查询,发现缓存未命中都会去访问数据库,导致数据库压力激增。解决这个问题的主要思路就是避免多线程并发去重建缓存,因此方案有两种。

第一种是基于互斥锁,当发现缓存未命中时需要先获取互斥锁,再重建缓存,缓存重建完成释放锁。这样就可以保证缓存重建同一时刻只会有一个线程执行。不过这种做法会导致缓存重建时性能下降严重。

第二种是基于逻辑过期,也就是不给热点Key设置过期时间,而是给数据添加一个过期时间的字段。这样热点Key就不会过期,缓存中永远有数据。

查询到数据时基于其中的过期时间判断key是否过期,如果过期开启独立新线程异步的重建缓存,而查询请求先返回旧数据即可。当然,这个过程也要加互斥锁,但由于重建缓存是异步的,而且获取锁失败也无需等待,而是返回旧数据,这样性能几乎不受影响。

需要注意的是,无论是采用哪种方式,在获取互斥锁后一定要再次判断缓存是否命中,做dubbo check. 因为当你获取锁成功时,可能是在你之前有其它线程已经重建缓存了。


http://www.hkcw.cn/article/aKYNuryMIj.shtml

相关文章

深入理解8086汇编:串传送、寻址、循环与引导扇区编程

在计算机底层编程中,汇编语言是不可或缺的工具。今天我们将通过一个8086汇编的引导扇区程序,详细讲解串传送指令、寄存器与寻址方式、循环控制以及标志寄存器等核心知识点。这个程序会在屏幕上显示一段文本和一个数字(初始为0)&am…

87万元现金遗落高铁车站 20分钟极速寻回

端午佳节,南粤大地人潮涌动,深圳北站迎来了52万人次的出发到达旅客。在这场流动的节日图景中,一场关乎87万元现金的“物品寻亲记”悄然上演。铁路工作人员以20分钟的高效协作,让失散的巨额财物与主人重新团聚,在传统节日里书写下新的温暖刻度。5月31日9时41分,深圳北站客…

德国汉堡一医院发生火灾致3死54伤

△图片来源:HamburgNews德国汉堡市霍恩费尔德区一医院6月1日凌晨发生火灾,造成3名患者死亡、54人受伤。目前,火灾发生原因仍在调查中。据德新社报道,大火发生在该医院专门诊治老年患者的科室一楼,并蔓延至二楼。除3人死亡外,还有两名伤者生命垂危,另有16人重伤、36人轻伤…

女子吃了半根甘蔗确诊美女病 颞下颌关节紊乱

23岁的杭州姑娘小吴自从两个月前啃了半根甘蔗后,嘴巴忽然张不开了。她只能吃软的、流质的食物,一嚼就痛,已经两个月没能好好吃饭了。在杭州市中医院针灸康复科的诊室里,小吴愁眉苦脸,因为疼痛,说话也有些痛苦。磁共振检查结果显示,小吴颞下颌关节盘不可复性前移位,属于…

长春一龙舟S型走位直冲岸边 引发游客欢笑

5月31日,正值端午佳节,长春2025端午龙舟赛在伊通河上拉开帷幕。比赛河畔长约300米,挤满了前来观赛的游客。上午11时左右,随着哨声响起,两条龙舟敲鼓出击。1号龙舟一马当先,几秒钟内便遥遥领先。然而,此时岸边传来阵阵笑声,4号龙舟驶离起点后竟然走起了S型路线。许多游客…

孙中山长孙女在美去世 享年103岁 家人举办追思会

2025年5月,孙中山的长孙女孙穗瑛在美国加州举行了追思会。孙穗瑛于2025年3月24日在美国去世,享年103岁。家人为她举办了追思会,回顾了她的一生。孙穗瑛1922年1月16日出生于中国广州,父亲是孙中山长子、时任广州市长的孙科,母亲为陈淑英。她的两位兄长孙治平、孙治强及妹妹…

Blender的一些设置

1. 将Blender长度单位改为毫米(mm), 并设置guides Grid的缩放系数,避免网格不见了。 2. 布尔操作的(Apply)应用按钮在哪里?好吧,在这里: 可以按下 CTRL A 快捷键。 3. 移动、旋转、缩放快捷键: G,R,S

34岁姑娘去世捐出能用的器官 爱与希望的传递

“别人救过我的命,我也想帮帮别人!”这是34岁的武汉姑娘宁馨生前最常说的一句话。10个月前,一场肾移植手术将她从尿毒症的死亡边缘拉回。5月30日凌晨4时50分,她的生命定格,却以另一种方式重启——捐出肝脏、心脏和一对角膜,至少让3人重获新生。手术开始前,全体医务人员面…

强降雨致云南一道路中断 有游客被困 丙中洛成“孤岛”

端午节期间,云南怒江到丙察察G219路段因持续降雨引发多处塌方。5月31日,多名网友在社交平台发帖称,219国道云南贡山县段因泥石流导致前往丙中洛镇的交通中断。一位丙中洛镇民宿老板表示,他在5月30日从自家民宿到贡山县办事,由于大雨导致道路中断,无法返回。他提到,丙中洛…

中国队亚锦赛女子接力摘金 收官日四金闪耀

第26届亚洲田径锦标赛在韩国龟尾市落幕,中国队在最后一个比赛日共收获4枚金牌。5月31日,中国队选手朱俊颖、陈妤颉、梁小静、李玉婷在女子4x100米接力决赛中以43秒28的成绩夺得冠军。同日,陈妤颉在女子200米决赛中以22秒97的成绩夺冠,并创造个人最佳成绩,李玉婷获得季军。…

尊界S800上市1小时大定过1000辆 超预期热销

华为与江淮联合推出的鸿蒙智行系列最贵车型尊界S800正式上市。这款瞄准迈巴赫和劳斯莱斯的超豪华车型,起售价定为70.8万元,最高版本为101.8万元,相比预售时喊出的100至150万元的价格范围更为务实。华为与江淮希望尊界S800能在竞争激烈的新车市场中抢占奔驰S、宝马7系和奥迪A…

比亚迪韩冰谈智驾系统与安全 构建全方位安全保障

在有条件自动驾驶模式下,安全是产业技术的核心和瓶颈。当智能驾驶系统开始接管方向盘时,必须确保安全无虞。为此,需要采用冗余设计、交互安全策略和协同安全设计等方法来构建一个全面的安全体系。6月1日,在未来汽车先行者大会智能网联汽车商业化论坛上,比亚迪新技术研究院…

歼-10挂弹起飞 俯冲投弹一击命中 精准制敌训练现场

歼-10战机进行对地攻击训练的过程引人关注。对于三代机飞行员新生来说,这是他们第一次进行大吨位实弹性质的对地攻击训练。这次训练不仅考验飞行员的目标搜索识别和战术应用能力,还检验了歼-10战机火控雷达系统的性能。在山脚下设置了两处靶标,歼-10战机的实弹对地攻击训练即…

@极光爱好者:2日左右,我国北部有机会出现极光

据中国气象局国家空间天气监测预警中心信息发布平台消息,北京时间5月31日07时45分左右,太阳活动区14100开始爆发耀斑,软X射线流量快速上升,并在08时05分达到峰值M8.1级中等耀斑强度。中国气象局国家空间天气监测预警中心预判,未来三天,可能发生地磁暴。2日左右,我国北部…

【论文笔记】Transcoders Find Interpretable LLM Feature Circuits

Abstract 机制可解释性(mechanistic interpretability)的核心目标是路径分析(circuit analysis):在模型中找出与特定行为或能力对应的稀疏子图。 然而,MLP 子层使得在基于 Transformer 的语言模型中进行细粒度的路径分析变得困难。具体而言,…

2024 CKA模拟系统制作 | Step-By-Step | 20、题目搭建-节点维护

一、题目 您必须在以下Cluster/Node上完成此考题: Cluster Master node Worker node zk8s master …

印度军方首次直接承认:战机被击落

印度军方首次直接承认:印战机在印巴冲突中被击落。当地时间5月31日,印度国防参谋长阿尼尔乔汉在接受媒体采访时表示,印度在此前的印巴冲突中损失了飞机,但未透露具体数字。这是印度方面自5月7日印巴冲突爆发以来,首次直接承认印度战机被击落。乔汉在接受彭博电视台采访时说…

“藏海师父”梁超:肖战非常严谨,堪称完美

从小教藏海纵横之术,并一直陪伴在他身边,助力藏海复仇,帮他搜集信息情报,兼任保镖,并进行心理疏导,照顾藏海生活起居——师父高明堪称十项全能。用高明的扮演者梁超的话说,高明与藏海的关系,“亦师亦友、亦父亦子。”由郑晓龙、曹译文执导,赵柳逸编剧,肖战、张婧仪、…

应用于分子生成的免训练引导多模态流模型 - TFG-Flow 评测

TFG-Flow (Training-Free Guidance Flow Model)是一个新颖的无训练引导的多模态生成流模型,它基于给定的无条件生成模型和目标性质预测器,旨在在没有额外训练的情况下生成具有期望目标性质的分子样本。这是一种高效的技术&#xf…

Linux RAID之RAID1介绍

RAID1磁盘阵列 RAID1使用数据镜像的概念,数据被镜像或克隆到一组相同的磁盘,如果其中一个磁盘发生故障,可以使用另一个磁盘。它还提高了读取性能,因为可以同时从所有磁盘访问不同的数据块。这可以在下图中解释。多线程进程可以同…