架构思维:缓存层场景实战_读缓存(下)

打印 上一主题 下一主题

主题 1856|帖子 1856|积分 5572



Pre

Java避坑案例 - 高并发场景下的分布式缓存计谋
Redis - 缓存设计深度剖析:穿透、并发、雪崩与热点计谋
深入理解分布式技术 - 先更新数据库,还是先更新缓存
架构思维:读缓存 - 减少数据库读利用压力

业务场景

某系统商品详情页因叠加保举、成交记录、优惠活动等功能,导致每次访问需实验数十条SQL,平均响应时间从3.61秒恶化至15.53秒。初期思量本地缓存(如Guava),但测算发现5万商品数据需750GB内存(30节点×25GB),成本过高。最终采用分布式缓存方案,将数据集中存储(如Redis),所有服务节点共享同一缓存源,避免冗余存储,显著提拔访问速率至毫秒级,同时降低硬件成本。优化后,异步加载非核心数据(如成交记录)进一步减轻及时查询压力。
技术选型完成后,开始思量缓存的一些详细问题,先从缓存何时存储数据入手

缓存存储数据的时机与常见问题办理方案

1. 缓存读取与存储逻辑


  • 先查缓存:请求到达时,优先从缓存(如Redis)读取数据。
  • 缓存未掷中:若数据不存在或逾期,则查询数据库,并将结果写入缓存。
  • 返回数据:最终返回缓存中的数据给调用方。

2. 高并发下的缓存问题及办理方案

这种逻辑唯一麻烦的地方是,当用户发来大量的并发请求时,它们会发现缓存中没有数据,那么所有请求会同时挤在第2)步,此时如果这些请叱责部从数据库读取数据,就会让数据库崩溃。
数据库的崩溃可以分为3种情况。


  • 1)单一数据逾期或者不存在,这种情况称为缓存击穿
           办理方案:第一个线程如果发现Key不存在,就先给Key加锁,再从数据库读取数据生存到缓存中,最后释放锁。如果其他线程正在读取同一个Key值,那么必须比及锁释放后才行。关于锁的问题前面已经讲过,此处不再赘述。
  • 2)数据大面积逾期或者Redis宕机,这种情况称为缓存雪崩
           办理方案:设置缓存的逾期时间为随机分布或设置永不逾期即可。
  • 3)一个恶意请求获取的Key不在数据库中,这种情况称为缓存穿透
           好比正常的商品ID是从100000到1000000(10万到100万之间的数值),那么恶意请求就大概会故意请求2000000以上的数据。这种情况如果不做处理处罚,恶意请求每次进来时,肯定会发现缓存中没有值,那么每次都会查询数据库,虽然最终也没在数据库中找到商品,但是无疑给数据库增加了负担。这里给出两种办理办法。
    ①在业务逻辑中直接校验,在数据库不被访问的前提下过滤掉不存在的Key。
    ②针对恶意请求的Key存放一个空值在缓存中,防止恶意请求骚扰数据库。
故,总结如下:


  • 缓存击穿(单一Key失效)

    • 问题:热点Key失效时,大量请求同时穿透到数据库。
    • 办理:加锁(如Redis分布式锁),仅允许一个线程查询DB并回填缓存,其余线程等候。

  • 缓存雪崩(大量Key同时失效或Redis宕机)

    • 问题:缓存大面积失效,导致数据库被压垮。
    • 办理

      • 设置随机逾期时间(如30分钟±5分钟)。
      • 缓存永不逾期,后台异步更新(如定时任务)。


  • 缓存穿透(恶意查询不存在的数据)

    • 问题:恶意请求查询不存在的Key,导致频仍访问DB。
    • 办理

      • 业务层校验:如商品ID范围查抄(100000~1000000)。
      • 缓存空值:对不存在的Key存储null或占位符,并设置较短逾期时间。



3. 缓存预热(减少冷启动问题)

上面这些逻辑都是在确保查询数据的请求已经过来后如何适本地处理处罚,如果缓存数据找不到,再去数据库查询,最终是要占用服务器额外资源的。那么最理想的就是在用户请求过来之前把数据都缓存到Redis中。这就是缓存预热。
其详细做法就是在深夜无人访问或访问量小的时候,将预热的数据生存到缓存中,这样流量大的时候,用户查询就无须再从数据库读取数据了,将大大减小数据读取压力。
故,总结如下:


  • 时机:在低峰期(如凌晨)提前加载热点数据到缓存。
  • 方式

    • 定时任务扫描DB并写入缓存。
    • 启动时主动加载核心数据。


