1. 缓存基础:为什么需要缓存、缓存的工作原理、缓存的分类

大家好,我是你们的老朋友。今天咱们聊聊缓存——这个后端开发里绕不开的话题。

说实话,我刚入行那会儿,对缓存的理解特别肤浅。就觉得「哦,把数据存内存里,快一点呗」。直到有一次线上事故,数据库被打挂了,我才真正意识到:缓存不是锦上添花,而是保命用的

1.1 为什么需要缓存?

你想想看,一个典型的后端服务,数据最终都落在数据库里。但数据库的读写速度,尤其是磁盘IO,跟内存比起来差了几个数量级。

我给大家列个直观的对比:

存储介质 典型延迟 QPS(粗略)
CPU L1缓存 ~1ns 极高
内存(Redis) ~100ns 10万+
SSD磁盘 ~100μs 几千
传统机械硬盘 ~10ms 几百
网络请求(跨机房) ~50ms 几十

看到了吧?内存比磁盘快上千倍。这就是缓存存在的根本原因——用空间换时间

我个人习惯把缓存的作用总结为三点:

  • 降低延迟:用户请求不用每次都穿透到数据库,直接从缓存拿,响应时间从几十毫秒降到微秒级。
  • 减轻数据库压力:数据库的连接数和IO能力是有限的。缓存扛住了大部分读请求,数据库就能喘口气。
  • 应对流量尖峰:比如秒杀、热点新闻。数据库扛不住瞬间的并发,但缓存可以。我在项目中遇到过双十一的流量,没有缓存的话,数据库早就被冲垮了。

核心观点:缓存不是可选项,而是高并发系统的必选项。没有缓存的系统,就像没有安全气囊的车——平时没事,出事就是大事。

1.2 缓存的工作原理

说白了,缓存就是一个键值对存储。你给它一个key,它返回一个value。就这么简单。

但背后的机制,其实挺有意思的。我画个简单的流程图给你看:

用户请求 → 查缓存 → 命中? → 是 → 返回缓存数据
                    ↓
                  否
                    ↓
              查数据库 → 写入缓存 → 返回数据

这个流程里有两个关键概念:

  • 缓存命中(Cache Hit):数据在缓存里找到了,直接返回。这是最理想的情况。
  • 缓存未命中(Cache Miss):数据不在缓存里,需要去数据库查。查完之后,记得把数据写回缓存,下次就能命中了。

嗯,这里要注意:缓存未命中本身不可怕,可怕的是大量请求同时未命中。这就是所谓的「缓存穿透」——所有请求都打到数据库上,数据库瞬间就跪了。

避坑指南:我曾经在一个项目里,上线前没做缓存预热。结果上线后,第一批用户的请求全部穿透到数据库,数据库连接池瞬间被打满,服务直接挂了。从那以后,我每次上线前都会做缓存预热,或者用「惰性加载+互斥锁」的方式防止雪崩。

缓存还有一个重要的机制——过期策略。你不能让数据永远留在缓存里,否则数据会跟数据库不一致。常见的过期策略有:

  • TTL(Time To Live):设置一个固定的过期时间,比如5分钟。时间到了自动删除。
  • LRU(Least Recently Used):当缓存满了,淘汰最久没被访问的数据。
  • LFU(Least Frequently Used):淘汰访问频率最低的数据。

我个人最常用的是TTL+LRU的组合。TTL保证数据不会太旧,LRU保证热点数据不被淘汰。

1.3 缓存的分类:本地缓存 vs 分布式缓存

缓存按部署方式,可以分为两大类。咱们一个一个说。

1.3.1 本地缓存

本地缓存,就是把数据存在当前进程的内存里。比如Go里的sync.Map,或者用map + sync.RWMutex自己实现一个。

优点很明显:

  • 速度极快:没有网络开销,直接内存访问。
  • 实现简单:几行代码就能搞定。
  • 适合小规模数据:比如配置信息、字典表。

缺点也很致命:

  • 容量有限:受限于单机内存大小。
  • 数据不一致:每个进程都有自己的缓存,更新一个进程的缓存,其他进程不知道。
  • 进程重启就丢失:服务重启,缓存全没了。

举个例子:

// Go 本地缓存简单实现
type LocalCache struct {
    data map[string]interface{}
    mu   sync.RWMutex
}

func (c *LocalCache) Get(key string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *LocalCache) Set(key string, val interface{}) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = val
}

这段代码看起来简单,但线上用的时候要小心。我见过有人直接用map不加锁,结果并发读写直接panic了。

1.3.2 分布式缓存

分布式缓存,就是独立部署的缓存服务,比如Redis、Memcached。所有应用进程共享同一个缓存集群。

优点:

  • 容量大:可以横向扩展,加机器就行。
  • 数据一致:所有进程访问同一个缓存服务。
  • 高可用:主从复制、哨兵、集群模式,挂了也能自动切换。

缺点:

  • 有网络开销:每次读写都要走网络,比本地缓存慢一点。
  • 运维复杂:需要部署、监控、扩容。
  • 成本更高:需要额外的机器资源。

我的建议:实际项目中,我通常会用「两级缓存」架构。本地缓存扛高频访问,分布式缓存做兜底。比如热点数据用本地缓存,普通数据用Redis。这样既快又稳。

1.3.3 怎么选?

我给大家一个简单的决策思路:

场景 推荐方案 原因
配置信息、字典表 本地缓存 数据量小,变化不频繁
用户会话(Session) 分布式缓存 多进程共享,需要一致性
热点数据(如商品详情) 两级缓存 本地缓存扛高频,Redis兜底
计数器、排行榜 分布式缓存 Redis的原子操作支持
临时数据(验证码) 分布式缓存 自动过期,跨服务共享

嗯,这里要强调一点:不要为了用缓存而用缓存。我见过有人把数据库里的所有数据都塞进Redis,结果内存爆了,数据还经常不一致。缓存是用来解决特定问题的,不是万能药。

小结

今天咱们聊了缓存的基础知识。总结下来就三句话:

  • 缓存是为了,用内存换时间。
  • 缓存的工作原理就是查缓存→未命中→查数据库→写缓存
  • 本地缓存快但容量小,分布式缓存容量大但有网络开销。实际项目中,两级缓存是最优解

下一章,咱们会深入Redis的数据结构,看看那些看似简单的命令背后,藏着哪些坑和技巧。到时候见。