缓存与主副本数据一致性系统设计方案(上篇)

摘要:多数谈及 Cache-Aside 模式的文章中,几乎无例外的推荐采取更新数据库后删除缓存的方式,这种方式真的是正确的吗?更新数据库后更新缓存的方式,真的容易造成数据不一致吗?会不会是实现方式有问题?在系统设计与实际场景下,在两种实现方式中究竟要如何选择?如果你也有这些疑问,本文希望能够纠正一系列广泛流传的错误,同时能为你提供一种可借鉴的解决方案。


对应用系统来讲,为提升系统整体性能与并发度,通常会采取为系统添加基于内存的存储系统方案。由该存储系统充当缓存(本文余下内容会采用“缓存 / Cache”指代此类存储系统),实现对应用系统整体的性能提升,并带来更高的并发表现。

通常在引入缓存后,加上以数据库为代表的数据存储系统,势必会导致系统内存在两个数据存储系统。因为两个数据存储系统的存在,从架构上便构成了主从架构。因为数据需要在两个副本之间传递复制,这一行为带来了数据一致性挑战。面对主从副本数据一致性解决方案主要分为两类,分别属于 强一致性解决方案 和 弱一致性解决方案 。

本文接下来讨论的方案属于弱一致性,对于弱一致性而言,最简单的解决方式是对所有非主副本的数据变更,均通过对主副本拷贝来实现。

如果你是一名有经验的工程师,对于缓存与数据库一致性方案一定听说过 Cache-Aside Pattern ,该模式以数据库做为数据的主副本,同时也是应用系统广泛采纳的设计方案。Cache-Aside 模式不会主动将数据复制到缓存中,而是在缓存未查询到数据时,从数据库复制到缓存中,所以又名 Lazy Loading 模式。

除此以外,你可能也听过 Read-Through 与 Write-Through 两种模式。这两种模式依赖缓存系统的能力,通过缓存系统内置能力实现对数据库的查询与更新操作,而应用系统无需关心对数据库的变更,只与缓存打交道。在应用系统视角下好像数据库不存在一样,将缓存看作是数据主副本。但现实往往未必如此理想,所以在本专栏文章中给出的系统设计方法,会避免技术与组件的特性,尽可能做到技术中立

在实现 Cache-Aside 模式的方案中,主要争议点在数据变更场景(数据变更存在三种,分别是数据的新增、修改和删除)下数据同步到缓存的方式。两种方式最核心的区别是数据复制到缓存的时机不同。 一种会主动清理缓存中数据,依赖于数据查询时从数据库复制到缓存中。另一种会主动将数据复制到缓存中,确保数据常驻缓存。

Cache-Aside 模式下数据读取方式

在探讨两种数据变更方式之前,我们先熟悉一下 Cache-Aside 模式下数据读取的方式。其执行过程如下:

  1. 当对于某条数据记录的查询请求到来时,应用服务先确认该数据记录是否存在于缓存中;
  2. 如果数据记录在缓存中则直接返回查询结果;
  3. 如果数据记录不在缓存中,则查询主副本(数据库)获取该数据记录;
  4. 将主副本数据记录复制到缓存中,并返回该数据记录的查询结果。

结合下图与下方的注释说明,可以进一步直观的理解数据读取时的的过程。

如上图所示我们通过 Server - 1 与 Server - 2 两个应用服务来分别说明两种数据读取场景(本文中的配图均会采取此种表示,后续对于相同内容不再赘述):

  • 缓存中存在查询记录数据
  1. 图中 Server - 1 欲查询数据记录 1 的数据,如图中红色字体所示;
  2. 与 Server - 1 相关的数字 1-2 表示 IPC 请求与响应过程,其中可以将线条的斜率理解为该过程的耗时,斜率越大耗时越久;
  3. 绿色线条表示缓存执行数据记录查询操作的耗时,如 query cache time 所示;
  4. server - 1 query data process time 所代表的蓝色线条为执行本次缓存记录查询操作的总耗时;
  • 缓存中不存在查询记录数据
  1. 图中 Server - 2 欲查询数据记录 2 的数据,如图中红色字体所示,但缓存中不存在该数据;
  2. 与 Server - 2 相关的数字 1-6 表示 IPC 请求与响应过程,其中可以将线条的斜率理解为该过程的耗时,斜率越大耗时越久;
  3. 黑色线条表示数据库执行记录查询操作的耗时,即 i:query data time 所示;
  4. 蓝色线条表示应用服务执行操作耗时,如 server process time 所示,均表示收到 IPC 成功响应后准备发起后续 IPC 请求的耗时;
  5. 绿色线条表示缓存执行数据记录修改操作的耗时,即 iii:modify cache time 所示;
  6. 其中 server - 2 query data process time 所代表的蓝色线条为执行本次数据记录查询操作的总耗时;

