数据库缓存一致性问题

Posted 热爱编程的大忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据库缓存一致性问题相关的知识,希望对你有一定的参考价值。

数据库缓存一致性问题


问题: 更新数据时是先删除缓存还是先更新数据库?

  • 先删除缓存再更新数据库


问题: 两个并发操作,一个更新操作,一个查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把旧的数据读出来放到缓存中,然后更新了数据库,于是缓存中的数据还是老的数据。

  • 先更新数据库,再删除缓存


大家可以思考一下,这样就完全没有一点漏洞存在了吗?


缓存更新的常见Design Pattern

Cache Aside Pattern(旁路缓存模式)

流程:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

简单概括: 先更新数据库,再删除缓存。

存在的漏洞: 两个并发线程,一个读,一个写,读线程发现缓存失效,去数据库查询数据,查询完后更新redis; 但是更新redis前,写线程率先完成了写入操作,导致读线程最终放入redis的还是旧数据。

不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。


Read/Write Through Pattern(读写穿透模式)

在上面的Cache Aside套路中,我们的应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。所以,应用程序比较啰嗦。而Read/Write Through套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。

  • Read Through

Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

  • Write Through

Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)


对于读写缓存穿透模式而言,我们直接面向缓存代理进行数据存储管理开发,而由缓存代理帮我们实现缓存和数据库的一致性。


Write Behind Caching Pattern(异步缓存写入模式)

Write Behind 又叫 Write Back。一些了解Linux操作系统内核的同学对write back应该非常熟悉,这不就是Linux文件系统的Page Cache的算法吗?是的,你看基础这玩意全都是相通的。所以,基础很重要,我已经不是一次说过基础很重要这事了。

Write Back套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。

但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。软件设计从来都是取舍Trade-Off。

另外,Write Back实现逻辑比较复杂,因为他需要track有哪数据是被更新了的,需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫lazy write。

在wikipedia上有一张write back的流程图,基本逻辑如下:


补充

为什么不能在更新完数据库后更新缓存呢?


1.线程A先发起一个写操作,第一步先更新数据库
2.线程B再发起一个写操作,第二步更新了数据库
3.由于网络等原因,线程B先更新了缓存
4.线程A更新缓存。

这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了,脏数据出现啦。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。


缓存双删

有些小伙伴可能会说,不一定要先操作数据库呀,采用缓存延时双删策略就好啦?什么是延时双删呢?


1.先删除缓存
2.再更新数据库
3.休眠一会(比如1秒),再次删除缓存。

这个休眠一会,一般多久呢?都是1秒?

  • 这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

删除缓存重试机制

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,如果第二步的删除缓存失败呢,删除失败会导致脏数据哦~

删除失败就多删除几次呀,保证删除缓存成功呀~ 所以可以引入删除缓存重试机制


1.写请求更新数据库
2.缓存因为某些原因,删除失败
3.把删除失败的key放到消息队列
4.消费消息队列的消息,获取要删除的key
5.重试删除缓存操作


读取biglog异步删除缓存

重试删除缓存机制还可以,就是会造成好多业务代码入侵。其实,还可以通过数据库的binlog来异步淘汰key。


mysql为例 可以使用阿里的canal将binlog日志采集发送到MQ队列里面,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性


转载

缓存更新的套路

美团二面:Redis与MySQL双写一致性如何保证?

以上是关于数据库缓存一致性问题的主要内容,如果未能解决你的问题,请参考以下文章

redis缓存一致性

数据库与缓存一致性问题解决方案

redis缓存与数据一致性

Redis缓存和数据库一致性问题

redis读写一致性遇到的问题

redis系列之数据库与缓存数据一致性解决方案