如何在分布式系统中正确的使用缓存?别给你的项目引入定时炸弹!
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)的订阅吧。
到这里可能有的小伙伴要问了,“如果上了分布式缓存,还需要本地缓存吗?”。那我们就来看看这个问题。
[](
)本地缓存还要不要?
在解答这个问题之前我们先来思考一个问题,一个分布式系统最重要的价值是什么?
是「无限扩展」,只要堆硬件就能应对业务增长。要达到这点的背后需要满足一个特性,就是程序要「无状态」。
那么既想引入缓存来加速,又要达到「无状态」,靠的就是分布式缓存。
所以,能用分布式缓存解决的问题就尽量不要引入本地缓存。否则引入分布式缓存的作用就小了很多。
但是在少数场景下,本地缓存还是可以发挥其价值的,但是我们需要仔细识别出来。主要是三个场景:
不经常变更的数据。(比如一天甚至好几天更新一次的那种)
需要支撑非常高的并发。(比如秒杀)
对数据准确性能容忍的场景。(比如浏览量,评论数等)
不过,我还是建议你,除了第二种场景,否则还是尽量不要引入本地缓存。原因我们下面来说说。
其实这个原因的根本问题就是在引入了本地缓存后,本地缓存(进程内缓存)、分布式缓存(进程外缓存)、数据库这三者之间的数据一致性该怎么进行呢?
[](
)本地缓存、分布式缓存、db 之间的数据一致性
如果是个单点应用程序的话,很简单,将本地缓存的操作放在最后就好了。
可能你会说本地缓存修改失败怎么办?比如重复 key 啊什么的异常。那你可以反思一下为这种数据为什么可以成功的写进数据库。。。
但是,本地缓存带来的一个巨大问题就是:虽然一个节点没问题,但是多个本地缓存节点之间的数据如何同步?
解决这个问题的方式中有两种:要么是由接收修改的节点通知其它节点变更(通过 rpc 或者 mq 皆可),要么借助一致性 hash 让同一个来源的请求固定落到一个节点上。
后者可以让不同节点上的本地缓存数据都不重复,从源头上避免了这个问题。
但是这两个方案走的都是极端,前者变更成本太高,比如需要通知上千个节点的话,这个成本难以接受。而后者的话对资源的消耗太高,而且还容易出现压力分摊不均匀的问题。
所以,一般系统规模小的时候可以考虑前者,而规模越大越会选择后者。
还有一种相对中庸一些的,以降低数据的准确性来换成本的方案。就是设置缓存定时过期或者定时往下游的分布式缓存拉取最新数据。
这和前面「先 DB 再缓存」中提到的定时机制是一样的逻辑,胜在简单,缺点就是会存在更长时间的数据不一致。
小结一下,本地缓存的数据一致性解决方案,能彻底解决的是借助一致性 hash 的方案,但是成本比较高。所以,如非必要还是慎重决定要不要做本地缓存。
[](
)总结
好了,我们一起总结一下。
这次呢,我先花了大量的篇幅和你讨论「先写 DB 还是缓存」的问题,并且带你层层深入,通过一点一点的演进来阐述不同的解决方案。
然后与你讨论了「本地缓存」的意义以及如何在「分布式缓存」和「数据库」的基础上做好数据一致性,这其中主要是多个本地缓存节点之间的数据同步问题。
评论