缓存更新计谋(双写问题)

关于缓存何时存数据的问题就讨论完了,接下来开始讨论更新缓存的问题,这部分内容因涉及双写(缓存+数据库),以是会花费一些篇幅。


  • 先更新DB还是缓存?

    • Cache-Aside(旁路缓存):先更新DB,再删除缓存(保举)。
    • Write-Through(写穿透):先更新缓存,再同步到DB(一致性高,但性能较低)。
    • Write-Behind(写回):先更新缓存,异步刷回DB(高性能,但大概丢数据)。

在缓存更新时,我们必要思量 数据库与缓存的一致性,同时避免 并发问题性能瓶颈。以下是 5种常见的缓存更新计谋,分析它们的优缺点,并给出保举方案。

1. 先更新缓存,再更新数据库(不保举)

对于这个组合,会碰到这种情况:假设第二步更新数据库失败了,要求回滚缓存的更新,这时该怎么办呢?Redis不支持事故回滚,除非采用手工回滚的方式,先生存原有数据,然后再将缓存更新回原来的数据,这种办理方案有些缺陷。
这里简单举个例子。
1)原来缓存中的值是a,两个线程同时更新库存。
2)线程A将缓存中的值更新成b,且生存了原来的值a,然后更新数据库。
3)线程B将缓存中的值更新成c,且生存了原来的值b,然后更新数据库。
4)线程A更新数据库时失败了,它必须回滚,那如今缓存中的值更新成什么呢?理论上应该更新成c,因为数据库中的值是c,但是,线程A里面无从获得c这个值。
如果在线程A更新缓存与数据库的整个过程中,先把缓存及数据库都锁上,确保别的线程不能更新,是否可行?固然是可行的。但是其他线程能不能读取?
假设线程A更新数据库失败回滚缓存时,线程C也加入进来,它必要先读取缓存中的值,这时又返回什么值?
看到这个场景,是不是有点儿认识?不错,这就是典型的事故隔离级别场景。以是就不保举这个组合,因为此处只是必要使用一下缓存,而这个组合就要思量事故隔离级别的一些逻辑,成本太大。接着思量别的组合。
故, 总结如下:
流程

  • 更新缓存
  • 更新数据库
     问题


  • 事故回滚困难:如果数据库更新失败,缓存无法主动回滚(Redis不支持事故回滚)。
  • 并发问题

    • 线程A更新缓存为 b,线程B更新缓存为 c,最终缓存大概是 b 或 c,而数据库大概是另一个值。
    • 必要加锁,但会降低性能。

结论:❌ 不保举,容易导致数据不一致。

2. 先删除缓存,再更新数据库(不保举)

使用这种方案,即使更新数据库失败了也不必要回滚缓存。这种做法虽然奥妙规避了失败回滚的问题,却引出了两个更大的问题。
1)假设线程A先删除缓存,再更新数据库。在线程A完成更新数据库之前,后实验的线程B反而超前完成了利用,读取Key发现没有数据后,将数据库中的旧值存放到了缓存中。线程A在线程B都完成后再更新数据库,这样就会出现缓存(旧值)与数据库的值(新值)不一致的问题。
2)为了办理一致性问题,可以让线程A给Key加锁,因为写利用特别耗时,这种处理处罚方法会导致大量的读请求卡在锁中。
以上描述的是典型的高可用和一致性难以两全的问题,如果再加上分区容错就是CAP(一致性Consistency、可用性Availability、分区容错性Partition Tolerance)了,这里不睁开讨论,接下来继续讨论另外3种组合

故, 总结如下:
流程

  • 删除缓存
  • 更新数据库
     问题


  • 缓存与数据库不一致(旧数据问题):

    • 线程A删除缓存 → 线程B查询缓存未掷中 → 从DB读取旧数据并写入缓存 → 线程A更新DB,导致缓存是旧数据。

  • 高并发下缓存击穿:大量请求穿透到数据库。
办理方案


  • 加锁(如分布式锁),但会影响性能。
结论:❌ 不保举,容易导致缓存与数据库不一致。

3. 先更新数据库,再更新缓存(不保举)

