缓存雪崩/穿透/击穿/失效原理图/14种缓存数据特征+10种数据一致性方案

前言

在数据驱动的互联网时代,缓存成为了提升应用性能的关键技术。面对海量用户请求,如何通过缓存策略有效减轻数据库压力、降低响应延迟?本文深入探讨了缓存雪崩、缓存穿透、缓存击穿等常见问题,并提供了全面的解决方案。通过实际案例分析,揭示了缓存设计的最佳实践,确保数据一致性的同时,最大化系统吞吐量。让我们一起探索缓存的奥秘,为构建高效、稳定的系统架构打下坚实基础。

1、缓存应用设计

flowchart TB
  客户端 -->|发送请求|应用服务器
  应用服务器 -->|检查缓存| 本地缓存
  分布式缓存 -->|未命中| 数据库
  应用服务器 -->|更新数据| 数据库
  本地缓存 -->|未命中| 分布式缓存
  本地缓存 -->|命中| 返回数据
  分布式缓存 -->|命中|返回数据
  数据库 --> 更新缓存
  更新缓存 --> 返回数据

图说明

  • 客户端:用户的请求起点。
  • 应用服务器:处理业务逻辑,决定是否访问缓存或数据库。
  • 本地缓存:应用服务器上的缓存,用于存储频繁访问的数据,减少对分布式缓存或数据库的访问。
  • 分布式缓存:跨多个应用服务器共享的缓存,用于提供更高的扩展性和可用性。
  • 数据库:数据的持久存储层,当缓存未命中时,从这里获取数据。
  • 更新缓存:在数据更新时,同步更新缓存以保持数据一致性。

2、缓存特性

  • 快速访问:缓存通常存储在内存中,访问速度远快于磁盘或数据库,这可以显著提高数据检索的速度。
  • 减轻数据库负载:通过存储频繁访问的数据,缓存可以减少对数据库的直接查询,从而降低数据库的负载和压力。
  • 提高性能:缓存可以减少系统的响应时间,提高整体性能,特别是在高流量的情况下。
  • 数据局部性原理:缓存利用了计算机科学中的局部性原理,即最近访问的数据项在未来也可能被访问。
  • 可扩展性:通过增加缓存层,系统可以更容易地扩展以应对更多的用户和请求。
  • 容错性:缓存可以作为数据源的一层保护,即使数据源暂时不可用,缓存中的数据仍然可以被访问。
  • 数据一致性:虽然缓存可以提高性能,但它也带来了数据一致性的挑战。需要策略来确保缓存数据与原始数据源保持同步。
  • 多种缓存策略:有多种缓存策略,如最近最少使用(LRU)、先进先出(FIFO)、时效性过期等,可以根据不同的应用场景选择最合适的策略。
  • 分布式缓存:对于大型系统,可以使用分布式缓存来跨多个服务器共享缓存数据,提供更高的可用性和可扩展性。

3、业务缓存数据特征

高读取频率
特征:缓存那些被频繁读取的数据,可以显著减少对后端数据库的访问压力。
业务案例:电商平台的商品详情页,如亚马逊或淘宝,用户频繁查看商品信息,这些信息变化不频繁,因此可以被缓存以减少数据库访问。

相对静态
特征:不经常改变的数据,如产品信息、用户配置文件等,适合缓存,因为它们不需要频繁更新。
业务案例:在线教育平台的课程介绍,如Coursera或Udemy,课程的描述和大纲在发布后很少改变,可以缓存这些数据以加快访问速度。

不涉及复杂事务
特征:对于那些不涉及复杂事务处理的数据,缓存可以提供快速读取,而不用担心一致性问题。
业务案例:网站的静态内容,如HTML页面、CSS样式表和JavaScript文件,它们不涉及后端逻辑,适合使用CDN缓存。

可预测性
特征:可预测的数据访问模式,如特定时间段内的热点数据,可以通过缓存提前准备。
业务案例:新闻网站在重大新闻事件发布时,可以预测到某些新闻页面的访问量会激增,提前将这些页面缓存起来。

大数据量
特征:大量数据的聚合结果或计算密集型操作的结果,缓存这些数据可以减少重复计算。
业务案例:金融交易分析平台,如股票交易数据的实时分析结果,这些结果涉及大量数据计算,缓存这些聚合结果可以提高响应速度。

