【分布式技术专题】「缓存解决方案」一文带领你好好认识一下企业级别的缓存技术解决方案的运作原理和开发实战(数据更新场景策略和方案分析)

简介: 【分布式技术专题】「缓存解决方案」一文带领你好好认识一下企业级别的缓存技术解决方案的运作原理和开发实战(数据更新场景策略和方案分析)

数据更新场景

在引入缓存后,数据会同时存放在缓存和数据库两个地方。因此,当需要更新数据时,需要确保这两个地方都能够得到更新,并且不同的更新时序可能会产生不同的结果。在业界,已经形成了多种解决数据更新问题的模式,例如Cache Aside Pattern和Read/Write Through等。

Cache Aside Pattern

Cache-Aside Pattern(缓存旁路模式) 是一种常用的缓存更新策略,可以提高系统的性能和可靠性。以下是Cache-Aside Pattern的主要流程:



  1. 读取数据:当应用程序需要获取数据时,首先会检查缓存中是否已经存在所需数据。如果数据存在于缓存中,则应用程序直接从缓存中读取数据,无需访问底层数据存储系统。
  2. 缓存未命中:如果数据不存在于缓存中,即缓存未命中,应用程序会查询底层数据存储系统来获取数据,并将数据存储到缓存中以备后续读取。
  3. 更新数据:当应用程序需要更新数据时,它首先会更新底层数据存储系统中的数据。然后,应用程序会使缓存失效,即从缓存中删除相应的数据。
  4. 下次读取:在下次读取该数据时,应用程序会重新执行步骤1和步骤2,将更新后的数据加载到缓存中。

策略思想

Cache-Aside Pattern的关键思想是将读操作和写操作分开处理,这种策略在数据读取频繁、数据更新相对较少的场景中非常适用

  • 读操作优先从缓存中获取数据,减少对底层数据存储系统的访问,从而提高系统的性能。
  • 写操作会更新底层数据存储系统,并使缓存失效,以保持数据的一致性。

具体操作分析

失效(Invalidation)

  • 当应用程序需要获取数据时,首先会尝试从缓存中查找数据。
  • 如果缓存中不存在该数据(即缓存失效),应用程序会从数据库中读取数据。
  • 读取成功后,应用程序会将数据放入缓存,以便后续访问。

命中(Cache Hit)

  • 当应用程序需要获取数据时,首先会尝试从缓存中查找数据。
  • 如果缓存中存在该数据(即缓存命中),应用程序会直接从缓存中返回数据,避免了对数据库的访问。

更新(Update)

  • 当数据需要更新时,首先将更新操作应用到数据库中。
  • 如果更新成功,再将缓存中对应的数据失效(即从缓存中删除),以便下次访问时能够重新从数据库中读取最新的数据。

通过使用缓存旁路模式,可以有效地提高系统的性能和响应速度。缓存可以减少对数据库的访问次数,从而减轻数据库的负载,并且在缓存命中时可以快速返回数据,减少网络延迟。同时,通过合理管理缓存的失效和更新,可以保证缓存中的数据与数据库中的数据保持一致性。

注意,在使用Cache-Aside Pattern时,应用程序需要考虑缓存和底层数据存储系统之间的一致性问题,以及缓存失效带来的性能损失。可以使用一些技术手段来解决这些问题,例如基于时间的缓存失效策略、使用缓存锁定来处理多个请求同时更新缓存等

两个待分析的问题

  • Cache Aside Pattern为什么不是更新缓存,而是失效(删除)缓存?
  • Cache Aside Pattern在数据更新的时候是采用先更新数据库,再失效缓存。

Cache Aside Pattern为什么不是更新缓存,而是失效(删除)缓存?


如上图所示,假设A、B两个线程,A先更新数据库后 B再更新数据库,然后分别进行更新缓存,但是B先更新缓存成功,A后更新缓存成功,这样就导致数据库是最新的数据但是缓存中是旧的脏数据。而如果失效缓存数据的话,可以保证下一次读请求回源到数据库将最新的数据载入到缓存中,避免脏数据的问题。因此,针对数据更新缓存采用失效的方式进行处理

失效数据方案

根据上述情况,可以采取以下步骤来解决数据更新缓存的问题:

  1. 在A线程更新数据库后,立即使B线程的缓存数据失效。可以通过设置一个标志位或者发送一个通知来实现。
  2. 在B线程更新数据库之前,检查缓存数据是否已经失效。如果缓存数据已经失效,则直接更新数据库并更新缓存。
  3. 如果缓存数据未失效,B线程需要等待一段时间,等待A线程的缓存更新完成。可以使用一些同步机制,如锁或信号量来实现。
  4. 在等待一段时间后,B线程再次检查缓存数据是否已经失效。如果缓存数据已经失效,则直接更新数据库并更新缓存。

