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的数据结构,看看那些看似简单的命令背后,藏着哪些坑和技巧。到时候见。