热点数据
特征:某些数据项可能比其他数据更受欢迎,成为系统中的热点,这些数据非常适合缓存。
业务案例:社交媒体平台,如Twitter或Facebook,某些热门话题或趋势的页面访问量巨大,缓存这些热点数据可以减轻服务器压力。

读取成本高
特征:从数据库或其他存储系统中读取成本较高的数据,缓存可以减少这种成本。
业务案例:在线视频平台,如Netflix或YouTube,视频元数据的读取成本较高,缓存这些数据可以减少数据库的压力。

写入后读取
特征:数据写入后通常会被立即读取,这种模式适合缓存,因为可以减少对原始数据源的访问。
业务案例:在线购物车,用户添加商品到购物车后,通常会立即查看购物车,缓存购物车数据可以加快访问速度。

数据一致性要求不高

特征:对于那些可以容忍短时间数据不一致的业务场景,缓存可以提供性能上的优化。
业务案例:内容管理系统,如WordPress,文章的草稿版本可以缓存,因为它们不需要实时一致性。

数据预热
特征:对于需要在系统启动或特定事件后立即可用的数据,缓存可以用于数据预热。
业务案例:电子商务网站在大型促销活动(如618或双十一)前,可以预先加载和缓存促销商品的信息。

过期数据可接受
特征:如果数据过期后可以重新生成或从数据源重新加载,那么这些数据适合缓存。
业务案例:实时公交或地铁时刻表,虽然时刻表会定期更新,但短时间内的过期数据对于用户来说通常是可以接受的。

数据粒度
特征:细粒度的数据(如单个对象)和粗粒度的数据(如数据集合)都可以缓存,但需要根据访问模式和更新频率来决定。
业务案例:用户个人资料,如GitHub或微信,用户的个人资料页面可以缓存,因为它包含了用户信息的细粒度数据。

可序列化
特征:可以轻松序列化和反序列化的数据,因为缓存通常需要将数据在网络中传输或在进程间共享。
业务案例:RESTful API服务,如微服务的API,返回的JSON格式数据可以轻松序列化和反序列化,适合缓存。

独立性
特征:那些不依赖于其他数据项的数据,因为缓存数据时,需要考虑数据之间的依赖关系。
业务案例:天气预报服务,每个地区的天气信息相对独立,可以单独缓存,而不依赖于其他地区的天气数据。

4、缓存数据一致性方案

  1. 缓存失效(Cache Expiration)
  • 方案:为缓存数据设置过期时间,过期后缓存自动失效,下一次访问时从数据库加载最新数据。
  • 一致性:最终一致性,不能保证实时一致性,但可以减少数据不一致的时间窗口。
  • 适用场景:适用于数据更新频率不高,对数据实时性要求不高的场景。
  • 优点:简单易实现,减轻数据库压力。
flowchart TB
  开始 --> 请求数据
  请求数据 --> B{缓存命中?}
  B-->|是|返回缓存数据
  B-->|否|从数据库加载-->更新缓存 --> 返回缓存数据-->结束
  • 缺点:存在数据不一致的窗口期,对实时性敏感的业务不适用。
  1. 写入时清除缓存(Write Through Cache)
  • 方案:数据更新时,同时更新缓存和数据库,确保两者数据一致。
  • 一致性:强一致性,但会增加写操作的延迟。
  • 适用场景:适用于对数据一致性要求极高的场景,如金融交易。
  • 优点:保证了数据的强一致性。
flowchart TB
  开始 --> 更新数据请求 --> 更新数据库 --> A{成功?} -->|是|更新缓存-->结束
  A-->|否|回滚更新--> 结束
  • 缺点:写操作性能受影响,增加了系统的复杂性。
  1. 延迟写入(Write Behind Cache)
  • 方案:数据更新时,先更新缓存,异步批量写入数据库。
  • 一致性:短暂不一致,存在数据丢失的风险,但可以提高写操作的性能。
  • 适用场景:适用于写操作频繁且对数据实时性要求不高的场景。
  • 优点:提高了写操作的性能,减轻了数据库的压力。
