安庆大理运城常德铜陵江西
投稿投诉
江西南阳
嘉兴昆明
铜陵滨州
广东西昌
常德梅州
兰州阳江
运城金华
广西萍乡
大理重庆
诸暨泉州
安庆南充
武汉辽宁

一文详解缓存策略

7月7日 不回头投稿
  缓存是应对高并发场景下的一大神器,而如何设计好缓存模块并非直观想象的那么简单。本文聊一聊缓存模块设计过程中的那些事儿。涉及到的讨论有:
  缓存与数据库操作的非原子性引发的一致性问题并发引发的一致性问题写链路中是选择更新缓存还是删除缓存主从延迟和延迟双删问题一、引入缓存小试牛刀
  随着业务的发展,QPS有了一定的升高,对数据库造成的压力越来越大。这一阶段主要是希望通过加一层缓存,分担数据库的读请求压力1。1方案一:全量缓存定时更新
  方案示意图如下:
  写请求直接打到DB,不对缓存做更新读请求打到redis,缓存不设置过期时间,因此无需回源额外起定时任务将全量库存数据同步到redis中A方式:查询mysql数据更新到redisB方式:监听binlog,启动时全量插入一遍缓存,而后根据binlog增量更新(中间件Canal)1。2方案二:缓存设置过期时间
  方案示意图如下:
  写请求和方案一一样,直接打到DB,不对缓存做更新读请求先打到缓存命中缓存,则获取值而后返回未命中缓存,回源DB,查询到数据构建缓存,而后返回1。3方案分析
  方案
  优点
  缺点
  方案一
  实现简单
  1。缓存利用率低,冷数据会长期占用redis空间
  2。同步数据:采用A方式需要全量扫库,高频更新
  会导致数据库压力大,且任务执行时间会决定DB
  和redis中数据不一致的时间;采用B方式更合理些,
  但也引入了canal外部依赖
  方案二
  缓存利用率高,通过设置过期时间
  冷数据自动过期清除
  1。过期时间不好设置,一般依靠经验值会设计为秒级
  2。DB和redis之间的缓存不一致情况由过期时间决定,
  因此一般会存在秒级的不一致方案二中的一些坑
  简单来说需要考虑以下几个问题大量请求获取一个本不在redis也不在DB中的值,导致缓存穿透热key失效引发缓存击穿缓存击穿到重新构建缓存期间,请求相同key的请求继续回源DB,导致DB瞬时压力激增
  详细解决方案可以参考后端研发避坑指南1。1缓存设计如何选择
  方案之间的对比需要放在场景中分析,没有绝对的好与坏,只有适不适合方案一适合需要缓存的数据量不大、读远多于写的场景方案二适合数据量大,数据之间有冷热的区分
  另外,由于二者在写时都不会去操作缓存,因此在缓存和数据库的实时一致性方面都是比较差的(理论上都是秒级以上),对实时一致性要求比较高的场景不适合用这两种方案二、追求实时一致性
  上面提到的2种方案,在写请求时都没有去操作缓存,如果在写DB的同时主动去操作缓存,是不是会在实时一致性方面表现更好呢。简单分析下:如果写DB时更新缓存,那二者的时间差几乎就是写缓存所耗费的时间,约等于10ms如果写DB时删除缓存,下次读请求就会回源,理论上似乎没有一致性问题
  这样看,在写请求时操作缓存确实可以使得实时一致性至少从秒级提高到毫秒级
  但事情似乎没有这么简单
  当写请求从只写DB到需要写DB写缓存时,我们需要考虑的点就变多了,总的来说需要考虑到:顺序问题:是先写DB还是先写缓存?并发问题:非原子性:第2步操作失败主从延迟问题:极端情况下主从延迟会达到秒级,这对方案设计和选择会有什么影响
  在回答是选删除还是选更新前,先按照以上三点分别剖析这两种方案,最后再来做比较(虽然缓存带过期时间是个比较好的实践,但下面讨论的方案中如没有特别说明都是没有过期时间的缓存)2。1删除缓存先删除缓存,后更新数据库
  读写并发线程A要更新X2(原值X1)线程A先删除缓存线程B读缓存,发现不存在,准备回源线程B回源读取到X1,构建缓存X1并返回结果线程A更新数据库X2
  最终导致DB中的值是新值,缓存中的值是旧值,发生不一致
  写并发
  写并发并不会对写操作有影响,因为实际上底层数据库的更新还是串行的。影响可能是在写多的场景下,会导致缓存频繁删除,进而读请求频繁回源,对DB产生压力
  第2步失败
  删除缓存成功,更新数据库失败,此时请求同步返回失败。对于发起写请求的用户,会感知到失败,而后可以进行重试对于发起读请求的用户,仍然是正常使用服务先更新数据库,后删除缓存
  读写并发缓存中X不存在(可能是被写请求删除,也可能是过期自动删除),数据库中X1线程B读取缓存,不存在,回源DB,获取到X1线程A更新数据库X2线程A删除缓存(此时缓存本来也不存在)线程B将旧值写入缓存X1
  最终同样会导致缓存和DB中的值不一致
  写并发
  同上
  第2步失败
  更新数据库成功,删除缓存失败,假设请求返回失败对于发起写请求的用户,会感知到失败,而后可以进行重试对于发起读请求的用户,在写请求重试成功之前,会读取到旧值
  假设请求返回成功对于发起写请求的用户,认为是请求成功了,不会发起重试对于发起读请求的用户,在新的写请求到来并且删除缓存成功或者缓存自动过期之前,会读取到旧值分析
  从并发的角度
  不管顺序如何都有导致缓存和数据库不一致的可能,那到底该如何选呢?需要定性分析下这两种情况的可能性到底谁大谁小
  对于前者,写请求线程A的操作是25,两步写操作,读请求34两步读操作。通常写数据库时底层数据库会加锁,而读数据库不会加锁,因此理论上25的时间会大于34的时间;
  对于后者,读请求的操作是25,写请求是34,根据上面所说的2步写请求的时间一般会大于2步读请求的时间,从这点来看,后者发生的可能性是要小于前者的。
  除此之后,后者还需要叠加另外两个条件线程B读取缓存时,缓存刚好失效读请求和写请求并发
  所以总体上,先更新数据库后删除缓存的方案出现缓存和数据库不一致的可能性更小
  从第2步失败的角度
  看起来是先删除缓存再更新数据库更胜一筹
  在实际生产环境中,更倾向于选择先更新数据库,再删除缓存的方案。对于该方案在第2步失败方面的短板,一般解决方案是:失败后多次重试(比如引导用户多次重试,或者配置失败自动重试请求)消息队列,异步重试。代码中在更新数据库成功之后向MQ生产一条消息,消费者消费时保证一定成功。订阅数据库Binlog日志:相较于消息队列的方式,与业务代码解耦,且避免了写消息队列失败的情况。大概原理就是伪装成数据库的slave获取到Binlog日志完美了吗?
  还有一种近乎无解的情况:主从延迟
  不管是用哪种方式,如果回源DB时,由于主从延迟导致查询到值本身就是旧值,那写入缓存的也必定是旧值了。这里是有解决方案的,就是缓存回源的时候强制读主库。但是一般都不会使用这种方案,原因是这会使得回源的读请求直接打到主库,风险非常大,另外本身用于承担查询请求的从库也就没有了其存在的意义
  还有一种解决方案:延迟双删。所谓的双删是:写请求中更新数据库删除缓存后,再通过一条延时消息随后触发再次删除缓存。这样的目的是为了把读请求中在从库读出的数据清掉。但这个方案有个很大的问题,延迟时间如何设置?只能按照经验去设置
  所以,缓存和数据库之间的一致性是很难做到强一致的,只能是尽可能减小产生不一致的可能性和不一致状态的时间2。2更新缓存
  同样采取刚刚的分析框架先更新缓存,后更新数据库
  读写并发线程A更新X2(旧值X1),先更新缓存,成功线程B读取缓存X2线程A更新数据库X2
  这么一看,好像没啥问题,此时仅仅只有读写并发确实没有问题,等会结合第2步失败一起看
  写并发线程A更新X2(旧值X1),先更新缓存,此时缓存X2线程B更新X3,更新缓存成功,此时缓存X3线程B更新数据库,此时数据库X3线程A更新数据库,此时数据库X2
  最终导致缓存中的值是3,数据库中的值是2
  第2步失败
  更新缓存成功,更新数据库失败,此时请求同步返回失败。对于发起写请求的用户,会感知到失败,而后可以进行重试对于发起读请求的用户,读取到的数据是数据库中并不存在的数据,一旦缓存失效,读取到的仍然是旧值,对业务有影响先更新数据库,后更新缓存
  读写并发线程A更新X2(旧值X1),先更新数据库,此时数据库X2线程B读取缓存X1线程A更新缓存X2
  最终的值是一致的,但是步骤2中读到的值与当时数据库中的值不一致
  写并发线程A更新X2(旧值X1),先更新数据库,此时数据库X2线程B更新X3,更新数据库,此时数据库X3线程B更新缓存,此时缓存X3线程A更新缓存,此时缓存X2
  导致了不一致
  第2步失败
  更新数据库成功,更新缓存失败,假设请求返回失败对于发起写请求的用户,会感知到失败,而后可以进行重试对于发起读请求的用户,在写请求重试成功之前,会读取到旧值
  假设请求返回成功对于发起写请求的用户,认为是请求成功了,不会发起重试对于发起读请求的用户,在新的写请求到来并且更新缓存成功或者缓存自动过期之前,会读取到旧值分析
  从并发的角度:两种顺序都会导致不一致,且可能性是类似的(因为都是两步写操作),不一致的时间取决于缓存的过期时间
  从第2步失败的角度,相对于读到旧值,读到不存在的值更不可接受,因此从这点来看先更新数据库,后更新缓存的方案更好一些2。3到底是删除还是更新?
  从尽可能保证缓存和数据库一致性的角度,选删除好一些。这也是业界比较推荐的一种方式,被称为CacheAside(旁路缓存)。流程如下:
  除此之外还需要考虑的点是:当缓存的值需要经过一系列的计算得到时,删除也比更新合适。删除使得缓存类似于一种懒加载的模式,有请求才会去构建缓存,可以节省计算资源
  但是笔者有了解到,某些大型互联网电商也有采用写请求时更新缓存的方式。其给出的理由是:写时删除缓存,会导致C端读请求的集中回源(比如秒杀场景)会对DB造成很大的压力。值得一提的是,它们的方案中写时更新缓存是异步的,并且通过一些防抖设计减少了更新次数以降低缓存侧的写压力
  其实这也道出了删除缓存和更新缓存一个很大的区别:更新缓存可以最大程度的保证读请求能Hitcache,提高缓存命中率;而删除缓存实际上是依靠回源DB来保持数据的新鲜程度的。因此在一些特定场景下,如果回源DB的请求都足以打垮数据库时,是可以考虑使用更新缓存的方式的
  另一方面,删除缓存的方案在回源DB的场景下是可以做一些优化,以降低数据库的压力。比如golang中有Singleflight,可以在单机层面减少回源的请求(比如原本有100个请求同一行数据的请求,Singleflight会拦截后99个)三、缓存的各种读、写模式
  接下来会介绍四种缓存的读、写模式,分别对应读、写请求的策略。按读、写区分,理论上是可以两两组合3。1ReadThrough
  意为读穿透模式,它的流程和CacheAside中的读流程类似,不同点在于Readthrough多了一个访问控制层,如下图
  优点是:上游只和访问控制层交互,并不关心下游是否有缓存以及是什么缓存策略,上游的业务层会更加简洁;同时对缓存层和持久化层交互的封装程度更高,更便于移植
  该模式适合的场景是:readheavy
  当然这种方式会存在不一致的问题,在下面写模式中会有相应的策略3。2WriteThrough
  意为直写模式,如图:
  注意这里与CacheAside模式不同的是:是更新缓存而非删除缓存更新缓存在先,更新DB在后
  这种方式的优缺点在上面已经分析过了。该模式适合的场景是:写操作较多且对一致性要求比较高的场景。理论上ReadThrough和WriteThough组合可以获得不错的缓存利用率和实时一致性,据说亚马逊的DynamoDBAccelerator就是采用了这两种模式3。3WriteAround
  如果对一致性的要求较弱,可以选择在CacheAside读链路中增加缓存的过期时间,在写链路中仅仅更新数据库,不做任何的删除或更新缓存的操作。这其实就是第一部分中的方案二。这种方案实现简单,但缓存中的数据和数据库数据一致性较差3。4WriteBehindWriteBack
  意为异步回写模式,它具有类似WriteThrough的访问控制层,不同的是,该模式下的写链路,只更新缓存而不更新数据库,对于数据库的更新,则是通过批量异步更新的方式进行的,并且可以通过上面提到的防抖设计聚合更新请求,以减少对DB的实际写访问
  该模式下,写请求延迟较低,具有较好的系统吞吐。但缺点也很明显:缓存和数据库的一致性弱,数据库是落后于缓存的缓存负载大,若缓存宕机会造成数据丢失,因此需要重点考虑缓存的高可用部署
  因此该模式比较适合瞬时写操作的场景,比如电商领域的秒杀场景四、小结第一部分主要介绍了简单的应用缓存扛住读流量的方案,其主要的缺点是缓存与数据库的一致性较差第二部分的方案主要是为了追求实时一致性,因此在写链路上需要操作缓存,分析了操作应该选是删除还是更新。业界一般采用删除缓存的方式,同时使用相关组件(如Singleflight)解决重复的回源DB的读请求。但更新缓存也有具体的实践,二者需要根据具体业务场景、资源(数据库、缓存)等情况来选择先更新数据库后删除缓存优于先删除缓存后更新数据库,原因是后者在并发场景下缓存不一致发生的可能性更低,触发的条件更苛刻第2步失败场景下,一般需要失败重试,好的解决方式是订阅Binlog,消费者重试保证最终成功第三部分按读、写总结了缓存策略中常用的四种模式,以及其适合的场景,总结来说读多写少场景下:CacheAside消费binlog异步重试比较适合,进一步其中讲述的ReadThrough可以与CacheAside模式中的读链路做替换写多场景下,可以选择WriteThrough,但WriteThrough在并发场景下缓存和数据库不一致的可能性会由于多个线程并发写而提高,因此使用该方案时需要对此有预期写多的极端场景,可以选择WriteBehind方案
  在笔者的工作中,一开始采用的方案是第一部分的方案二,即设置缓存时间,后续采用的是CacheAside的方案,并对回源请求引入了SingleFlight以保护DB参考文档
  developer。baidu。comarticledet
  codeahoy。com20170811最后如果觉得有收获,三连支持下;文章若有错误,欢迎评论留言指出,也欢迎转载,转载请注明出处;个人vx:EchoAtaraxia,交流技术、面试、学习资料、帮助一线互联网大厂内推等个人博客建设中:https:blog。echoataraxia。icu复制代码
