我是靠谱客的博主 专注音响,最近开发中收集的这篇文章主要介绍如何保证缓存与数据库的一致性,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

关系型数据库系统给我们带来许多惊艳的特性,例如:ACID。但为了维护这些特性,数据库的性能在高负载下也会下降。为了提高性能,通常会在项目的应用层(处理业务逻辑)和存储层(持久化到数据库)之间添加一个缓存层。因为数据库的性能瓶颈通常是在硬盘(或二级存储)的读写(I/O)。所以缓存层通常使用内存来实现。当然,我们只会将一些“热数据”存储在缓存中。为识别“热数据”,通常会指定一个过期策略,如LFU(最少使用)和LRU(最近最少使用)。

将数据库中部分数据存储在内存缓存中,性能是有所提高,数据库的服务器压力也相应的减小,但一份数据分别存储在两个地方,那我们如何来保证两边的数据是一致?

接下来,我们来看下一些解决方案。

设置缓存过期

这是最简单的一种解决方案。这种方案的做法:将数据直接写入数据库,读取数据时先从缓存中读取,如果缓存数据不存在,再从数据库中读取并写回缓存。在写回缓存的时候为每个数据都添加一个过期时间。

假设缓存过期时间设置为30分钟,在这时候段内,A君更新了数据,而缓存中的数据还未过期,B君读取的数据为脏数据。那如果设置成一分钟或者更短,在大流量和高并发的情况下,将会有很多缓存未能命中,且系统的性能也会大大降低,那么缓存的数据将毫无价值。这就违背了使用缓存的最初目标。

缓存的过期时间设置过长,会导致脏读,会增加数据不一致的时间;设置过短,则缓存无效。所以,很难为过期时间设置一个合适的值。

显然,这种方案不合适高并发,且更改较频繁的数据。当然,我们可以在更新数据时候增加一个更新缓存的机制。

Cache Aside

Cache Aside 就翻译成缓存备用模式。它有三种实现方式,三种方式都是围绕数据读取和写入(新增、修改、删除)。

第一种方式:

读取

  • 缓存命中:直接从缓存返回数据,不查询数据库

  • 缓存没有命中:从只读数据库获取数据,再将数据保存到缓存中

写入

  • 新增、修改、删除数据库中的数据

  • 删除缓存(始终删除而不是更新,下次读取缓存未命中时再插入到缓存)

这种方式在理论情况下,基本上可以保证一致性。当然,也有例外的情况:

  1. 假设:A君已成功更新了数据库,在删除缓存前,B君获得了缓存命中的数据,因为该缓存还未删除。这里B君读取到为脏数据,但缓存还是会被删除。后面C君会得到更新后的值。

  2. 假设:A君已成功更新数据,但是在删除缓存时进程突然中止了,则该缓存将永远不会被删除,后续 C 君将继续读取旧数据。(进程中止的原因有:更新版本,老版本的程序被中止;中止多余的服务;应用程序崩溃。)

  3. 假设:A君未能缓存命中,从数据库中获取了数据。突然,A君出现了未知的故障卡了一下,这时,B君更新数据且删除了条目。之后,A君恢复了且将旧值保存到缓存。后续C君读取的都是脏数据。

当应用程序正确操作数据时,可以尽量减少情况1和情况3的发生。比如说,情况1在更新数据库,马上删除缓存,不要做其它任何事情。而情况2避免人为发生的可能性,但程序崩溃就没办法避免了。最后情况3在从数据库中读取数据后,尽快将结果写入缓存,不要做额外的格式转换。这样可以减少不一致性发生的概率。当然也有一些无法避免的情况,如垃圾回收产生的stop-the-word。

第二种方式

第二种方式是在第一种方式在写入时颠倒下顺序。先删除缓存,再新增、更新、删除数据到数据库。

这种方式虽然解决了第一种方式带来情况1和情况2的问题,但也会出现新的问题。假设A君在更新现有值,已成功清理了缓存,在更新数据库之前,B君来获取数据,此时缓存没能命中,然后就跑去数据库获取值并写入到缓存。但数据库里的值还未更新,这时不会再删除缓存,因此C君获取仍是旧的值。

若要说这种方式与第一种方式中情况1和情况2发生的概率,这种方式会小很多。但也没能有效的改善。因此,也不是一个很好的方案。

第三种方式

第三种方式也是在第一种方式可变操作中更改了缓存写入方式。具体方式:先新增、更新、删除数据库中的数据,再对应的创建、更新、删除缓存。

这种方式也存在一定的问题。假设B君在A君之前更新了数据库,而A君在B君之前更新了缓存。最终还是会导致数据库和缓存不一致。

在多服务部署的情况这种方式发生的概率极大。因此,这也是一种糟糕的方式。

总得来说,缓存备用模式也是一种简单的实现方式,但相比设置缓存过期时间还是比较可靠。如果想进一步提高一致性,这种方案也是不够看的。

Read Through

Read Through 就翻译成只读模式。只读模式不做写入缓存,客户端始终简单从缓存中读取。缓存命中与否对客户端是透明的。如果未命中,缓存会自动从数据库中获取。