flowchart TB
  开始 --> 更新数据请求 --> 更新缓存 --> 异步入数据库 --> 结束
 
  • 缺点:数据可能不一致,存在数据丢失的风险。
  1. 消息队列(Message Queue)
  • 方案:数据更新时,通过消息队列异步更新缓存,确保数据库和缓存的更新操作解耦。
  • 一致性:最终一致性,通过消息队列的确认机制来保证数据的最终同步。
  • 适用场景:适用于分布式系统,需要异步处理数据更新的场景。
  • 优点:提高了系统的伸缩性和响应速度,降低了直接操作数据库的压力。
flowchart TB
  开始 --> 更新数据请求 --> 更新数据库 --> 发送更新消息到队列 --> 消费者更新缓存 --> 结束
 
  • 缺点:增加了系统的复杂性,需要处理消息丢失和重复的问题
  1. 发布订阅模式(Pub/Sub)
  • 方案:数据库更新时,发布变更事件,缓存订阅并响应这些事件来更新数据。
  • 一致性:最终一致性,依赖于事件的传播速度和订阅者的响应时间。
  • 适用场景:适用于分布式系统中,需要实时更新多个服务或组件的场景。
  • 优点:实现了数据的实时更新,提高了系统的响应速度。
flowchart TB
  开始 --> 数据库更新 --> 发布更新事件 --> 缓存订阅者 --> 更新缓存 --> 结束
 
  • 缺点:需要额外的发布订阅系统支持,增加了系统复杂性。
  1. 版本号或CAS(乐观锁)
  • 方案:缓存和数据库中的数据都有版本号或CAS值,更新数据时检查版本号或CAS值是否一致。
  • 一致性:强一致性,适用于高并发场景,但增加了操作的复杂性。
  • 适用场景:适用于高并发且需要严格一致性的业务场景。
  • 优点:保证了数据的强一致性,适用于并发更新频繁的环境。
flowchart TB
  开始 --> 读取数据 --> 获取版本号 --> 更新请求 --> A{版本号匹配?} -->|是| 更新数据库 --> 更新缓存 --> 结束
  A -->|否|重试或失败
  • 缺点:增加了操作的复杂性,可能会影响性能。
  1. 数据库事务(Transactional Cache)
  • 方案:将缓存操作纳入数据库事务中,确保缓存和数据库的一致性。
  • 一致性:强一致性,但可能会牺牲一些性能。
  • 适用场景:适用于需要严格事务控制的场景,如金融、会计系统。
  • 优点:保证了数据的强一致性。
flowchart TB
  开始 --> 开始数据库事务 --> 更新数据库 --> 更新缓存 --> A{事务成功?} -->|是| 提交事务 --> 结束
  A -->|否|回滚 --> 结束
  • 缺点:可能会降低性能,增加了系统的复杂性。
  1. 双写一致性(Double Write)
  • 方案:数据更新时,先写入数据库,再写入一个临时的队列,然后从队列中更新缓存。
  • 一致性:最终一致性,通过队列确保数据最终被写入缓存。
  • 适用场景:适用于对数据一致性要求高,且需要异步处理的场景。
  • 优点:通过队列机制确保了数据的最终一致性。
flowchart TB
  开始 -->  更新数据库 --> 写入更新队列 --> 队列更新缓存--> 结束
  • 缺点:增加了系统的复杂性,可能会引入延迟。
  1. 缓存穿透保护(Cache Penetration Protection)
  • 方案:使用布隆过滤器等机制,防止对数据库的无效查询,同时缓存空结果。
  • 一致性:最终一致性,通过缓存空结果减少对数据库的访问。
  • 适用场景:适用于需要防止大量无效请求穿透到数据库的场景。
  • 优点:减少了对数据库的无效访问,保护了数据库。
flowchart TB
  开始 -->  请求数据 --> A{缓存或布隆过滤器命中?}-->|是|返回数据 --> 结束
  A-->|否|更新数据库-->更新缓存-->返回数据
  
  • 缺点:增加了内存消耗,需要维护布隆过滤器。
  1. 热点数据保护(Hot Data Protection)
  • 方案:对频繁更新的热点数据使用锁或其他同步机制,防止并发写入导致的数据不一致。
  • 一致性:强一致性,但可能会降低热点数据的并发性能。
  • 适用场景:适用于热点数据更新频繁,且对数据一致性要求高的场景。
  • 优点:保证了热点数据的一致性。
  • 缺点:可能会降低热点数据的并发性能,增加了锁的开销。
