若是第二次看到个人文章 ,欢迎点击文末连接扫码订阅我我的的公众号(跨界架构师)哟~每周五11:45 按时送达。固然了,也会时不时加个餐~
程序员本文长度为4209字,建议阅读12分钟。redis
坚持原创,每一篇都是用心之做~数据库
在前一篇《360°全方位解读「缓存」》中,咱们聊了运用缓存的三种思路,以及在一个完整的系统中能够设立缓存的几个位置,而且分享了关于浏览器缓存、CDN缓存、网关(代理)缓存的一些使用经验。浏览器
此次Z哥将深刻到实际场景中,来看一下「进程内缓存」、「进程外缓存」运用时的一些最佳实践。因为篇幅缘由,此次先聊三个问题。缓存
首当其冲的就是“先写DB仍是缓存?”。我想,只要你开始运用缓存,这会是你第一个要好好思考的问题,不然在前方等待你的就是灾难。。。bash
一个程序能够没有缓存,可是必定要有数据库。这是你们的广泛观点,因此数据库的重要性在你的潜意识里老是被放在了第一位。微信
若是不细想的话你可能会以为,数据库操做失败了,天然缓存也不用操做了;数据库操做成功了,再操做缓存,没毛病。网络
可是数据库操做成功,缓存操做的失败的状况该怎么解?(主要在用到redis,memcached这种进程外缓存的时候,因为网络因素,失败的可能性大增)多线程
办法也是有的,在操做数据库的时候带一个事务,若是缓存操做失败则事务回滚。大体的代码意思以下:架构
begin trans
var isDbSuccess = write db;
if(isDbSuccess){
var isCacheSuccess = write cache;
if(isCacheSuccess){
return success;
}
else{
rollback db;
return fail;
}
}
else{
return fail;
}
catch(Exception ex){
rollback db;
}
end trans复制代码
如此一来就万无一失了吗?并非。除了因为事务的引入,增长了数据库的压力以外,在极端场景下可能会出现rollback db失败的状况。是否是很头疼?
解决这个问题的方式就是write cache的时候作delete操做,而不是set操做。如此一来,用多一次cache miss的代价来换rollback db失败的问题。
就像图上所示,哪怕rollback失败了,经过一次cache miss从新从db中载入旧值。
题外话:其实这种作法有一种专业的叫法——Cache Aside Pattern。为了便于记忆,你能够和分布式系统的CAP定理同时记忆,叫「缓存的CAP模式」。
是否是看上去妥了?能够开始潇洒了?
若是你的数据库没有作高可用的话,的确能够妥了。可是若是数据库作了高可用,就会涉及到主从数据库的数据同步,这就有新问题了。
题外话:因此你们不要过分追求技术的酷炫,可能会得不偿失,自找麻烦。
什么问题呢?就是若是在数据还未同步到「从库」的时候,因为cache miss去「从库」取到了未同步前的旧值。
解决它的第一个方式很简单,也很粗暴。就是定时去「从库」读数据,发现数据和缓存不同了就set到缓存里去。
可是这个方式有点“治标不治本”。不断的从数据库定时读取,对资源的消耗大不说,这个间隔频率也很差定义一个比较合适的统一标准,过短吧,会致使重复读取的次数加大,太长吧,又会致使缓存和数据库不一致的时间变长。
因此这个方案仅适用于项目中只有二、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失败的状况,问题就来了。
在这个时候最新的数据只有缓存里有,怎么办?单独起个线程不断的重试往数据库写?这个方案在必定程度上可行,但不适合用于对数据准确性有高要求的场景,由于缓存一旦挂了,数据就丢了!
题外话:哪怕选择了这个方案,重试线程应确保只有1个,不然会存在“ABBA”的「并发写」问题。
可能你会说用delete cache不就没问题了?
能够是能够,可是要有个前提条件,访问缓存的程序不会产生并发。由于只要你的程序是多线程运行的,一旦出现并发就有可能出现「读」的线程因为cache miss从数据库取的时候,「写」的线程还没将数据写到数据库的状况。
因此,哪怕用delete cache的方式,要么带lock(多客户端状况下还得上分布式锁),要么必然出现数据不一致。
值得注意的是,若是数据库一样作了高可用,哪怕带了lock,也还须要考虑和上面提到的「先DB再缓存」中同样的因为主从同步的时间差可能会产生的问题。
固然了,「先缓存再DB」也不是一文不值。当对写入速度有极致要求,而对数据准确性没那么高要求的场景下就很是好使,其实就是前一篇(《360°全方位解读「缓存」》)提到的「延迟写」机制。
小结一下,相比缓存来讲,数据库的「高可用」通常会在系统发展的后期才会引入,因此在没有引入数据库「高可用」的状况下,Z哥建议你使用「先DB再缓存」的方式,而且缓存操做用delete而不是set,这样基本就能够高枕无忧了。
可是若是数据库作了「高可用」,那么团队必然也造成必定规模了,这个时候就老老实实的作数据库变动记录(binlog)的订阅吧。
到这里可能有的小伙伴要问了,“若是上了分布式缓存,还须要本地缓存吗?”。
在解答这个问题以前咱们先来思考一个问题,一个分布式系统最重要的价值是什么?
是「无限扩展」,只要堆硬件就能应对业务增加。要达到这点的背后须要知足一个特性,就是程序要「无状态」。那么既想引入缓存来加速,又要达到「无状态」,靠的就是分布式缓存。
因此,能用分布式缓存解决的问题就尽可能不要引入本地缓存。不然引入分布式缓存的做用就小了不少。
可是在少数场景下,本地缓存仍是能够发挥其价值的,可是咱们须要仔细识别出来。主要是三个场景:
不常常变动的数据。(好比一天甚至好几天更新一次的那种)
须要支撑很是高的并发。(好比秒杀)
对数据准确性能容忍的场景。(好比浏览量,评论数等)
不过,我仍是建议你,除了第二种场景,不然仍是尽可能不要引入本地缓存。缘由咱们下面来讲说。
其实这个缘由的根本问题就是在引入了本地缓存后,本地缓存(进程内缓存)、分布式缓存(进程外缓存)、数据库这三者之间的数据一致性该怎么进行呢?
若是是个单点应用程序的话,很简单,将本地缓存的操做放在最后就行了。
可能你会说本地缓存修改失败怎么办?好比重复key啊什么的异常。那你能够反思一下为这种数据为何能够成功的写进数据库。。。
可是,本地缓存带来的一个巨大问题就是:虽然一个节点没问题,可是多个本地缓存节点之间的数据如何同步?
解决这个问题的方式中有两种和以前咱们聊过的Session问题(《作了「负载均衡」就能够随便加机器了吗?》)是相似的。要么是由接收修改的节点通知其它节点变动(经过rpc或者mq皆可),要么借助一致性hash让同一个来源的请求固定落到一个节点上。后者可让不一样节点上的本地缓存数据都不重复,从源头上避免了这个问题。
可是这两个方案走的都是极端,前者变动成本过高,好比须要通知上千个节点的话,这个成本难以接受。然后者的话对资源的消耗过高,并且还容易出现压力分摊不均匀的问题。因此,通常系统规模小的时候能够考虑前者,而规模越大越会选择后者。
还有一种相对中庸一些的,以下降数据的准确性来换成本的方案。就是设置缓存定时过时或者定时往下游的分布式缓存拉取最新数据。这和前面「先DB再缓存」中提到的定时机制是同样的逻辑,胜在简单,缺点就是会存在更长时间的数据不一致。
小结一下,本地缓存的数据一致性解决方案,能完全解决的是借助一致性hash的方案,可是成本比较高。因此,如非必要仍是慎重决定要不要作本地缓存。
好了,咱们一块儿总结一下。
此次呢,Z哥先花了大量的篇幅和你讨论「先写DB仍是缓存」的问题,而且带你层层深刻,经过一点一点的演进来阐述不一样的解决方案。
而后与你讨论了「本地缓存」的意义以及如何在「分布式缓存」和「数据库」的基础上作好数据一致性,这其中主要是多个本地缓存节点之间的数据同步问题。
但愿对你有所启发。
此次的缓存实践是一个很是好的例子,从中咱们能够看到一件事情的精细化所带来的复杂度须要更加的精细化去解决,可是又会带来新的复杂度。因此做为技术人的你,须要无时无刻考虑该怎么权衡,而不是人云亦云。
相关文章:
若是你喜欢这篇文章,能够点一下左侧的「大拇指」哦~。
这样能够给我一点反馈。: )
谢谢你的举手之劳。
▶关于做者:张帆(Zachary,我的微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。本文首发于公众号:「跨界架构师」(ID:Zachary_ZF)。<-- 点击后阅读热门文章
按期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
若是你是初级程序员,想提高但不知道如何下手。又或者作程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注个人公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思惟导图。
若是你是运营,面对不断变化的市场一筹莫展。又或者想了解主流的运营策略,以丰富本身的“仓库”。欢迎关注个人公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思惟导图。