Cache-Aside 模式下两种数据变更实现方式

上文中提到两种实现方式最核心的区别是数据复制到缓存的时机不同。因为时机不同使得两种实现方式差异明显。接下来我们分别探讨两种实现方式。

采取更新主副本数据后删除缓存方式

结合下方图示,删除缓存的数据变更的实现过程描述如下:

  1. 对于某条数据记录的变更请求到来时,应用服务先变更主副本(数据库)对应数据记录;
  2. 之后删除缓存中对应数据记录;

操作完成后数据库对应记录为 1,而缓存中不存在该记录值。

采取更新主副本数据后更新缓存方式:

这里提供的方案与网络上多数文章实现略有不同,请结合下方图示,更新缓存的数据变更的实现过程描述如下:

  1. 对于某条数据记录的变更请求到来时,应用服务先变更主副本(数据库)对应数据记录;
  2. 之后再次向主副本(数据库)发起对该记录的数据查询请求(切记不可以依赖应用服务进程/内存中数据);
  3. 将从主副本(数据库)查询到的数据记录更新到缓存中;

操作完成后数据库与缓存中的记录均为 1。

数据不一致的问题

在探讨两种实现方式孰优孰劣的争辩中,主要集中在潜在的数据不一致问题上。在前文的图示中,一直暗含着三类明显的组件。分别是代表主副本的数据库组件,以及缓存组件,余下的便是由多个实例组成的应用服务组件,这三类组件构成了一个分布式系统。

在分布式系统设计中主要面临三大挑战:对组件并发性的支持以及克服全局时钟的缺乏和管理组件的独立失效。两种模式在数据变更场景下不涉及全局时钟的问题,我们围绕对组件并发性与组件独立失效,来分析两种模式下数据不一致的场景。

首先来看面对组件独立失效时,会产生怎样的效果。因为数据库扮演者主副本的角色,所以当数据库不可用时,可以认为整个系统是失去可用性的。此时已经无需再谈及数据一致性问题了。所以我们重点关注缓存组件失效后,会产生的效果。组件失效可能存在多种原因,这里探讨的是对缓存操作失效的情况。结合下图,分别展示了两种方式下缓存操作失效引发的数据不一致。当数据修改操作完成后,数据库中记录值已被修改为 2 ,但是在两种方式下,一种记录旧数据并未删除,另一种是未成功更新缓存中的记录值。

除了针对缓存组件操作失效外,另一种分布式挑战是面对组件的并发访问特性。在面对应用服务的并发操作时,两种实现方式依然存在导致数据不一致的情况。

更新主副本数据后删除缓存方式并发访问问题

因为对于缓存的删除操作具有幂等性,所以即便是在多个应用服务并发修改的情况下,缓存数据都会以空值做为最终状态。那么会导致数据不一致的场景必然是混合了数据变更与查询的情况。

结合下面图示,系统中存在 3 个应用服务,其中服务 1-2 将同一条数据记录分别修改为 1 和 2。当 Server - 1 完成操作后,缓存中的数据已经为空值。在 Server - 2 发起数据修改前 Server - 3 执行数据查询逻辑,因为缓存中数据不存在所以从数据库中获取到记录数据为 1,并准备将其更新到缓存中。在 Server - 3 更新缓存之前 Server - 2 将记录数据更改为 2,并且在 Server - 3 更新缓存前完成了数据修改操作。如图所示 Server - 3 因为 GC 等原因导致进程/线程执行受阻,导致对缓存的更新操作滞后,将记录的历史值更新到了缓存中。最终缓存与数据主副本相同记录的数据呈现不一致的状态。

更新主副本数据后更新缓存方式并发访问问题