投诉 评论 转载

自驾豳州礼佛清晨,沿清凉山开凿的大佛寺已从夜的帷幕中走出,沐着朝晖,静静地注视着北方蜿蜒东流的泾河。大佛寺,始建于唐贞观二年(公元628年),距今已有一千四百多年的历史,原名应福寺,……养乐多中国涨价!一款饮料卖59年光环不再,乳酸菌大王着急了?图片来源:养乐多(中国)投资有限公司官网养乐多又涨价了,涨幅高达14。3?1月17日,北京商报记者了解到,市场上养乐多产品售价进行了上调。在业内看来,此次价格调整与……一文详解缓存策略缓存是应对高并发场景下的一大神器,而如何设计好缓存模块并非直观想象的那么简单。本文聊一聊缓存模块设计过程中的那些事儿。涉及到的讨论有:缓存与数据库操作的非原子性引发的一致……一场能源危机,激活全球万亿核电市场导语:俄乌冲突带来的不仅仅是欧洲能源危机,还有核能的回归。数据显示,2021年,全球核电装机新增883。60万KW,同比增长了97。54。核电作为优质基荷能源,已经出现回……艺术与科技的再次完美结合,华为发布HUAWEIWATCHGT在12月9日,华为召开了华为冬季全场景新品发布会,会上华为发布了多款新品,这其中就有智能手表产品的又一款旗舰力作HUAWEIWATCHGT3Pro典藏版,这款华为GT系列的旗舰……42岁熊黛林跟老公逛商场,全程挽手太恩爱,个子高挑身材火辣近日,有网友在社交平台上晒出偶遇熊黛林夫妇的画面,当天两人现身某商场逛街,很是恩爱,而两人罕见合体的照片也是引来无数网友的围观和热议。在网友晒出的视频中能看出当天夫妻二人……福启新岁,万事顺遂若干年后,回首2022,注定是刻在人们记忆里难以抹去的岁月。在严防死守的第三个年头,仍然看不到疫情清零的曙光。看到的是疫情影响下众生的艰难,当然,也更多的是看到国家为保护老百姓……天才中锋彻底水掉,曾凡博又被弃用,解立彬险翻船,方硕单骑救主不知不觉间,联赛第二十三轮比赛已经拉开帷幕。在此前结束的首钢对阵四川的比赛中,经历四节鏖战,首钢91:77击败四川。这场比赛,范子铭轮休,李慕豪首发出战13分钟,0分1篮……我国出境游逐步恢复,预计今年五一或暑期迎高峰1月8日起,我国取消入境后全员核酸检测和集中隔离,另外,本着试点先行原则,有序恢复中国公民出境旅游。目前,我国出境旅游正在逐步恢复,但游客预定量仍处于低位,预计将在今年五一或暑……北京取消新十条外核酸查验要求北京市政府新闻发言人徐和建今日介绍,取消新十条要求外的复工复产场所核酸阴性证明查验。取消进口非冷链货品、散装散建、房屋中介、住宿服务、图书馆、文化馆、美术馆等等级旅游景区、生产……股票市场是零和博弈,当所有股票都下跌的时候大家亏的钱都去哪了股票市场是零和博弈,那么,当所有股票都下跌的时候大家亏的钱都去哪了?所谓股市里的钱蒸发了,是指股票贬值等同于多少钱损失了,并不是真有一笔等价的钱蒸发了,只是股票市值降低了……在世界杯上踢进一颗球,能值多少钱呢?2022世界杯举世瞩目的卡塔尔世界杯已经进入到紧张激烈的淘汰赛阶段。尽管在12月6日凌晨的16强淘汰赛上,日韩两支球队纷纷折戟,但是亚洲球队的总体崛起,仍旧令球迷们……
中国三大领域实现突破,国产芯开始爆发了国庆旅游攻略,川西稻城亚丁人少还免费抖音6月1日起将对本地生活商家收取服务费,最高费率8,新商家战神5赫尔海姆主要物品如何收集海南人嗨游海南五条嗨游路线全部圆满收官秋季孩子上吐下泻家长应该这样做原地结婚!王诗玥柳鑫宇520拍时尚照,大柳偷偷玩玥玥头发疯狂快看!上海青浦藏着一条道路,沿途处处是美景丁俊晖总奖金突破400万英镑屡战屡败吃老本再不雄起真成廉价帐小额存款还会越存越少?六大国有行客服回应小米发布带加湿仿真火焰的取暖器彻底凉凉!这三款新能源SUV销量加一起还不到10辆厨师比刀法女性强迫症影响生活吗怀孕反应大怎么办黄梅天气是什么?黄梅天气少吃什么瓜果?微信语音转发不了给你的朋友试一试这种方冬至要来了我爱北京天安门观后感方苞四君子传王源原文及翻译论及造句用论及造句大全男人如何做个潮人女生短发烫什么卷好看小波浪卷灵动俏皮又减龄女性孕前不得不的事情和注意事项

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找七猫云易事利