缓存这「加速神器」从入门到填坑,看完再也不被产品怼慢

上周刚上线的用户中心接口,第二天产品就举着监控截图冲过来:"这接口咋比我早上的地铁还堵?用户说点个按钮等 3 秒,再慢就要卸载了!"

排查半天发现 ------ 数据库被按在地上反复摩擦:同一个用户信息,每秒被查几十次,数据库 CPU 直接飙到 90%。这时候我才拍大腿:"忘了加缓存!"

今天就带大家把缓存扒得明明白白:从 "啥是缓存" 到 "一致性坑怎么填",全是后端日常能用的干货,看完直接上手不踩雷!

1. 先搞懂:缓存到底是个啥?(不是 "存零食" 那种)

其实缓存的逻辑特简单,就像你工位上的 "零食抽屉":

  • 平时想吃零食(查数据),不用每次都跑楼下超市(数据库)------ 抽屉里有(缓存命中),直接拿,1 秒搞定;
  • 抽屉里没有(缓存未命中),再去超市买,回来顺便塞抽屉里(写缓存),下次吃就快了。

用技术话讲:缓存是 "暂存高频访问数据的临时存储器" ,速度比数据库快 N 倍(比如内存缓存比硬盘数据库快 100-1000 倍),核心作用就一个 ------ 帮数据库 "减负",让接口跑得飞起。

2. 缓存分两类:本地缓存 vs 分布式缓存(别用混了!)

后端写多了就知道,缓存不是 "一刀切",得看场景选 ------ 主要分 "自己用的" 和 "大家共用的" 两种。

2.1 本地缓存:单机 "小抽屉",快但有局限

本地缓存就是把数据存在当前服务器的内存里,比如 Java 里的Guava Cache、Caffeine,Python 的functools.lru_cache。

优点:快到离谱!

不用走网络,直接读内存,响应时间能压到毫秒甚至微秒级 ------ 比如你写个定时任务,把热门配置加载到本地缓存,接口查配置时根本不用碰数据库,爽歪歪。

缺点:分布式环境直接 "翻车"

就像你工位的零食抽屉,只有你能吃 ------ 要是服务部署了 3 台服务器(分布式),每台都有自己的本地缓存:

  • 服务器 A 更新了数据库里的用户余额,同步更了自己的缓存;
  • 但服务器 B、C 的缓存还是 "旧余额",用户访问 B 就看到旧数据,访问 A 就看到新数据,这不就 "精神分裂" 了?

所以本地缓存只适合:单机部署、数据不常变、不需要多机共享的场景(比如本地测试、简单工具类)。

2.2 分布式缓存:多机 "共享冰箱",统一又能打

分布式缓存是把数据存在独立的缓存服务器集群里(比如 Redis、Memcached),所有后端服务器都去这一个 "冰箱" 里拿数据 ------ 不管你访问哪台后端服务器,查的都是同一个缓存,数据肯定一致。

优点:分布式场景的 "刚需"

  • 多机共享:所有服务器都读同一个缓存,不会出现数据不一致;
  • 容量大:缓存服务器可以集群扩容,存个几十 G 数据没问题;
  • 高可用:Redis 搞个主从 + 哨兵,就算一台缓存机挂了,另一台马上顶上,不影响业务。

缺点:多走一步网络

毕竟要和缓存服务器通信(走 TCP/IP),比本地缓存慢一点 ------ 但也就多几毫秒,对比数据库的几十上百毫秒,还是快很多,绝大多数场景都能接受。

总结:怎么选?

场景 选哪种? 例子
单机部署、数据不变 本地缓存 Guava Cache 存配置
分布式部署、多机共享 分布式缓存 Redis 存用户会话、商品

3. 最头疼的坑:缓存一致性咋破?(别让缓存变 "脏")

用缓存爽归爽,但一更新数据就容易出问题 ------ 比如你改了数据库里的商品价格,缓存里还是旧价格,用户看到的就是 "假价格",这就是缓存一致性问题(缓存脏了)。

先看为啥会脏?常见场景:

  1. 先更数据库,再更缓存 ------ 要是更缓存失败,数据库是新的,缓存是旧的,脏了;
  1. 先删缓存,再更数据库 ------ 要是更数据库失败,下次查缓存没了,去查数据库(旧的),又把旧数据写回缓存,还是脏。

别慌!下面 3 个方案,从 "强一致" 到 "最终一致",覆盖不同业务需求。

3.1 双写加锁:追求 "绝对一致",牺牲点性能

核心思路:用锁保证 "更新数据库" 和 "更新缓存" 是一个 "原子操作"------ 同一时间只有一个线程能改,别人都得排队,不会乱。

代码逻辑(Java 示例):

php 复制代码
// 用key做锁,保证同一key的操作排队
synchronized (productId) {
    try {
        // 1. 先更数据库(成功才算第一步)
        productMapper.updatePrice(productId, newPrice);
        // 2. 再更缓存(数据库成功了,缓存才更)
        redisTemplate.opsForValue().set("product:price:" + productId, newPrice);
    } catch (Exception e) {
        // 失败了可以回滚缓存(比如删了),避免脏数据
        redisTemplate.delete("product:price:" + productId);
        throw new RuntimeException("更新失败");
    }
}

优点:强一致!

只要锁没毛病,数据库和缓存肯定同步,适合 "钱相关" 的场景(比如用户余额、订单金额)------ 总不能用户付了钱,缓存里还是没付的状态吧?

缺点:并发高了会 "堵"