通过以上步骤,可以确保在数据更新时,先使缓存数据失效,然后再更新数据库和缓存,从而避免脏数据的问题。这样下一次读请求将会从数据库中获取最新的数据,并将其载入到缓存中。

Cache Aside Pattern在数据更新的时候是采用先更新数据库,再失效缓存

双写不同数据源很容易导致数据不一致的问题。

当A线程先更新数据库,然后B线程再更新数据库,接着分别更新缓存时,如果B线程先更新缓存成功,而A线程后更新缓存成功,那么数据库中的数据就是最新的,但缓存中却是旧的脏数据。为了避免这种情况发生,可以采用失效缓存的方式处理。

通过失效缓存数据,可以确保下一次读请求从数据库中获取最新的数据并将其加载到缓存中,以避免出现脏数据的问题。这种方式能够提高数据一致性,但在并发情况下,同时写数据库和更新缓存仍然存在双写不成功的可能性,这将在后续章节中进一步讨论。

总之,为了保证数据的一致性,建议在数据更新时使用失效缓存的方式进行处理,确保下一次读请求能够获取到最新的数据。

先更新缓存再更新数据库



先更新缓存再更新数据库也存在一些弊端。下面是其中一些可能的问题:

  1. 数据丢失风险:如果在更新缓存成功后,但在更新数据库之前发生错误或中断,那么数据更新将会失败,导致数据库中的数据与缓存中的数据不一致。这可能导致数据丢失的风险。
  2. 系统可用性降低:由于先要更新缓存,然后再更新数据库,整个过程需要两次数据库操作。如果其中一次操作失败或延迟,整个数据更新过程将会更加复杂和耗时。这可能导致系统的可用性下降。
  3. 竞态条件:如果多个并发操作同时尝试更新缓存和数据库,可能会导致竞态条件。例如,如果两个操作同时读取了旧数据到缓存中,然后同时进行写入,最终可能会发生数据不一致的情况。
  4. 错误处理复杂性:先更新缓存再更新数据库会增加错误处理的复杂性。如果在更新缓存时发生错误,需要有一种机制来回滚或撤销这次更新。这增加了系统的复杂性和维护成本。

因此,在设计数据更新方案时,需要综合考虑数据一致性、可用性和系统复杂性等方面的因素。一种常用的做法是在更新数据库后立即更新缓存,以确保数据的一致性。

优化数据懒加载

优化数据懒加载,避免不必要的计算开销:如果某些缓存值需要进行复杂计算才能得出,每次更新数据时都更新缓存可能会导致大量计算性能的浪费,特别是在一段时间内没有读取该缓存数据的情况下。为了更符合数据懒加载的概念并降低计算开销,在读请求到来时再进行计算可能是更好的选择。

以下是改进的建议

  • 延迟缓存更新:在数据更新时,不立即更新相关缓存值,而是延迟到读请求到达时进行计算和更新。这样可以最大限度地避免不必要的计算开销。
  • 设置缓存过期时间:为每个缓存设置适当的过期时间。如果在过期时间内没有读取该缓存数据,就不进行更新。只有当有读请求到来时,再根据需要进行计算和更新缓存。
  • 使用缓存命中率监控:监控缓存的命中率,即缓存被读取的频率。如果某个缓存的命中率较低,说明该缓存可能是不必要的,可以考虑取消或延迟更新该缓存。
  • 考虑基于事件的更新:将缓存更新与特定事件关联,只有在事件触发时才进行计算和更新相关缓存。这样可以确保在需要更新缓存时才进行计算,避免不必要的计算开销。

通过采取上述措施,可以更好地遵循数据懒加载的原则,减少不必要的计算开销,提高系统性能和资源利用率。

失效缓存和更新数据库的顺序问题

在处理数据更新后缓存失效的情况下,针对数据库和缓存更新的时序可以归纳为以下几种情况:

先失效缓存再更新数据库

当缓存数据更新机制中先失效缓存再更新数据库时,可能会导致以下问题:

  1. 数据读取延迟:由于缓存失效后,下一次读取数据需要从数据库中获取,可能会增加读取的延迟,特别是在高并发的情况下。这会影响系统的响应时间和用户体验。
  2. 数据不一致性:如果在缓存失效之后,但在更新数据库之前,有其他请求读取了旧的缓存数据,那么就会导致数据不一致的问题。因为这些请求读取到的是失效的缓存数据,而不是最新的数据库数据。
  3. 频繁的数据库操作:由于缓存失效后立即更新数据库,可能会导致频繁的数据库操作。这会增加数据库的负载和资源消耗,可能影响系统的性能和稳定性。

果请求1的线程在失效缓存后,请求2的线程读请求发现缓存数据为空时,从数据库中读取旧值放入缓存,会导致脏数据的问题确实存在。这是因为在缓存失效期间,B线程读请求发生,缓存尚未更新,所以读取到的是旧值。

