面试被问到缓存和数据库双写一致性,我这个答案只能88分?

本文记录了一次面试经历,面试中主要讨论了高并发场景下缓存和数据库如何保证一致性。提到了先更新数据库再删除缓存、延时双删策略以及并发问题的解决方案,包括设置缓存过期时间、异步删除和数据库的主从同步。面试者还讨论了在读写分离架构下的数据一致性问题和重试机制来应对删除缓存失败的情况。
摘要由CSDN通过智能技术生成

缓存和数据库双写一致性

目前在职,不方便请假去面试,就和一个闪电快车的HR约了周六去面试,这天我准时的来到他们公司的楼下,地铁北苑路北站,地铁口3百米很近,然后给HR打电话,问是那栋楼,发现和BOSS上的地址不一样,问了之后才知道原来这个楼有2个名字,我就胆战心惊的上去了。
在这里插入图片描述

一面30分钟,一些八股文,巴拉巴拉的说完了,说让我等下,我窃喜:这就完事儿了?这也太简单了。过了5分钟,一个发际线很高的中年男人来了,穿着一双老年拖鞋,衣衫不整,感觉很邋遢的样子,让我做下自我介绍,然后告诉我他是二面的面试官,我瞬间被他的强大气场给压迫了。
面试官看了下我的简历,问:你用过缓存?缓存都用于啥场景能说下吗?
:巴拉巴拉,说我们系统中一个防止用户频繁请求获取验证码接口的case
1用户手机号放入缓存,过期时间一分钟,保证用户一分钟之内只能请求一次接口,毕竟系统发短信也是要钱的。
2.防止高并发场景下,用户的请求给DB搞挂,如下图,所以加入了缓存
在这里插入图片描述

面试官:高并发下缓存和数据库如何保证一致性,了解吗?
:这个问题很熟悉啊,好像在哪里看过,我努力的回想,我想到了,用延时双删的策略
1先删缓存
2更新数据库
3延迟一断时间再删缓存
伪代码如下:
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
面试官:如果你用了mysql的读写分离架构怎么办?
:在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值
上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。
面试官:采用这种同步淘汰策略,吞吐量降低怎么办?
:ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。

面试官:你为什么要用这种方案?先更新数据库,再更新缓存这种不行吗?
:卧槽,这家伙怎么这么奇葩,问的咋和别人不一样呢?别人都是问解决方案,你竟然文问我为什么不用别的方案?当然,这些我没说出来
我努力思考,哦哦,想到了:

第一点高并发场景下如果2个请求过来,顺序如下,会发生线程安全的问题
1请求更新了数据库
2请求更新了数据库
2请求更新了缓存
1请求更新了缓存
那么最后1请求更新了缓存,就会有脏数据,如果这个缓存没有过期时间的话,这个脏数据就会永远存在
第二点
(1)如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
(2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。

面试官:那先删除缓存,再更新数据库呢?
:同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:
(1)请求A进行写操作,删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A将新值写入数据库
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

面试官:那先更新数据库,再删除缓存呢?
:首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出

失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。
面试官:这种情况不存在并发问题么?
:不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效,过期时间到了
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
面试官:一定要解决怎么办?
我心中意淫:你工地出身的?这么能抬杠?
如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。
还有其他造成不一致的原因么?
有的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的一个问题,如果删缓存失败了怎么办,那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。这也是缓存更新策略(2)里留下的最后一个疑问。
如何解决?
提供一个保障的重试机制即可,这里给出两套方案。
方案一
在这里插入图片描述
流程如下所示
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
方案二
在这里插入图片描述
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。

上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。另外,重试机制是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可。

面试官:还有别的方案吗?
我心中意淫:你这个比真的烦,咋一直问呢?难道这就是传说的架构师级别?动不动就让一直说方案,选最优?
实在不想聊这个了,我:我知道的就这些,别的可能还有吧,等我回去再看看。
面试官:今天就到这吧,有消息我们会联系你,然后穿着他的老年拖鞋,悠哉的走了,嘴里还在念叨:什么公司这么忙?周六来面试?周一到周五不能来吗?
我望着他的背景:这个比不简单。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值