锁会让并发请求排队,比如每秒 1000 个请求,都得等着一个一个处理,接口响应时间会变长 ------ 鱼和熊掌不可兼得,看业务能不能接受。

3.2 延迟双删:最终一致,性能不翻车

要是业务能接受 "短暂不一致"(比如商品详情页,1 秒内看到旧价格没事),延迟双删是性价比很高的方案。

核心思路:删两次缓存,中间隔一会儿,把 "旧数据写回缓存" 的坑堵上。

步骤拆解:

  1. 先删缓存:不管后面成不成,先把旧缓存清了,避免新请求读旧数据;
  1. 更新数据库:正常改数据库;
  1. 等一会儿(比如 1 秒):关键!等那些 "在更新数据库前就查缓存、没查到去查数据库" 的请求,把旧数据写回缓存之前,再删一次;
  1. 再删缓存:把可能被写回的旧缓存清掉,下次请求就会查新数据库,写新缓存。

代码逻辑(关键部分):

scss 复制代码
public void updateProductPrice(String productId, BigDecimal newPrice) {
    // 1. 第一次删缓存
    redisTemplate.delete("product:price:" + productId);
    
    // 2. 更新数据库
    productMapper.updatePrice(productId, newPrice);
    
    // 3. 等1秒(时间根据业务调,比如数据库读耗时+网络延迟)
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    
    // 4. 第二次删缓存
    redisTemplate.delete("product:price:" + productId);
}

优点:性能好,几乎不阻塞

不用加锁,请求来了直接处理,就等 1 秒(一般业务感知不到),适合大部分非金融场景(商品、文章、用户资料)。

缺点:短暂不一致

中间那 1 秒,可能有用户看到旧数据 ------ 但总比一直脏数据强,而且大部分场景用户根本没感觉。

3.3 异步双写:主流程飞快,缓存慢慢更

要是你想让 "更新接口" 跑得飞快,哪怕缓存慢个几十毫秒也没事,异步双写就很合适。

核心思路:更新数据库后,不用等缓存更新,直接返回;缓存更新扔给 "异步线程" 或者 "MQ" 去做,后台慢慢处理。

步骤拆解:

  1. 同步更新数据库(这步必须成功);
  1. 往 MQ(比如 RabbitMQ、Kafka)里扔一个 "更新缓存" 的消息;
  1. 接口直接返回 "更新成功";
  1. 后台起个消费者,拿 MQ 消息,异步更新缓存。

优点:主流程不阻塞

用户点 "更新",瞬间看到 "成功",体验好;缓存更新在后台慢慢跑,就算 MQ 排队,也只是晚一点同步,不影响用户。

缺点:一致性延迟可能更长

要是 MQ 堆积严重,缓存可能几分钟都没同步 ------ 适合 "对实时性要求不高" 的场景(比如用户改个昵称,就算缓存晚 1 分钟同步,也没啥大问题)。

4. 最后一招:缓存过期不能忘(兜底神器)

不管你用哪种一致性方案,都得给缓存加个 "过期时间"(比如 Redis 的expire)------ 这是最后一道兜底,防止缓存 "永久脏"。

为啥要加过期时间?

  1. 就算更新缓存失败,过期后缓存会自动失效,下次请求会查数据库,把新数据写回缓存,相当于 "自动修复";
  1. 避免缓存越存越多,占满内存 ------ 比如一些冷门数据,存一年都没人查,过期了就自动删掉,省内存。

注意点:

  • 过期时间别太短:比如设 10 秒,缓存频繁失效,会频繁查数据库("缓存穿透" 的前兆);
  • 过期时间别太长:比如设 24 小时,要是缓存脏了,得等 24 小时才修复,用户体验太差;
  • 建议:根据业务调,比如商品价格设 5 分钟,用户会话设 2 小时。

总结:缓存用法一句话搞定

  1. 单机用本地缓存(Guava),分布式用 Redis;
  1. 钱相关用双写加锁(强一致),一般业务用延迟双删(性价比高),用户体验优先用异步双写;
  1. 不管啥方案,都给缓存加个过期时间(兜底!)。

最后想问大家:你们平时用缓存踩过哪些坑?比如遇到过缓存穿透、雪崩吗?评论区聊聊,一起避坑涨经验!

相关推荐
zzywxc78712 分钟前
大模型落地实践指南:从技术路径到企业级解决方案
java·人工智能·python·microsoft·golang·prompt
相与还17 分钟前
IDEA+SpringBoot实现远程DEBUG到本机
java·spring boot·intellij-idea
小杨勇敢飞18 分钟前
IDEA 2024 中创建 Maven 项目的详细步骤
java·ide·intellij-idea
野犬寒鸦1 小时前
从零起步学习Redis || 第四章:Cache Aside Pattern(旁路缓存模式)以及优化策略
java·数据库·redis·后端·spring·缓存
白水先森1 小时前
C语言作用域与数组详解
java·数据结构·算法
茉莉玫瑰花茶1 小时前
Redis - Bitfield 类型
数据库·redis·缓存
草莓熊Lotso3 小时前
从 “Hello AI” 到企业级应用:Spring AI 如何重塑 Java 生态的 AI 开发
java·人工智能·经验分享·后端·spring
doulbQuestion3 小时前
【无标题】
java·spring
Metaphor6923 小时前
Java 旋转 PDF 页面:使用 Spire.PDF 实现高效页面处理
java·经验分享·pdf
哈利路亚胡辣汤4 小时前
spring多数据源配置
java·spring·mybatis