1、缓存基础:为什么需要缓存、缓存的工作原理、缓存带来的挑战

大家好,我是你们这节课的讲师。咱们直接进入正题——缓存。

说实话,我刚开始做后端那会儿,对缓存的理解特别肤浅。就觉得「哦,把数据存内存里,快一点嘛」。直到有一次线上事故,缓存雪崩把数据库打挂了,大半夜被叫起来修……嗯,从那以后我才真正开始认真研究缓存。

1.1 为什么需要缓存?

你想想看,一个典型的 Web 请求链路是什么样的?

用户请求 → 应用服务器 → 数据库 → 返回数据

数据库呢,通常是整个链路里最慢的一环。尤其是关系型数据库,每次查询都要走磁盘 I/O,还要解析 SQL、加锁、事务……慢是常态。

我个人习惯把数据库比作「仓库」,缓存比作「前台桌面」。你总不能每次拿支笔都跑一趟仓库吧?桌面上放几支常用的,随拿随用,多省事。

缓存的核心价值就两点:

  • 降低延迟:内存读取是微秒级,磁盘是毫秒级,差了 1000 倍
  • 减轻压力:少查数据库,数据库就能扛住更多并发

一个真实数据:我在项目中做过压测,同样的接口,走数据库 QPS 大概 2000,加上 Redis 缓存后直接飙到 5 万+。差距就是这么大。

1.2 缓存的工作原理

说白了,缓存就是一个「以空间换时间」的套路。

你把热点数据放在更快的存储介质里,下次请求直接返回,不用再走慢路径。

典型的缓存工作流程是这样的:

  1. 请求来了,先查缓存
  2. 缓存命中 → 直接返回,完事
  3. 缓存没命中 → 查数据库,把结果写回缓存,再返回

用代码表示就是:

def get_user(user_id):
    # 1. 先查缓存
    user = redis.get(f"user:{user_id}")
    if user:
        return user
    
    # 2. 缓存没命中,查数据库
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    # 3. 写回缓存,设置过期时间
    redis.set(f"user:{user_id}", user, ex=3600)
    
    return user

这里有个细节——过期时间。我见过不少新手直接把缓存设成永不过期,结果数据变了,缓存还是旧的。嗯,这里要注意,缓存一定要有过期策略。

1.3 缓存带来的挑战

缓存不是银弹。用得好是神器,用不好就是定时炸弹。

我总结了一下,常见的坑有四个:一致性、雪崩、穿透、击穿。咱们一个一个说。

1.3.1 缓存一致性

什么叫一致性?就是「缓存里的数据」和「数据库里的数据」得一样。

但现实是,数据在变。你更新了数据库,缓存没更新,用户拿到的就是脏数据。

常见的解决方案有三种:

方案 做法 优缺点
先更新数据库,再删除缓存 更新 DB → 删缓存 简单,但删除可能失败
先删缓存,再更新数据库 删缓存 → 更新 DB 有并发问题,可能读到旧数据
延迟双删 删缓存 → 更新 DB → 延迟再删一次 更可靠,但多一次删除开销

我的建议:大多数场景用「先更新 DB,再删缓存」就够了。如果对一致性要求极高,可以考虑延迟双删或者用消息队列保证最终一致性。

1.3.2 缓存雪崩

缓存雪崩,就是大量缓存同时失效,或者缓存服务挂了,所有请求直接打到数据库上。

数据库扛不住,就挂了。然后整个系统跟着崩。

我曾经在一个电商项目中遇到过——双十一大促,所有商品的缓存过期时间都设成了整点。结果零点一到,缓存集体失效,数据库瞬间被打爆。那叫一个惨。

怎么防?

  • 过期时间加随机值:比如 3600 + random(0, 600),避免集体失效
  • 缓存高可用:用 Redis 哨兵或集群,别单点
  • 限流降级:数据库扛不住时,直接返回默认值或错误提示

避坑指南:我曾经以为 Redis 单机就够了,结果一次机房断电,缓存全丢,数据库被打到 100% CPU。从那以后,我再也不敢不做高可用。

1.3.3 缓存穿透

缓存穿透,是指请求的数据在缓存和数据库中都不存在。

比如查一个不存在的用户 ID。每次请求都绕过缓存,直接查数据库。如果恶意攻击者批量请求不存在的 ID,数据库就废了。

解决方案:

  • 缓存空值:查不到数据时,也缓存一个空值,设置短过期时间(比如 5 分钟)
  • 布隆过滤器:请求来之前先判断 ID 是否存在,不存在直接拒绝

我个人更推荐布隆过滤器。为什么呢?因为缓存空值会占用内存,而且如果攻击者换着 ID 打,你还是扛不住。

# 布隆过滤器示例(伪代码)
bloom_filter = BloomFilter(size=1000000, error_rate=0.01)

# 初始化时加载所有合法 ID
for user_id in db.query("SELECT id FROM users"):
    bloom_filter.add(user_id)

# 请求时先判断
if not bloom_filter.contains(request_id):
    return "数据不存在"

1.3.4 缓存击穿

缓存击穿和雪崩有点像,但范围不同。

击穿是指某个热点 key 过期了,同时有大量并发请求访问它。这些请求全部打到数据库上。

比如微博热搜第一的帖子,缓存过期了。一瞬间几万个人刷新,数据库直接跪。

怎么解决?

  • 互斥锁:第一个请求去查数据库,其他请求等待。查完写回缓存,后面的直接走缓存
  • 逻辑过期:缓存永不过期,但数据里带一个过期时间字段。后台异步更新

我的经验:互斥锁实现简单,但会阻塞请求。逻辑过期更优雅,但实现复杂一点。如果并发量不是特别大,互斥锁完全够用。

小结

这一节咱们聊了缓存的基础。说白了,缓存就是「用内存换速度」。但引入缓存的同时,也带来了四个挑战:

  • 一致性:数据同步问题
  • 雪崩:集体失效问题
  • 穿透:空数据攻击问题
  • 击穿:热点 key 过期问题

这些坑我都踩过。嗯,下一节咱们会深入 Redis 的数据结构,看看怎么用 Redis 解决这些问题。到时候我会分享更多实战中的血泪史。

今天就到这儿。有问题随时交流。


公众号:蓝海资料掘金营,微信deep3321