对于组合3,同样必要思量两个问题。
1)假设第一步(更新数据库)乐成,第二步(更新缓存)失败了怎么办?
因为缓存不是主流程,数据库才是,以是不会因为更新缓存失败而回滚第一步对数据库的更新。此时一样平常采取的做法是重试机制,但重试机制如果存在延时还是会出现数据库与缓存不一致的情况,不利益理。
2)假设两个线程同时更新同一个数据,线程A先完成了第一步,线程B先完成了第二步怎么办?线程A把值更新成a,线程B把值更新成b,此时数据库中的最新值是b,因为线程A先完成了第一步,后完成第二步,以是缓存中的最新值是a,数据库与缓存的值还是不一致,这个逻辑还是有问题的。
因此,也不建议采用这个组合
故, 总结如下:
流程

  • 更新数据库
  • 更新缓存
     问题


  • 缓存更新失败:如果第二步失败,缓存还是旧数据。
  • 并发问题

    • 线程A更新DB为 a,线程B更新DB为 b → 线程B先更新缓存为 b,线程A后更新缓存为 a,导致缓存是 a,而DB是 b。

结论:❌ 不保举,仍大概不一致。

4. 先更新数据库,再删除缓存(Cache-Aside模式 保举⭐)

针对组合4,先看看它能不能办理组合3的第二个问题。
假设两个线程同时更新同一个数据,线程A先完成第一步,线程B先完成第二步怎么办?
线程A把值更新成a,线程B把值更新成b,此时数据库中的最新值是b,因为线程A先完成了第一步,以是第二步谁先完成已经不告急了,因为都是直接删除缓存数据。这个问题办理了。
那么,它能办理组合3的第一个问题吗?假设第一步乐成,第二步失败了怎么办?
这种情况的出现概率与组合3相比明显低不少,因为删除比更新容易多了。虽然这个组合方案不完美,但出现一致性问题的概率较低。
故, 总结如下:
流程

  • 更新数据库
  • 删除缓存
     优点


  • 缓存删除失败概率低(删除比更新更简单)。
  • 并发问题较少

    • 即使线程A更新DB后未及时删除缓存,线程B读取旧数据,但下次查询会重新加载最新数据。

问题


  • 短暂不一致

    • 线程A更新DB后,未删除缓存前,线程B大概读到旧数据。

  • 缓存击穿:删除缓存后,大量请求穿透到DB。
办理方案


  • 延长双删(组合5)可进一步降低不一致概率。
  • 缓存预热减少击穿影响。
结论:✅ 保举,相比前3种方案更可靠。

5. 延长双删(先删缓存→更新DB→再删缓存)(最佳实践⭐)

除了组合3会碰到的问题,组合4还会碰到别的问题吗?
是的。假设线程A要更新数据,先完成第一步更新数据库,在线程A删除缓存之前,线程B要访问缓存,那么取得的就是旧数据。这是一个小小的缺陷。
那么,以上问题有办法办理吗?
另有一个方案,就是先删除缓存,再更新数据库,再删除缓存。这个方案其实和先更新数据库,再删除缓存差不多,因为还是会出现类似的问题:假设线程A要更新数据库,先删除了缓存,这一瞬间线程C要读缓存,先把数据迁移到缓存;然后线程A完成了更新数据库的利用,这一瞬间线程B也要访问缓存,此时它访问到的就是线程C放到缓存里面的旧数据。
不外组合5出现类似问题的概率更低,因为要刚好有3个线程配合才会出现问题(比先更新数据库,再删除缓存的方案多了一个必要配合的线程)。
但是相比于组合4,组合5规避了第二步删除缓存失败的问题——组合5是先删除缓存,再更新数据库,假设它的第三步“再删除缓存”失败了,也不要紧,因为缓存已经删除了。
其实没有一个组合是完美的,它们都有读到脏数据(这里指旧数据)的大概性,只不外概率差别。根据以上分析,组合5相对来说是比力好的选择。
不外这个组合也有一些问题要思量,详细如下。


  • 1)删除缓存数据后变相出现缓存击穿,此时该怎么办?此问题在前面已经给出了方案。
  • 2)删除缓存失败如何重试?这个重试可以做得复杂一点,也可以做得简单一点。简单一点就是使用try…catch…,假设删除缓存失败了,在catch里面重试一次即可;复杂一点就是使用一个异步线程不绝重试,甚至用到MQ。不外这里没有必要大动干戈。而且异步重试的延时大,会带来更多的读脏数据的大概性。以是仅仅同步重试一次就可以了。
  • 3)不可避免的脏数据问题。虽然这个问题在组合5中出现的概率已经大大降低了,但是还是有的。关于这一点就必要与业务沟通,究竟这种情况比力少见,可以根据现实业务情况判断是否必要办理这个瑕疵。
   任何一个方案都不是完美的,但如果剩下1%的问题必要花好几倍的代价去办理,从技术上来讲得不偿失,这就要求架构师去说服业务方,去平衡技术的成本和收益。
  故, 总结如下:
