如何在分布式系统中正确的使用缓存?别给你的项目引入定时炸弹!

如果不细想的话你可能会觉得,数据库操作失败了,自然缓存也不用操作了;数据库操作成功了,再操作缓存,没毛病。

但是数据库操作成功,缓存操作的失败的情况该怎么解?

这种情况主要在用到 redis,memcached 这种进程外缓存的时候,由于网络因素,失败的可能性大增。

办法也是有的,在操作数据库的时候带一个事务,如果缓存操作失败则事务回滚。大致的代码意思如下:

在这里插入图片描述

如此一来就万无一失了吗?并不是。除了由于事务的引入,增加了数据库的压力之外,在极端场景下可能会出现rollback db失败的情况。是不是很头疼?

解决这个问题的方式就是write cache的时候做delete操作,而不是set操作。如此一来,用多一次cache miss的代价来换rollback db失败的问题。

如下图所示:

在这里插入图片描述

就像图上所示,哪怕rollback失败了,通过一次cache miss重新从db中载入旧值。

题外话:其实这种做法有一种专业的叫法——Cache Aside Pattern。

为了便于记忆,你可以和分布式系统的CAP定理同时记忆,叫「缓存的CAP模式」。

是不是看上去妥了?可以开始潇洒了?

在这里插入图片描述

如果你的数据库没有做高可用的话,的确可以妥了。但是如果数据库做了高可用,就会涉及到主从数据库的数据同步,这就有新问题了。

题外话:所以大家不要过度追求技术的酷炫,可能会得不偿失,自找麻烦。

什么问题呢?就是如果在数据还未同步到「从库」的时候,由于cache miss去「从库」取到了未同步前的旧值。

在这里插入图片描述

解决它的第一个方式很简单,也很粗暴。就是定时去「从库」读数据,发现数据和缓存不一样了就set到缓存里去。

在这里插入图片描述

但是这个方式有点“治标不治本”。不断的从数据库定时读取,对资源的消耗大不说,这个间隔频率也不好定义一个比较合适的统一标准,太短吧,会导致重复读取的次数加大,太长吧,又会导致缓存和数据库不一致的时间变长。

所以这个方案仅适用于项目中只有2、3处需要做这种处理的场景,并且还不能是数据会频繁修改的情况。因为在数据修改频次较高的场景,甚至可能还会出现这个定时机制所消耗的资源反而大于主程序的情况。

一般情况下,另一种更普适性的方案是采用接下去聊的这种更底层的方式进行,就是“哪里有问题处理哪里”,当「从库」完成同步的时候再额外做一次delete cache或者set cache的操作。

在这里插入图片描述

如此,虽说也没有100%解决短暂的数据不一致问题,但是已经将脏数据所存在的时长降到了最低(最终由主从同步的耗时决定),并且大大减少了无谓的资源消耗。

可能你会说,“不行,这么一点时间也不能忍”怎么办?办法是有,但是会增加「主库」的压力。就是在产生数据库写入动作后的一小段时间内强制读「主库」来加载缓存。

怎么实现呢?先得依赖一个共享存储,可以借助数据库或者也可以是我们现在正在聊的分布式缓存。

然后,你在事务提交之后往共享存储中临时存一个

{ key = dbname + tablename + id,value = null,expire = 3s }

这样的数据,并且再做一次delete cache的操作。

begin trans

var isDbSuccess = write db;

if(isDbSuccess){

var isCacheSuccess = delete cache;

if(isCacheSuccess){

return success;

}

else{

rollback db;

return fail;

}

}

else{

return fail;

}

catch(Exception ex){

rollback db;

}

end trans

//在这里做这个临时存储,{key,value,expire}。

delete cache;

如此一来,当「读数据」的时候发生cache miss,先判断是否存在这个临时数据,只要在3秒内就会强制走「主库」取数据。

可以看到,不同的方案各有利弊,需要根据具体的场景仔细权衡。

先缓存再DB


你工作中的大部分场景对数据准确性肯定是低容忍的,所以一般不建议选择「先缓存再DB」的方案,因为内存是易失性的。一旦遇到操作缓存成功,操作DB失败的情况,问题就来了

在这里插入图片描述

在这个时候最新的数据只有缓存里有,怎么办?单独起个线程不断的重试往数据库写?

这个方案在一定程度上可行,但不适合用于对数据准确性有高要求的场景,因为缓存一旦挂了,数据就丢了!

题外话:哪怕选择了这个方案,重试线程应确保只有1个,否则会存在“ABBA”的「并发写」问题。

可能你会说用delete cache不就没问题了?

可以是可以,但是要有个前提条件,访问缓存的程序不会产生并发。

因为只要你的程序是多线程运行的,一旦出现并发就有可能出现「读」的线程由于cache miss从数据库取的时候,「写」的线程还没将数据写到数据库的情况。

如下图所示:

在这里插入图片描述

所以,哪怕用delete cache的方式,要么带lock(多客户端情况下还得上分布式锁),要么必然出现数据不一致。

值得注意的是,如果数据库同样做了高可用,哪怕带了lock,也还需要考虑和上面提到的「先DB再缓存」中一样的由于主从同步的时间差可能会产生的问题。

当然了,「先缓存再DB」也不是一文不值。对写入速度有极致要求,而对数据准确性没那么高要求的场景下就非常好使

小结一下,相比缓存来说,数据库的「高可用」一般会在系统发展的后期才会引入,所以在没有引入数据库「高可用」的情况下,我建议你使用「先DB再缓存」的方式,并且缓存操作用delete而不是set,这样基本就可以高枕无忧了。

但是如果数据库做了「高可用」,那么团队必然也形成一定规模了,这个时候就老老实实的做数据库变更记录(binlog)的订阅吧。

到这里可能有的小伙伴要问了,“如果上了分布式缓存,还需要本地缓存吗?”。那我们就来看看这个问题。

本地缓存还要不要?


在解答这个问题之前我们先来思考一个问题,一个分布式系统最重要的价值是什么?

是「无限扩展」,只要堆硬件就能应对业务增长。要达到这点的背后需要满足一个特性,就是程序要「无状态」。

那么既想引入缓存来加速,又要达到「无状态」,靠的就是分布式缓存。

所以,能用分布式缓存解决的问题就尽量不要引入本地缓存。否则引入分布式缓存的作用就小了很多。

但是在少数场景下,本地缓存还是可以发挥其价值的,但是我们需要仔细识别出来。主要是三个场景:

  1. 不经常变更的数据。(比如一天甚至好几天更新一次的那种)

  2. 需要支撑非常高的并发。(比如秒杀)

  3. 对数据准确性能容忍的场景。(比如浏览量,评论数等)

最后

由于篇幅限制,小编在此截出几张知识讲解的图解

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

P8级大佬整理在Github上45K+star手册,吃透消化,面试跳槽不心慌

最后

由于篇幅限制,小编在此截出几张知识讲解的图解

[外链图片转存中…(img-NGAyZ2jV-1721180412794)]

[外链图片转存中…(img-0tprPHSu-1721180412795)]

[外链图片转存中…(img-v7VLjZUi-1721180412795)]

[外链图片转存中…(img-JsBy3W9X-1721180412796)]

[外链图片转存中…(img-VfzJbEt0-1721180412796)]

  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值