谈谈数据库,缓存一致性

几年前,我在看博客的时候,看到有一篇博客的标题就是关于数据库,缓存一致性的,不觉得然,直接跳过去了,心想,这么简单的问题还讨论个鬼啊。这种想法持续了好久,直到某天,我看到愈来愈多的人都在讨论数据库,缓存一致性的问题,才好好的看了下博客,才发现原来数据库,缓存一致性真不是一个简单的问题。今天我也来谈谈数据库,缓存一致性问题。程序员

科普

考虑到有一些小伙伴可能技术不是那么好,可能没有接触过缓存,因此这里仍是花上一分钟的时间,来介绍下什么是缓存,为何要有缓存,以及数据库和缓存是如何搭配使用的。数据库

读取数据库是比较耗时的操做,若是每次都须要去数据库读取数据,会对数据库形成必定的压力,程序性能也会比较低下,因此须要引入缓存。缓存

缓存是提高程序性能的最重要、最有效、也是最简单的手段之一。服务器

引入缓存后,读操做会先去缓存中看下,若是没有命中缓存,才去读取数据库,而后把读取出来的数据再放到缓存中去,这样下一次读操做就能够命中缓存了,若是命中缓存,就能够直接把数据返回出去了。网络

image.png

写操做,除了修改数据库,还须要删除缓存,由于不删除缓存,读的操做读到的永远都是缓存中的旧数据。并发

先删除缓存,后修改数据库

这个方案显然是有问题的。性能

两个并发的读写操做:线程

  1. 一个写的操做先进来,把缓存删除了;
  2. 在写操做尚未更新数据库的时候,一个读的请求又进来了,发现没有命中缓存,就去数据库把老数据取出来了;
  3. 写操做更新了数据库;
  4. 读操做把老数据放在了缓存中。

这样,数据库中的数据和缓存中的数据就不一致了,为了更好的让你们理解这个过程,献上一张丑到没法自拔的图:
image.pngblog

这个方案显然不行,可是这个方案真的一无可取吗?队列

非也,让咱们设想下这样的场景:一个写的请求进来,删除缓存,这个时候,Redis服务器忽然出问题了,或者网络忽然出问题了,致使删除缓存失败,抛出了一个异常,致使程序没有继续执行修改数据库的操做。从数据库、缓存一致性的角度来讲,这里很好的保证了数据库、缓存的一致性,二者保存的数据是同样的,尽管保存的都是老数据。

先修改数据库,后删除缓存

相信绝大多数小伙伴都是运用的这个方案, 先前我以为数据库,缓存一致性没有什么好讨论的,太简单了,就是由于我以为这个方案是如此完美,可是后面我才慢慢发现这个方案也有必定的问题。

看到第一种方案存在的问题,你们也必定想到了这个方案也有一样的问题。

在没有缓存的状况下,两个并发的读写操做:

  1. 读操做先进来,发现没有缓存,去数据库中读数据,这个时候由于某种缘由卡了,没有及时把数据放入缓存;
  2. 写的操做进来了,修改了数据库,删除了缓存;
  3. 读操做恢复,把老数据写进了缓存。

image.png

这样就形成了数据库、缓存不一致,不过,这个几率出现的很是低,由于这须要在没有缓存的状况下,有读写的并发操做,在通常状况下,写数据库的操做要比读数据库操做慢得多,在这种状况下,还要保证读操做写缓存晚于写操做删除缓存才会出现这个问题,因此这个问题应该能够忽略不计。

说了这么多,并无看到先修改数据库,后删除缓存的致命问题啊,别急,让咱们继续设想这样的场景:一个写的操做进来,修改了数据库,可是删除缓存的时候 ,因为Redis服务器出现问题了,或者网络出现问题了,致使删除缓存失败,这样数据库保存的是新数据,可是缓存里面的数据仍是老数据,妥妥的数据库、缓存不一致啊。

