如何保证缓存与数据库双写时的数据一致性?第2部分

这是该系列的最后一篇文章。上一篇我们介绍了为什么需要缓存,也介绍了Read Aside的流程和潜在的问题,当然也解释了如何提高Read Aside的一致性。尽管如此,Read Aside还不足以满足高一致性要求。

Read Aside会导致问题的原因之一是因为所有用户都可以访问缓存和数据库。当用户同时操作数据时,由于操作顺序的各种组合而导致不一致。

那么我们就可以通过限制操作数据的行为来有效避免不一致,这就是后面几个方法的核心概念。

通读

读取路径

写入路径

潜在问题

这种方式最大的问题是不是所有的缓存都支持,本文的Redis例子不支持这种方式。

当然,有些缓存是支持的,比如NCache,但是 NCache 也有它的问题。

首先,它不支持很多客户端 SDK。.NET Core是原生支持语言,剩下的选项不多了。

另外,它分为开源版和企业版,但是要知道,如果开源版的人用的不是很多,那么出了问题就悲剧了。即便如此,企业版不仅需要支付基础设施费用,还需要支付软件许可证费用。

如何提高

NCache的成本很高,是否可以自己实现Read Through?答案是肯定的。

对于应用程序来说,我们并不关心它背后有什么样的缓存,只要它足够快地为我们提供数据,这就是我们所需要的。因此,我们可以将 Redis 封装为一个独立的服务,称为数据访问层(DAL),内部有一个 API 服务器来协调缓存和数据库。

应用程序只需要使用定义的 API 从 DAL 中获取数据,而不需要关心缓存如何工作或数据库在哪里。

直写

读取路径

写入路径

潜在问题

Read Through一样,并非每个缓存都受支持,必须自行实现。

此外,缓存并非设计用于数据操作。很多数据库都有缓存不具备的能力,尤其是关系型数据库的ACID保证。

更重要的是,缓存不适合数据持久化。当应用程序写入缓存并认为更新已完成时,缓存可能仍会因为“某种原因”而丢失数据。然后,当前的更新将永远不会再发生。

如何提高?

Read Through一样,必须实施 DAL,但 ACID 和持久性问题仍未克服。于是,

Write Ahead诞生了。

提前写

读取路径

写入路径

潜在问题

同样,许多缓存也不支持Write Ahead 。尽管读取路径和写入路径看起来与Write Through相同,但其背后的实现却大不相同。

Write Ahead是为了解决Write Through的问题而产生的,所以先介绍一下。

我们还将实现一个 DAL,但与Write Through不同,它实际上是一个内部消息队列而不是缓存。从上图可以看出,整个DAL架构变得更加复杂。要正确使用消息队列,需要更多的领域知识和更多的人力资源来设计和实现。

如何提高?

通过使用消息队列,可以有效保证变化的持久性,同时消息队列也保证了一定的原子性和隔离性,虽然没有关系型数据库那么完备,但仍然具有基本的可靠性。

此外,消息队列可以将碎片化的更新合并成批次。例如,当一个应用程序想要更新三个缓存以便发送三个消息时,DAL worker 可以将这三个消息合并为一个 SQL 语法以减少对数据库的访问。

需要注意的是必须使用消息队列来保证消息的顺序,因为对于数据库的更新来说,先插入再删除和先删除再插入的意义是截然不同的。每个消息队列确保消息顺序的方法略有不同

1)Kafka ,可以通过使用正确的分区键来实现。

2)RocketMQ,可以基于队列(分区)的顺序消费,顺序消费,分为全局顺序消费,和局部顺序消费。
全局顺序消费:只能有一个数据队列(queue),和一个消费者实例。原因是RocketMQ只提供在单个queue上使用FIFO顺序的有序消息。多个queue之间并不能保证消息的严格先后性。
局部顺序消费:通常在实际应用中,我们需要将同一个订单号的相关操作,按照规则(可以是hash或取模等)发送到同一个queue上(使用MessageQueueSelector ),然后消费者实例,使用顺序消费模式消费消息(使用MessageListenerOrderly)

然而,实现Write Ahead的复杂度非常高。如果您负担不起这样的复杂性,那么Read Aside是更好的选择。

双重删除

我们已经讨论了两种主要类型的缓存模式,它们是

这两种类型之间最根本的区别是实现的复杂性。以Read Aside为例,实现起来非常容易,做对也非常简单。但是,Read Aside在很多交互下很容易产生各种 corner case。

另一方面,通过实现 DAL 可以避免 corner cases,但是正确实现 DAL 非常困难,并且需要广泛的领域知识才能正确实现,这进一步使 DAL 难以实现。

那么,DAL 是减少极端案例数量的唯一方法吗?不,不是真的。

这就是Double Delete模式试图解决的问题。

读取路径

该过程与Read Aside完全相同。

写入路径

潜在问题

Double Delete的目的是尽量减少因Read Aside corner cases而导致灾难的时间。

整个不一致完全取决于等待时间,等于最大等待时间。

但如何等待也是一个棘手的实际问题。如果让client本来就开始处理,那么corner case 2的killed场景还是解决不了。如果其他人以异步的方式执行,那么这之间的通信契约和工作流控制就会很复杂。

如何提高

与Read Aside相同的极端情况 2 ,但同样,它可以通过正常关闭来减少。

结论

在这篇文章中,我们介绍了很多提高一致性的方法。通常,当一致性不是关键要求时,缓存过期就足够了并且需要非常低的实现工作量。事实上,广泛使用的 CDN 只是使用Cache Expiry的其中一种情况。

随着场景越来越关键,对一致性要求越来越高,那就考虑用Read Aside甚至Double Delete来实现。这两种方法的正确实现足以使一致性满足大多数场景。

然而,随着一致性要求的不断提高,需要更复杂的实现,例如Read ThroughWrite Through甚至Write Ahead 。虽然这可以提高一致性,但成本也很高。首先,它需要足够的人力和领域知识来实施。此外,实施的时间成本和事后的维护成本也明显更高。此外,运营这样的基础设施还有额外的费用。

为了进一步提高一致性,需要使用更先进的技术,如共识算法,以多数共识的方式保证缓存和数据库内容的一致性。这也是TAO背后的理念,但我不打算介绍这么复杂的做法,毕竟我们不是Meta,至少我不是。

在一般的组织中,对一致性的要求没有那么严格,比方说,10个或更多的9,一般的组织无法操作如此复杂和庞大的架构。

因此,在本文中,我选择了我们都可以实现的做法,但即使是简单的做法,如果正确实施,也已经具有足够高的一致性。

展开阅读全文

页面更新:2024-03-12

标签:缓存   数据库   数据   队列   路径   顺序   正确   消息   操作   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top