最后
毕竟工作也这么久了 ,除了途虎一轮,也七七八八面试了不少大厂,像阿里、饿了么、美团、滴滴这些面试过程就不一一写在这篇文章上了。我会整理一份详细的面试过程及大家想知道的一些问题细节
美团面试经验
字节面试经验
菜鸟面试经验
蚂蚁金服面试经验
唯品会面试经验
因篇幅有限,图文无法详细发出
如此一来就万无一失了吗?并不是。除了由于事务的引入,增加了数据库的压力之外,在极端场景下可能会出现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失败的情况,问题就来了。
在这个时候最新的数据只有缓存里有,怎么办?单独起个线程不断的重试往数据库写?
这个方案在一定程度上可行,但不适合用于对数据准确性有高要求的场景,因为缓存一旦挂了,数据就丢了!
题外话:哪怕选择了这个方案,重试线程应确保只有1个,否则会存在“ABBA”的「并发写」问题。
可能你会说用delete cache不就没问题了?
可以是可以,但是要有个前提条件,访问缓存的程序不会产生并发。
因为只要你的程序是多线程运行的,一旦出现并发就有可能出现「读」的线程由于cache miss从数据库取的时候,「写」的线程还没将数据写到数据库的情况。
如下图所示:
所以,哪怕用delete cache的方式,要么带lock(多客户端情况下还得上分布式锁),要么必然出现数据不一致。
值得注意的是,如果数据库同样做了高可用,哪怕带了lock,也还需要考虑和上面提到的「先DB再缓存」中一样的由于主从同步的时间差可能会产生的问题。
当然了,「先缓存再DB」也不是一文不值。对写入速度有极致要求,而对数据准确性没那么高要求的场景下就非常好使
小结一下,相比缓存来说,数据库的「高可用」一般会在系统发展的后期才会引入,所以在没有引入数据库「高可用」的情况下,我建议你使用「先DB再缓存」的方式,并且缓存操作用delete而不是set,这样基本就可以高枕无忧了。
但是如果数据库做了「高可用」,那么团队必然也形成一定规模了,这个时候就老老实实的做数据库变更记录(binlog)的订阅吧。
到这里可能有的小伙伴要问了,“如果上了分布式缓存,还需要本地缓存吗?”。那我们就来看看这个问题。
在解答这个问题之前我们先来思考一个问题,一个分布式系统最重要的价值是什么?
是「无限扩展」,只要堆硬件就能应对业务增长。要达到这点的背后需要满足一个特性,就是程序要「无状态」。
那么既想引入缓存来加速,又要达到「无状态」,靠的就是分布式缓存。
最后
这份文档从构建一个键值数据库的关键架构入手,不仅带你建立起全局观,还帮你迅速抓住核心主线。除此之外,还会具体讲解数据结构、线程模型、网络框架、持久化、主从同步和切片集群等,帮你搞懂底层原理。相信这对于所有层次的Redis使用者都是一份非常完美的教程了。
整理不易,觉得有帮助的朋友可以帮忙点赞分享支持一下小编~
你的支持,我的动力;祝各位前程似锦,offer不断!!!
你的支持,我的动力;祝各位前程似锦,offer不断!!!