延迟双删

能够看到修改数据库,后删除缓存有两个问题,虽然两个问题都是低几率的,可是永远追求完美的程序员可不能容许有这样的事情发生,因此第三种方案出现了:延迟双删。

延迟双删就是先删除缓存,后修改数据库,最后延迟必定时间,再次删除缓存。

图片.png

这么作就能够在必定程度上缓解上述两个问题,第一次删除缓存至关于检测下缓存服务是否可用,网络是否有问题,第二次延迟必定时间,再次删除缓存,是由于要保证读的请求在写的请求以前完成。

可是这么作,仍是有必定问题,好比第一次删除缓存是成功的,第二次删除缓存才失败,又该怎么办?

内存队列

上面三种方式,都有必定的问题:

  • 修改数据库、删除缓存这两个操做耦合在了一块儿,没有很好的作到单一职责;
  • 若是写操做比较频繁,可能会对Redis形成必定的压力;
  • 若是删除缓存失败,该怎么办?

为了解决上面三个问题,第四种方式出现了:内存队列删除缓存:写操做只是修改数据库,而后把数据的Id放在内存队列里面,后台会有一个线程消费内存队列里面的数据,删除缓存,若是缓存删除失败,能够重试屡次。

这样,就把修改数据库和删除缓存两个操做解耦了,若是删除缓存失败,也能够屡次尝试。因为后台有一个线程去消费内存队列去删除缓存,不是直接删除缓存,因此修改数据库和删除缓存之间产生了必定的延迟,这延迟应该能够保证读操做已经执行完毕了。

可是这么作也有很差的地方:

  • 程序复杂度成倍上升,须要维护线程、队列以及消费者;
  • 若是写操做很是频繁,队列的数据比较多,可能消费会比较慢,修改数据库后,间隔了必定的时间,缓存才被删除。

可是这也是没有办法的事情,哪有十全十美的解决方案。

第三方队列

通常来讲,系统分为前台系统和后台系统,前台系统主要是读操做,后台系统才有写操做。

好比商品中心,前台是面向用户的,当用户打开商品详情页,会去缓存中拿数据,后台是面向业务人员的,业务人员能够在后台系统对商品信息进行修改。

若是是具备必定规模的公司,前台系统和后台系统确定不在同一个服务器上,并且是由不一样的部门去负责的,因此内存队列是确定用不了的,若是后台系统修改数据库后,直接删除缓存,必定会发生以下的故事。

后台系统 小明:大家前台系统的产品详情缓存的key是什么格式的?发我下。
前台系统 小花:Product:XXXXX。
后台系统 小明:好的。

过了几天,小花找到小明。

前台系统 小花:不对啊。大家怎么没有把活动中的产品详情缓存给删掉啊?
后台系统 小明:纳尼,我怎么知道大家是两个缓存啊,把活动中的产品详情缓存的key的格式发我下。
前台系统 小花:Activity:Product:XXXX。
后台系统 小明:好的。

过了几天,订单系统的开发又找到小明。
订单系统 小强:大家修改了产品详情后,还要把订单中的产品详情缓存给删除。
后台系统 小明:。。。

过了几天,广告系统的开发又找到小明。
广告系统 小王:大家修改了产品详情后,还要把广告中的产品详情缓存给删除。

后台系统 小明 卒,享年25。

若是引用了第三方队列,如RabbitMQ,Kafka,小明就不会“卒”了,后台系统的小明修改了数据库后,不须要关心缓存的事情,只要把数据的Id丢到消息队列,前台系统、广告系统、订单系统的开发消费消息队列中的数据删除缓存。

上面说的几种方案,都是比较常见的,也比较简单,固然不一样的方案也能够搭配使用,可是没有“银弹”,没有完美的解决方案,就看大家的研发团队,大家的场景适合哪一种解决方案了。

今天的话题到这里就结束了。

相关文章
相关标签/搜索