因为数据常驻缓存,所以读请求不会影响数据一致性,问题只会发生在并发修改时。所以,本文给出的实现方式为更新完数据库后,再次获取数据库最新值可以有效降低并发情况下出现的数据不一致现象。因数据库行级锁的存在,导致对于数据库同一条记录的修改操作必然是顺序执行逻辑。进而对于数据库记录查询操作起到了顺序约束作用,但后续操作的并发性,依然会引起数据不一致的情况。

结合下面图示,系统中存在 2 个应用服务分别将同一条数据记录修改为 1 和 2。Server - 2 因为网络原因导致执行数据更新操作较慢,在 Server - 2 完成对数据记录更新为 2 的操作前,Server - 1 已经完成数据查询操作。此时只要 Server - 2 在 Server - 1 之前完成对缓存的更新操作,便会导致缓存与主副本数据不一致的情况,如图中所示 Server - 1 因 GC 等原因导致进程/线程执行受阻,使得更新缓存操作在 Server - 2 之后完成。最终缓存与数据库主副本相同记录的数据呈现不一致的状态。

分析完两种实现方式下可能存在的数据不一致问题,不论是针对缓存操作失效导致的不一致,还是组件并发性导致的数据不一致。两种实现方式所需要采取的解决方案是一致的,我们会在下篇对如何解决一致性问题给出改进方案

现阶段两种实现方式的选择

目前主流的选择都是采用删除缓存的方式,但更新缓存的方式真的不适用吗?将缓存做为数据从副本引入系统,通常都是为了解决性能与并发问题,即读多写少的场景。当数据查询请求远高于数据变更请求时,仅仅因并发修改而存在数据不一致问题的方案真的不如因查询引起数据不一致问题的方案吗?

我们上文中提到两种实现方式最核心的区别是数据复制到缓存的时机不同,这使得两者最大的区别反而在读取场景下。删除缓存方式存在 cache miss 的情况,而更新缓存方式确保数据常驻缓存,不会发生 cache miss 的情况。所以如果你的应用在超高并发访问下,无法承受 cache miss 造成对数据库(主副本)的访问压力时,毫无疑问只能选择更新缓存的方式。不过代价则是需要更大容量的缓存存储数据,因为该方式下缓存数据是全量复制于主副本。

通常情况下,超高并发访问场景也是针对数据而言,极少会存在全量数据均处于高并发情况下。所以我们可以基于热点数据的维度,将两种模式进行结合,以换取较低的缓存容量要求。实现方式会略复杂一些,如果你感兴趣的话,请留言告诉我,我会在后续文章中探讨具体实现。

在实现复杂度上,基于本文目前讨论的内容来讲,两种方式实现复杂度几乎相同,如果要一定要区分的话更新缓存会相对复杂一点。同时删除缓存的方式还有更好语义一致性。因为当数据新增、修改和删除时,更新缓存的方式也要对应三种逻辑。

最后,两种实现方式在数据一致性等级方面均属弱一致性,所以依然存在数据不一致情况,我们会在下篇中给出解决不一致的方案,到时会对两者的选择再进行一次对比。如果对数据一致性要求没有那么高的话,可以基于以下表格选择适合于自身应用场景的解决方案。

对比内容更新主副本数据后删除缓存方式更新主副本数据后更新缓存方式
数据读取是否存在 cache miss存在不存在
对缓存容量要求相对较低
数据一致性等级弱一致性弱一致性
应用场景适合一般并发访问场景,可以接受 cache miss 对主副本造成的访问适合超高并发访问场景,避免出现 cache miss 的情况对主副本造成访问压力
实现复杂度(最高:🌟🌟🌟🌟🌟)🌟🌟🌟

本文为系统设计实战:常用架构与模式详解专栏系列文章。你背了很多八股,刷了许多算法,面对系统设计却无从下手。你迈过中高级工程师的门槛,却苦于缺乏系统设计实践经验,而迷失成长方向。系统设计实战专栏便是为你准备的!我将在其中持续更新技术中立的系统设计系列文章。如果您发现文章内容中任何不准确或遗漏的部分。非常希望您能评论指正,我将尽快修正疏漏,为大家提供优质技术内容。

关联阅读

你好,我是 HAibiiin,一名探索技术之外更多可能性的 Product Engineer。如果本篇文章对你有所启发或提供了一定价值,还请不要吝啬点赞、收藏和关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值