解决方案

为了解决这些问题,可以考虑使用更智能的缓存策略,例如:

  • 更新数据库后再失效缓存:先更新数据库,确保数据的一致性,然后再失效相关缓存,以便下一次读取时能获取最新的数据。
  • 使用缓存更新队列:将需要更新数据库的操作放入队列中,然后按顺序处理。这样可以确保数据库的操作顺序和数据一致性,并减少频繁的数据库操作。
  • 引入缓存同步机制:在更新数据库之前,先将缓存标记为过期状态,在更新完成后再重新加载缓存。这样可以避免读取到过期的缓存数据。
  • 更新缓存时加锁:在线程A失效缓存并更新数据库时,可以通过加锁的方式确保其他线程不能读取脏数据。只有当更新完成后,其他线程才能继续读取缓存数据。
  • 延迟失效策略:在线程A失效缓存后,在更新数据库之前,可以设置一个较短的延迟时间,让线程B等待一段时间再进行读取操作。这样可以增加线程A完成更新的机会,减少读取脏数据的概率。
  • 引入版本号或时间戳:在缓存数据中引入一个版本号或时间戳,当读请求发生时,比较请求的时间戳和缓存数据的时间戳,如果时间较新,则不再读取数据库,避免读取脏数据。

需要根据具体的业务场景和需求选择合适的缓存更新策略,以提高系统的性能和数据一致性。

采用延时双删

针对这种情况,可以采用延时双删的策略来有效避免。伪代码如下:

java

复制代码

cache.delKey(key);
db.update(data);
Thread.sleep(xxx);
cache.delKey(key);

这种策略主要在写请求完成数据库更新后,休眠一段时间,然后再次删除可能由读请求引入的脏数据,从而最大限度地减少脏数据的存在。

然而,需要注意的是这种延时双删方式需要线程休眠,会降低系统的吞吐量,并不是一种优雅的解决方式。另外,如果数据库采用主从架构,读取的数据也有可能是主从未同步完成时导致的脏数据。

异步删除处理机制

针对这个问题,还可以考虑采用异步删除的方式。即,在写请求完成数据库更新后,不立即进行删除操作,而是异步地进行删除处理。这样可以避免线程休眠,提高系统的吞吐量,并且在一定程度上解决了主从未同步的问题。

另外,设置缓存的过期时间也是一种解决方案。通过设定适当的过期时间,当缓存过期后,系统会自动载入最新的数据,并且需要系统能够容忍一段时间的数据不一致性。

Write/Read Through  缓存透写/透读

在Cache Aside模式中,对于数据库和缓存的更新逻辑由调用方自行控制,这显然是一个相当复杂的过程。而在写透写/读透读模式中,对于调用方而言,缓存是整个数据存储的接口,而不需要关心缓存背后的数据库更新,数据库的更新由缓存统一管理,对于调用方来说,只需要与缓存进行交互,整个过程是透明的。

在写透写/读透读模式下

当调用方要更新数据时,首先会将数据写入缓存。缓存在收到写请求之后,会负责将数据更新到数据库,并确保数据的一致性。读取数据时,调用方也直接通过缓存进行读取,如果缓存中不存在所需数据,缓存会自动从数据库中获取。这样,调用方就可以将缓存作为数据存储的唯一接口,而不需要直接与数据库进行交互。

读写模式的有点机制

使用写透写/读透读模式可以简化调用方的代码逻辑,减少数据访问的复杂性。同时,由于缓存统一管理数据库的更新,可以提高系统的性能和吞吐量。然而,需要注意的是,缓存和数据库之间的数据一致性需要得到保证,可能需要采用一些额外的机制来解决缓存与数据库之间的同步问题。



总结起来,写透写/读透读模式将缓存作为整个数据存储的接口,统一管理数据库的更新,可以简化调用方的代码逻辑,并提高系统的性能。在实际使用中,需要根据具体的需求和场景,综合考虑数据一致性、性能需求以及系统复杂性等因素,选择合适的缓存策略。

Write Behind Cache Pattern 后写缓存模式

后写缓存模式是在数据更新时直接更新缓存数据,并建立异步任务去更新数据库。这种异步方式使得请求响应速度快,系统的吞吐量也会显著提升。然而,由于是异步更新数据库,数据一致性的保障会相对较弱。如果更新数据库失败,就会永远导致系统产生脏数据。因此,需要精心设计系统的重试策略。此外,如果异步服务出现故障,还需要考虑如何持久化更新的数据,以便在服务重启后能够快速恢复。



在更新数据库时,由于存在并发多任务,还需要考虑并发写是否会导致脏数据问题,因此需要追溯每次更新数据的时序。使用这种模式需要考虑的细节很多,设计出一套良好的方案并不容易。

注意要点

尽管后写缓存模式可以提升系统性能,但需要注意以下几点。