流程

  • 删除缓存
  • 更新数据库
  • 延长几百毫秒后,再次删除缓存
     优点


  • 减少不一致窗口:第二次删除能清理大概的脏数据。
  • 避免缓存更新失败问题:即使第二次删除失败,缓存已被第一次删除,不会恒久存储旧数据。
问题


  • 短暂不一致仍存在(但概率更低)。
  • 实现稍复杂:必要引入延长任务(如MQ、定时任务)。
适用场景


  • 对一致性要求较高的电商、金融等业务。
结论:✅ 最佳实践,比方案4更可靠。 延长双删通过两次删除利用创建安全窗口,在工程实践上实现了性能与一致性的最佳平衡,是分布式系统缓存更新的首选方案 .

总结:如何选择缓存更新计谋

方案描述一致性保举度1️⃣ 先更新缓存,再更新DB易回滚问题❌ 差❌ 不保举2️⃣ 先删缓存,再更新DB旧数据问题❌ 差❌ 不保举3️⃣ 先更新DB,再更新缓存并发问题⚠️ 一样平常❌ 不保举4️⃣ 先更新DB,再删缓存较可靠✅ 较好⭐ 保举5️⃣ 延长双删最可靠✅ 最佳⭐⭐⭐ 最佳
最终建议



  • 平凡业务:使用 方案4(先更新DB,再删缓存),简单可靠。
  • 高一致性业务(如支付、库存):使用 方案5(延长双删),减少不一致窗口。
  • 补充优化

    • 缓存预热减少击穿影响。
    • 异步重试(如MQ)处理处罚删除失败情况。
    • 加锁(如Redis分布式锁)防止并发问题。

没有完美方案,但 方案4和5 在大多数场景下能平衡 性能与一致性

缓存高可用设计核心要点与监控方案


1、缓存高可用设计的5大核心要点


  • 负载均衡(读扩展)

    • 目的:通过多节点分摊读请求压力。
    • 方案

      • 使用署理层(如Twemproxy、Redis Cluster)主动分配请求。
      • 客户端分片(如一致性哈希)直接路由请求。


  • 数据分片(写扩展)

    • 目的:分散数据存储与写压力。
    • 方案

      • Redis Cluster:主动分片(16384个槽),支持动态扩缩容。
      • Codis:Proxy层分片,兼容原生Redis协议。


  • 数据冗余(容灾)

    • 目的:单节点故障时数据不丢失。
    • 方案

      • 主从复制(Replication):主节点写,从节点读+备份。
      • 多副本存储:如Redis Cluster的每个分片包罗主从节点。


  • 故障主动转移(Failover)

    • 目的:节点宕机时主动切换,保障服务可用。
    • 方案

      • 哨兵模式(Sentinel):监控主节点,主动选举新主。
      • Redis Cluster内置Failover:主节点宕机时,从节点主动升级。


  • 一致性包管

    • 目的:数据分片、故障规复时避免脏数据。
    • 寻衅:Redis默认异步复制,大概丢失少量数据。
    • 办理方案

      • WAIT命令:同步复制(捐躯性能)。
      • 业务层补偿:如定时校对DB与缓存。


保举架构


  • 中小规模:Redis Sentinel + 主从复制。
  • 大规模:Redis Cluster(内置分片、Failover)。

2、缓存监控关键指标与工具


  • 核心监控指标

    • 掷中率:keyspace_hits / (keyspace_hits + keyspace_misses),低于80%需优化缓存计谋。
    • 内存使用:used_memory,避免高出最大内存(maxmemory)触发淘汰。
    • 慢查询:slowlog分析耗时命令(如KEYS *、大Value利用)。
    • 延长:redis-cli --latency,高出1ms需排查网络或壅闭命令。
    • 毗连数:connected_clients,防止毗连走漏。

  • 开源监控工具

    • RedisLive:及时仪表盘展示关键指标。
    • Prometheus + Grafana:自界说报警与可视化。
    • Redis-exporter:为Prometheus提供Redis指标。

  • 自研监控建议

    • 定时巡检脚本:查抄INFO命令输出的关键指标。
    • 主动化报警:如内存使用率超90%、掷中率低于70%时触发告警。


3、总结



  • 高可用核心:分片扩展写、冗余保障读、主动Failover。
  • 监控关键点:掷中率、内存、慢查询、延长。
  • 工具选型

    • 快速上手:RedisLive。
    • 恒久运维:Prometheus+Grafana。

最终建议:根据业务规模选择Redis Sentinel或Cluster,并配套监控诉警体系,确保缓存稳固支撑业务高峰。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

东湖之滨

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表