麒麟v10 上部署 TiDB v5.1.2 生产环境优化实践
746
2023-04-07
分布式缓存--缓存与数据库一致性方案
1. 概述
缓存设计是应用系统设计中重要的一环,是通过空间换取时间的一种策略,达到高性能访问数据的目的;但是缓存的数据并不是时刻存在内存中,当数据发生变化时,如何与数据库中的数据保持一致,以满足业务系统要求,本篇将给出具体分析。
2. 强一致与最终一致性
所谓强一致,就是指系统在对外提供服务的过程中,时刻让缓存数据与数据库保持一致,这种情况比如秒杀系统,商家后台,他会设置秒杀商品,参与秒杀活动,一旦说他参与了秒杀活动,商品的库存本来是在数据库里的,此时必须直接被加载到缓存里,缓存立马就要可以被使用。最终一致性,就是允许缓存与数据库在中间一小段时间中有不一致的情况,但是最终两者是一致的,比如微博的粉丝数,页面每天的访问数。本篇重点讲最终一致性,强一致的情况后续分析。
3. 缓存与数据库一致性
3.1 缓存的更新机制
缓存的更新,一般分为被动更新与主动更新,被动更新是指缓存在有效期到后,被淘汰。被动更新如下步骤:step1: 发起方查数据,缓存中没有,从数据库中获取,并写入缓存,同时设置过期时间t;step2: 在t内,所有的查询,都由缓存提供,所有的写,直接写数据库;step3: 当缓存数据到过期时间t后,缓存数据失效。后面的查询,回到了第1步。
主动更新,一般为调用方发起缓存与数据库同时更新,缓存分为删除、更新,数据库分为更新,通过组合与先后顺序,分为如下四种情况:更新缓存、更新数据库,更新数据库,更新缓存,删除缓存,更新数据库,更新数据库,删除缓存,下面逐一分析。
3.2 更新缓存、更新数据库
这种情况,当缓存更新成功,数据库更新不成功时,数据不一致的风险比较高,所以一般不采用。
3.2 更新数据库、更新缓存
当更新完数据库,缓存的加载前需要通过大量复杂计算才能得出缓存的值,不仅让发起方阻塞,影响性能;而且如果缓存命中率不高,很少使用,更浪费前期的复杂计算成本与缓存空间,这里就不符合懒加载的设计思想,故一般也不采用。
3.3 删除缓存、更新数据库
3.4 更新数据库,删除缓存
前提:缓存无数据,数据库有数据。A:查询,B:更新过程如下:step1: A查缓存,无数据,去读数据库,旧值;step2: B更新数据库为新值;step3: B删除缓存;step4: A将旧值写入缓存。
该场景最终也会出现不一致,产生的根源是是读比写慢,这种是小概率事件,一般很少出现,如果非要解决这种情况,可以采用延迟双删,再删除一次缓存。
3.5 Read/Write Through
上面的方式,数据库是缓存的来源,主导是数据库,而 Read/Write Through模式,相当于缓存占主导。在cache-aside模式中,我们的应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。而Read/Write Through做法是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。
Read Through 就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。
Write Through, 和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己同步更新数据库
值得注意的是,该方案在实现过程中,程序启动时,需将数据库的数据, 提前放到缓存中,不能等启动完成,再放缓存中。
3.5 Write Behind
Write Behind 又叫 Write Back。一些了解Linux操作系统内核的同学对write back应该非常熟悉,这不就是Linux文件系统的Page Cache的算法吗?是的,你看基础这玩意全都是相通的。所以,底层思想很重要,我已经不是一次说过底层很重要这事了。
Write Behind 思想,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作速度飞快(因为是直接操作内存),同时带来吞吐量大幅上升;因为异步,Write Behind 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。如果说软件功能模块的思维是逻辑与实现,那么软件架构设计的思维是权衡与取舍。
4. 总结
(1)上面讲的一些模式,具体在实际设计过程中,需要根据场景做权衡,这些东西都是计算机体系结构里的设计,比如CPU的缓存,硬盘文件系统中的缓存,硬盘上的缓存,数据库中的缓存。基本上来说,这些缓存更新的设计模式都是非常经典的,而且历经长时间考验的策略,所以这也就是,工程学上所谓的最佳实践。(2)有时候,我们觉得能做宏观的系统架构的人一定是很有经验的,其实,宏观系统架构中的很多设计都来源于这些微观的东西。比如,云计算中的很多虚拟化技术的原理,和传统的虚拟内存不是很像么?Unix下的那些I/O模型,也放大到了架构里的同步异步的模型,还有Unix发明的管道不就是数据流式计算架构吗?如果你要做好架构,首先你得把计算机体系结构以及很多底层的技术吃透了,应用层的架构一定能从底层找到原型或者影子。
3)在软件开发或设计中,我非常建议在之前先去参考一下底层软件已有的设计和思路,比如操作系统、编译原理、计算机组成原理以及网络,找到相应的经典设计思路与最佳实践,吃透了已有的这些东西,再决定是否要重新发明轮子。千万不要似是而非地,想当然的做软件设计。
4)上面,我们没有考虑缓存(Cache)和持久层(Repository)的整体事务的问题。比如,更新Cache成功,更新数据库失败了怎么吗?或是反过来。关于这个事,如果你需要强一致性,你需要使用两阶段提交协议——prepare, commit/rollback,比如Java 7 的XAResource,还有MySQL 5.7的 XA Transaction,有些cache也支持XA,比如EhCache,关于事务问题后续再分析。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。