首先,由于数据一致性的风险,需要谨慎权衡是否可以容忍脏数据的出现。其次,需要考虑并发写引发的脏数据问题,可能需要采取适当的并发控制措施。最后,需要设计合理的重试策略和数据持久化方案,以应对异步更新和服务故障导致的问题。

后写缓存模式可以提高系统性能,但在数据一致性、并发写控制、重试策略和数据持久化等方面需要投入较大的设计和实现工作。在实际应用中,需要根据具体情况权衡利弊,并综合考虑系统需求、资源情况和可靠性要求等因素,选择合适的缓存模式。

总结分析

最新的数据应该放置在数据库中。

缓存的目的是为了提升系统性能,通过利用内存的高速读取来提高系统吞吐量,并减轻数据库的压力。缓存的存在可以使得部分读请求无需到达数据库层,从而提高响应速度。然而,这也带来了一个问题,即数据存在于缓存和数据库这两个位置,所以在数据更新时需要考虑将“正确的数据放置在哪个最可信的存储介质上”,这需要结合业务性质在两个数据存储介质中进行选择。

在缓存除模式中,可以选择先更新数据库,然后使缓存失效(Cache Aside Pattern),这样可以确保最新和最准确的数据一定会存储在数据库中。这种方式可以保证数据库中的核心业务数据是可信的,但会导致更新逻辑更复杂,系统处理更新的耗时更长。对于非核心数据的更新,可以选择后写缓存模式(Write Behind Cache Pattern),只需更新缓存即可,以实现快速响应。然而,这种方式容易导致数据不一致,即数据库中的数据不一定是最可信的数据。

因此,不同的更新策略实际上是权衡最新数据放置的位置和系统性能的一种平衡。需要根据业务场景做出折衷选择。在核心业务数据的更新中,优先选择将最新数据放置在数据库中,以确保数据的可信性。而对于非核心数据的更新,可以考虑使用缓存来提高系统性能,但需要注意数据一致性的问题。

相关文章
|
11天前
|
NoSQL Java Redis
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
42 10
|
1月前
|
存储 Dubbo Java
分布式 RPC 底层原理详解,看这篇就够了!
本文详解分布式RPC的底层原理与系统设计,大厂面试高频,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
分布式 RPC 底层原理详解,看这篇就够了!
|
25天前
|
存储 缓存 监控
后端开发中的缓存机制:深度解析与最佳实践####
本文深入探讨了后端开发中不可或缺的一环——缓存机制,旨在为读者提供一份详尽的指南,涵盖缓存的基本原理、常见类型(如内存缓存、磁盘缓存、分布式缓存等)、主流技术选型(Redis、Memcached、Ehcache等),以及在实际项目中如何根据业务需求设计并实施高效的缓存策略。不同于常规摘要的概述性质,本摘要直接点明文章将围绕“深度解析”与“最佳实践”两大核心展开,既适合初学者构建基础认知框架,也为有经验的开发者提供优化建议与实战技巧。 ####
|
20天前
|
调度 数据库
什么场景下要使用分布式锁
分布式锁用于确保多节点环境下的资源互斥访问、避免重复操作、控制并发流量、防止竞态条件及任务调度协调,常见于防止超卖等问题。
33 4
|
21天前
|
机器学习/深度学习 存储 运维
分布式机器学习系统:设计原理、优化策略与实践经验
本文详细探讨了分布式机器学习系统的发展现状与挑战,重点分析了数据并行、模型并行等核心训练范式,以及参数服务器、优化器等关键组件的设计与实现。文章还深入讨论了混合精度训练、梯度累积、ZeRO优化器等高级特性,旨在提供一套全面的技术解决方案,以应对超大规模模型训练中的计算、存储及通信挑战。
54 4
|
24天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
53 8
|
2月前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
81 15
Android 系统缓存扫描与清理方法分析
|
26天前
|
缓存 NoSQL 数据库
缓存穿透、缓存击穿和缓存雪崩及其解决方案
在现代应用中,缓存是提升性能的关键技术之一。然而,缓存系统也可能遇到一系列问题,如缓存穿透、缓存击穿和缓存雪崩。这些问题可能导致数据库压力过大,甚至系统崩溃。本文将探讨这些问题及其解决方案。
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
41 5
|
1月前
|
缓存 监控 NoSQL
Redis 缓存穿透的检测方法与分析
【10月更文挑战第23天】通过以上对 Redis 缓存穿透检测方法的深入探讨,我们对如何及时发现和处理这一问题有了更全面的认识。在实际应用中,我们需要综合运用多种检测手段,并结合业务场景和实际情况进行分析,以确保能够准确、及时地检测到缓存穿透现象,并采取有效的措施加以解决。同时,要不断优化和改进检测方法,提高检测的准确性和效率,为系统的稳定运行提供有力保障。
56 5