使用缓存目的

使用缓存的目的是通过提高应用性能、降低数据库负载、减少网络延迟、提高可扩展性和降低成本等方式来改善系统的运行效率和用户体验。缓存是一种有效的技术手段,可以在很大程度上优化应用程序的性能和可用性。

常用的缓存方案:cache-aside-pattern

Cache-Aside 模式(也称为 Lazy-Loading 模式)是一种常见的缓存设计模式,用于在分布式系统中管理缓存数据。它的核心思想是在缓存中不会自动更新数据,而是在需要时从数据库或其他数据源中加载数据,并将加载的数据放入缓存中。

Cache-Aside 模式的基本流程:

读操作:

  1. 当系统需要读取数据时,首先检查缓存是否存在所需数据。
  2. 如果缓存中存在数据,则直接从缓存中获取并返回。
  3. 如果缓存中不存在数据,则从数据源(如数据库)中获取数据,并将数据放入缓存中。
  4. 返回获取到的数据给应用程序使用。

写操作:

对于写操作,究竟是先删除(更新)缓存,再更新数据库,还是先更新数据库,再删除(更新)缓存呢?

这里有好几种解决方案

  1. 设置过期时间: 这种方案适用于对数据一致性要求较低或者写请求很少的业务,当读请求没有命中缓存时,就从数据库中读,之后回写到缓存里,同时设置一个过期时间。 写请求直接更改数据库,不用操作缓存。因此当一个key没过期时,写请求更改了数据库,之后的读还是读取到旧数据。这个时候确实发生了不一致,但业务并不敏感。

  2. 更新数据库,再更新缓存: 写多读少时,频繁更新缓存会降低性能 并发情况下可能存在将脏数据写回缓存的风险

  3. 先更新缓存,再更新数据库: 写多读少时,频繁更新缓存会降低性能 并发情况下可能存在将脏数据写回缓存的风险

  4. 先更新数据库,再删除缓存: 并发情况下可能存在将脏数据写回缓存的风险,不过相比于上面方案,出现概率极低

  5. 先删除缓存,再更新数据库: 并发情况下可能存在将脏数据写回缓存的风险

  6. 延时双删: 更新前删除缓存,更新后再次删除缓存。此方案的难点在于,sleep的时间该怎么去确定。如果偏大,同步删除的话会造成吞吐量的降低与查脏。如果偏小,则有可能第二次删除在刷脏之前发生,起不到“双删”的作用。 因此,我们需要结合业务对sleep的时间做出评估。一般来说,sleep的时间应该稍大于读请求查询数据与回写缓存的时间。

  7. 消息队列: 先更新数据库,接着将删除缓存的消息投递到mq中。自身拿到消息后,尝试进行删除缓存。如果失败,则不断进行重试,这里引入了消息队列,系统的复杂性提升,可用性降低。

  8. 消息队列+订阅binlog: 业务代码只操作数据库,不操作缓存。同时启动一个订阅binlog的程序去监听删除操作,然后投递到消息队列中。再启动一个消费者,根据消息去删除缓存。系统的复杂性提升,可用性降低。

Cache-Aside 模式的优点包括

  1. 简单易实现:不需要复杂的缓存同步逻辑,读写操作分离,易于理解和实现。
  2. 缓存命中率高:只有在需要时才会将数据加载到缓存中,避免了不必要的缓存操作,提高了缓存命中率。

Cache-Aside 模式的一些缺点:

  1. 首次读取数据时的冷启动问题:如果数据不在缓存中,需要从数据源中加载数据,会增加读取数据的延迟。
  2. 缓存与数据源的数据不一致:由于缓存中的数据不会自动更新,可能会导致缓存中的数据与数据源不一致,需要额外的手段来保持数据一致性(如定期刷新缓存、通过消息通知更新缓存等)。

并发问题

上面很多方案都是并发会导致脏数据,也许有人会说通过分布式锁控制,防止多个线程修改数据,可是这样跟我们使用缓存的目的就背道而驰了。

如果使用CAP理论来看待这个由业务代码+数据库+缓存组成的分布式系统,首先该系统必须要能容忍网络分区,其次对于觉得部分的场景,该分布式系统应当也需要满足可用性。也就是说,缓存节点宕机后或出现网络闪断,整个系统应当还能够对外提供服务。根据CAP定理,该系统就无法满足强一致性。

以下内容总结的很好

从方案1到方案8,系统的复杂性逐步上升,但确实能解决一些痛点,例如同步删除性能差,第二次删除失败等等。

但是并不存在谁最好谁最差,应当结合业务来看,脱离业务谈技术就是一场空谈。

作为技术人员,我们应当根据业务场景选择相应的技术,但前提是对各种技术都有较深的理解,能分析其利弊。

我觉得技术人员的最好的归宿,就是能在不断解决问题的过程中,形成自己的方法论与解决方案。例如形成开源作品或技术博客,去影响别人

参考:https://blog.csdn.net/mnhpo_Q/article/details/136821214