ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

字节跳动超高难度三面java程序员面经,分分钟搞定!

2021-07-06 08:54:04  阅读:111  来源: 互联网

标签:缓存 java 请求 数据库 面经 写入 更新 程序员 数据


# 如何保证缓存和数据库一致性 说了这么多缓存的必要性,那么使用缓存是不是就是一个很简单的事情了呢,我之前也一直是这么觉得的,直到遇到了需要缓存与数据库保持**强一致**的场景,才知道让数据库数据和缓存数据保持一致性是一门很高深的学问。 从远古的硬件缓存,操作系统缓存开始,缓存就是一门独特的学问。这个问题也被业界探讨了非常久,争论至今。我翻阅了很多资料,发现其实这是一个权衡的问题。值得好好讲讲。 **以下的讨论会引入几方观点,我会跟着观点来写代码验证所提到的问题。** # 不更新缓存,而是删除缓存 **大部分观点认为,做缓存不应该是去更新缓存,而是应该删除缓存,然后由下个请求去去缓存,发现不存在后再读取数据库,写入缓存。** 观点引用:《分布式之数据库和缓存双写一致性方案解析》孤独烟 > **原因一:线程安全角度** > > 同时有请求A和请求B进行更新操作,那么会出现 > > (1)线程A更新了数据库 > > (2)线程B更新了数据库 > > (3)线程B更新了缓存 > > (4)线程A更新了缓存 > > 这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。 > > **原因二:业务场景角度** > > 有如下两点: > > (1)如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。 > > (2)如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。 **其实如果业务非常简单,只是去数据库拿一个值,写入缓存,那么更新缓存也是可以的。但是,淘汰缓存操作简单,并且带来的副作用只是增加了一次cache miss,建议作为通用的处理方式。** # 先操作缓存,还是先操作数据库 **那么问题就来了,我们是先删除缓存,然后再更新数据库,还是先更新数据库,再删缓存呢?** 先来看看大佬们怎么说。 《【58沈剑架构系列】缓存架构设计细节二三事》58沈剑: > 对于一个不能保证事务性的操作,一定涉及“哪个任务先做,哪个任务后做”的问题,解决这个问题的方向是:如果出现不一致,谁先做对业务的影响较小,就谁先执行。 > > 假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次Cache miss。 > > 假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致。 沈剑老师说的没有问题,不过**没完全考虑好并发请求时的数据脏读问题**,让我们再来看看孤独烟老师《分布式之数据库和缓存双写一致性方案解析》: > **先删缓存,再更新数据库** > > 该方案会导致请求数据不一致 > > 同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形: > > (1)请求A进行写操作,删除缓存 > > (2)请求B查询发现缓存不存在 > > (3)请求B去数据库查询得到旧值 > > (4)请求B将旧值写入缓存 > > (5)请求A将新值写入数据库 > > 上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。 **所以先删缓存,再更新数据库并不是一劳永逸的解决方案,再看看先更新数据库,再删缓存这种方案怎么样?** > **先更新数据库,再删缓存**这种情况不存在并发问题么? > > 不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生 > > (1)缓存刚好失效 > > (2)请求A查询数据库,得一个旧值 > > (3)请求B将新值写入数据库 > > (4)请求B删除缓存 > > (5)请求A将查到的旧值写入缓存 > > ok,如果发生上述情况,确实是会发生脏数据。 > > 然而,发生这种情况的概率又有多少呢? > > 发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,**数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。** **先更新数据库,再删缓存依然会有问题,不过,问题出现的可能性会因为上面说的原因,变得比较低!** (补充说明:我用了“先更新数据库,再删缓存”且不设过期时间策略,会不会有问题呢?由于先缓存和更新数据库不是原子的,如果更新了数据库,程序歇逼,就没删缓存,由于没有过期策略,就永远脏数据了。) 所以,如果你想实现基础的缓存数据库双写一致的逻辑,那么在大多数情况下,在不想做过多设计,增加太大工作量的情况下,请**先更新数据库,再删缓存!** # 我非要数据库和缓存数据强一致怎么办 那么,如果我非要保证绝对一致性怎么办,先给出结论: **没有办法做到绝对的一致性,这是由CAP理论决定的,缓存系统适用的场景就是非强一致性的场景,所以它属于CAP中的AP。** 所以,我们得委曲求全,可以去做到BASE理论中说的**最终一致性**。 > 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性 大佬们给出了到达最终一致性的解决思路,主要是针对上面两种双写策略(先删缓存,再更新数据库/先更新数据库,再删缓存)导致的**脏数据问题,进行相应的处理,来保证最终一致性。** # 缓存延时双删 问:先删除缓存,再更新数据库中避免脏数据? 答案:采用延时双删策略。 上文我们提到,在先删除缓存,再更新数据库的情况下,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。 **那么延时双删怎么解决这个问题呢?** > (1)先淘汰缓存 > > (2)再写数据库(这两步和原来一样) > > (3)休眠1秒,再次淘汰缓存 > > 这么做,可以将1秒内所造成的缓存脏数据,再次删除。 **那么,这个1秒怎么确定的,具体该休眠多久呢?** > 针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 **如果你用了mysql的读写分离架构怎么办?** > ok,在这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。 > > (1)请求A进行写操作,删除缓存 > > (2)请求A将数据写入数据库了, > > (3)请求B查询缓存发现,缓存没有值 > > (4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值 > > (5)请求B将旧值写入缓存 > > (6)数据库完成主从同步,从库变为新值 > > 上述情形,就是数据不一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。 **采用这种同步淘汰策略,吞吐量降低怎么办?** > ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。 **所以在先删除缓存,再更新数据库的情况下**,可以使用延时双删的策略,来保证脏数据只会存活一段时间,就会被准确的数据覆盖。 **在先更新数据库,再删缓存的情况下**,缓存出现脏数据的情况虽然可能性极小,但也会出现。我们依然可以用延时双删策略,在请求A对缓存写入了脏的旧值之后,再次删除缓存。来保证去掉脏缓存。 ## 言尽于此,完结 无论是一个初级的 coder,高级的程序员,还是顶级的系统架构师,应该都有深刻的领会到设计模式的重要性。 * 第一,设计模式能让专业人之间交流方便,如下: 程序员A:这里我用了XXX设计模式 程序员B:那我大致了解你程序的设计思路了 * 第二,易维护 项目经理:今天客户有这样一个需求… 程序员:明白了,这里我使用了XXX设计模式,所以改起来很快 * 第三,设计模式是编程经验的总结 程序员A:B,你怎么想到要这样去构建你的代码 程序员B:在我学习了XXX设计模式之后,好像自然而然就感觉这样写能避免一些问题 * 第四,学习设计模式并不是必须的 程序员A:B,你这段代码使用的是XXX设计模式对吗? 程序员B:不好意思,我没有学习过设计模式,但是我的经验告诉我是这样写的 ![image](http://www.icode9.com/i/li/?n=2&i=images/20210706/1625531591960907.jpg) 从设计思想解读开源框架,一步一步到Spring、Spring5、SpringMVC、MyBatis等源码解读,我都已收集整理全套,篇幅有限,这块只是详细的解说了23种设计模式,整理的文件如下图一览无余! [**资料领取方式:点击这里下载**](https://docs.qq.com/doc/DSmxTbFJ1cmN1R2dB) ![image](http://www.icode9.com/i/li/?n=2&i=images/20210706/1625531591104711.jpg) 搜集费时费力,能看到此处的都是真爱!

标签:缓存,java,请求,数据库,面经,写入,更新,程序员,数据
来源: https://blog.51cto.com/u_15290980/2985374

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有