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

缓存 专栏收录该内容
1 篇文章 0 订阅

缓存和数据库双写一致性

目前在职,不方便请假去面试,就和一个闪电快车的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日志的功能。另外,重试机制是采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可。

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

  • 0
    点赞
  • 2
    评论
  • 2
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:1024 设计师:我叫白小胖 返回首页

打赏作者

编程界的彭于晏qaq

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值