只读模式一个致命的缺点是许多缓存可能不支持。比如,Redis 就无法自动从 MySql 获取,除非自己写插件。还好 NCache 是支持,但支持的客户端还是有限。另外还有一些开源或付费版本,如果开源使用的人少,一旦出现问题就杯具。对于付费的企业版本,对于中小企业来说是一个不小的开销。因此可选择就很少了。

对此,我们还剩下自己造车轮的路子了。比如:我们可以把缓存打包为数据访问层,并通过内部API来协调缓存和数据库。这样我们不用关心缓存的类型,只要它足够快地为我们提供数据。当然这个对缓存内部API要很熟悉。

Write Through

Write Through 就翻译成只写模式。只写模式不做读取,而且由 Read Through 来完成。只写模式仅为缓存写入数据,然后以原子方式将数据同步到数据库。

很明显这是把缓存当作了关系型数据库,但是这有个问题,许多数据库具有缓存所不具备的功能,比如:缓存有ACID的保证?再者缓存也不适合数据持久性,那怕 Redis 支持 RDB 和 AOF 的持久化,但也不建议这样用。因为缓存会由于“某种原因”而丢失数据,而这数据丢失了也就没法找回了。所以也就有了 Write Behind 模式了。

Write Behind

Write Behind 模式同样没有读取数据,只管写入,但它不是以原子方式同步数据库。而是通过内部消息队列将数据异步复制到数据库。这样做不仅提高了吞吐量,还不必等复制结果。

消息队列不仅有效的保证数据持久性,也保证了一定程度的原子性和隔离性。虽然没有像关系型数据库那样完整,但基本上是可靠的。

当然,消息队列的加入会使结构变得复杂,使用消息队列也需要相应的领域知识和详细的设计及实现。

此外,消息队列可以将零碎的更新合并到批处理中,例如:在 Redis 5.0 以后的版本中提供了 Redis Steam 的功能,可以合并更改并批量更新到数据库,从而进一步提高了性能。

使用消息队列还有一个值得注意的点,一定要确保消息队列中的执行顺序,比如:数据先更新再删除与先删除再更新是具有不同的含义。显然 Write Behind 的复杂性就高很多。

另外,这里做一个延伸。可以通过解析 MySql 的 binlog,将数据库中的数据同步到 Redis 。这个主要是利用 MySql 的复制原理,用一句话来说就是:从服务器读取主服务器 binlog 中的数据,从而同步到缓存中。当MySQL中有数据写入时,我们就解析MySQL的 binlog,然后将解析出来的数据写入到Redis中,从而达到同步的效果。这种方式实质上一个复制队列,它按顺序记录所有事务,不用担心顺序一致性的问题。

Double Delete

Double Delete 是在 Cache Aside 中第二种方式的伸延,在更新完数据库后让线程稍等片刻再次清除缓存。具体如下:

读取

  • 缓存命中:直接从缓存返回数据,不查询数据库

  • 缓存没有命中:从只读数据库获取数据,再将数据保存到缓存中

写入

  • 首先删除缓存

  • 新增、修改、删除数据库中的数据

  • 等待片刻(如:500ms)

  • 再次删除缓存

Double Delete 最大目的是减少读取旧值保存到缓存中,因此在第二次清除缓存有效清除脏数据,但在等待的过程难免也会有意外的情况发生,即在这等待的时间内可能存在不一致的情况。因此如何设置等待时间也是一个难题。

有人可能会提出,可以通过异步的方式执行,这无异于给自己增加难度。

虽然不完美,但这种发生不一致的可能性比较小吧。

最后

以上这些都不能保证强一致性的要求。如果是为了强一致性,那我们必须在所有操作上实现 ACID。但这做会降低缓存的性能。这就违背我们使用缓存最初目标。当然我们可以根据自己业务情况选择合适的方案。

当一致性不是那么重要时,使用缓存过期就足够了,而且这个实现工作量比较低。比如:CDN使用的就是使用缓存过期之一。

随着一致性的需求越来越高,我们可以使用 Cache Aside 和 Double Delete。这两种基本上能满足大多数方案了。

但是,随着一致性要求的不断增加,就需要 Read Through、Write Through 和 Write Behind 这些模式来实现。虽然提高了一致性,但也要付出相应的代价。比如:需要人力去学习相应的知识来实施,且实施时间成本和之后的维护成本也会相应的增加。除些之外,还需要额外付出基础设施的费用。

当然,如果需要保证强一致,也只能使用更先进的技术了,比如:共识算法。至少目前我不会去掌握。因为我们要的是在100%正确和性能之间做权衡。也许99.9%正确就足够了,我们使用缓存的目的就是为了提高性能。

根据自己的情况选择可以实现的方案,即使是简单的方案,如果能正确实现它们,也能提高一致性。

好了,今天就到这。

祝大家学习愉快!

最后

以上就是专注音响为你收集整理的如何保证缓存与数据库的一致性的全部内容,希望文章能够帮你解决如何保证缓存与数据库的一致性所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(51)

评论列表共有 0 条评论

立即
投稿
返回
顶部