flowchart TB
  开始 -->  更新热点数据请求 --> 获取锁 -->更新数据库--> 更新缓存--> 释放锁--> 结束

5、缓存失效场景

5.1 缓存雪崩

想象一下,你正在网上愉快地购物,突然间,大量的用户同时涌入,他们都想快点看到商品信息。这时,如果所有的用户请求都直接打到数据库上,数据库可能就会像被雪崩一样压垮。缓存雪崩就是指在高流量下,缓存数据同时失效,导致大量请求直接访问数据库,造成数据库压力过大。

flowchart TB
  开始 -->  缓存服务运行中 -->A{缓存数据到期} -->|同时到期|大量缓存数据失效-->请求直接访问数据库-->B{数据库压力增大}-->|超过承受能力|数据库宕机-->系统不可用
  缓存服务运行中 -->C{缓存服务不可用} --> 大量缓存数据失效
  A-->|逐个到期|缓存数据逐个失效
  C-->缓存数据无法更新
  缓存数据逐个失效-->请求直接访问数据库
  缓存数据无法更新-->请求直接访问数据库
  B-->|未超过承载能力|数据库响应慢 --> 系统不可用

解决方案:

  • 设置不同的过期时间:让缓存数据的过期时间错开,避免同时失效。
  • 使用高可用架构:比如Redis集群,即使一个节点挂了,其他节点还能继续工作。

5.2 缓存穿透

缓存穿透是指一些不怀好意的用户,他们故意请求数据库中不存在的数据,比如查询一个不存在的用户ID。这样,缓存中不会有这些数据,每次请求都要去数据库查询,这就像有人拿着一把剑,一次次地刺穿你的防御。

flowchart TB
  开始 -->  用户发起请求 -->A{命中缓存?} --> |否| B{数据库命中?}-->|否|返回空结果-->C{是否缓存空结果}-->|是|写入空结果到缓存-->结束
  A-->|是|返回缓存数据--> 结束
  B-->|是|返回数据库数据并写入缓存--> 结束
  C-->|否|直接返回--> 结束

解决方案:

  • 布隆过滤器:在查询前先判断数据是否存在,不存在就直接返回,避免无用的查询。
  • 缓存空结果:将查询不到的数据也缓存起来,这样同样的请求就不会再次穿透到数据库了。

5.3 缓存击穿

缓存击穿和缓存穿透有点像,但它是指一个非常热门的数据在缓存中过期的那一瞬间,大量的请求同时到达,直接压垮了数据库。这就像是一群粉丝突然冲向一个刚下飞机的明星,场面一度失控。

flowchart TB
  开始 -->  请求热门数据-->A{命中缓存?} -->|否|请求数据库-->B{数据库响应}-->|成功|更新缓存并返回数据 --> 结束
  A -->|是|返回缓存数据 --> 结束
  B --> |失败|返回错误--> 结束

解决方案:

  • 设置热点数据不过期:对于非常热门的数据,可以不设置过期时间。
  • 使用互斥锁:在查询数据库前加锁,确保同一时间只有一个请求能查询数据库并回填缓存。

5.4 缓存失效

缓存失效是指缓存和数据库中的数据不一致。比如,你刚更新了数据库中的数据,但缓存中的数据还是旧的,这就导致了数据不一致的问题。

flowchart TB
  开始 -->  请求热门数据-->A{命中缓存?} -->|是|返回缓存数据
  A-->请求数据库 --> B{是否命中数据库?}-->|是|加载数据到缓存-->返回缓存数据
  B-->|否|处理数据库未命中请求-->返回缓存数据

解决方案:

  • 主动更新缓存:在更新数据库的同时,也更新缓存中的数据。
  • 使用消息队列:通过消息队列来监听数据变更,然后异步更新缓存。

6、热点数据重建

在缓存失效后,如何快速重建缓存,尤其是对于热点数据,这是一个问题。如果重建过程太慢,可能会导致缓存击穿。
解决方案:

  • 异步加载:在后台异步地重建缓存数据。
  • 分层缓存:使用多级缓存策略,比如内存缓存和磁盘缓存,这样可以在内存缓存失效时,先从磁盘缓存中读取数据。
关于我
loading