前言
这些年我参与设计过很多系统,越来越深刻地认识到:一个系统的性能如何,很大程度上取决于缓存用得怎么样。
同样是缓存,为何有人用起来系统飞升,有人却踩坑不断?
有些小伙伴在工作中可能遇到过这样的困惑:知道要用缓存,但面对本地缓存、Redis、Memcached、CDN等多种选择,到底该用哪种?
今天这篇文章跟大家一起聊聊工作中最常用的6种缓存,希望对你会有所帮助。
更多项目实战在我的技术网站:www.susan.net.cn/project
01 为什么缓存如此重要?
在正式介绍各种缓存之前,我们先要明白:为什么要用缓存?
想象这样一个场景:你的电商网站首页,每次打开都要从数据库中查询轮播图、热门商品、分类信息等数据。
如果每秒有1万个用户访问,数据库就要承受1万次查询压力。
java
// 没有缓存时的查询
public Product getProductById(Long id) {
// 每次都直接查询数据库
return productDao.findById(id); // 每次都是慢速的磁盘IO
}
这就是典型的无缓存场景。
数据库的磁盘IO速度远低于内存,当并发量上来后,系统响应变慢,数据库连接池被占满,最终导致服务不可用。
缓存的核心价值可以用下面这个公式理解:
scss
系统性能 = (缓存命中率 × 缓存访问速度) + ((1 - 缓存命中率) × 后端访问速度)
缓存之所以能提升性能,基于两个计算机科学的基本原理:
- 局部性原理:程序访问的数据通常具有时间和空间局部性
- 存储层次结构:不同存储介质的速度差异巨大(内存比SSD快100倍,比HDD快10万倍)
从用户请求到数据返回,数据可能经过的各级缓存路径如下图所示:
理解了缓存的重要性,接下来我们逐一剖析这六种最常用的缓存技术。
02 本地缓存:最简单直接的性能提升
本地缓存指的是在应用进程内部维护的缓存存储,数据存储在JVM堆内存中。
核心特点
- 访问最快:直接内存操作,无网络开销
- 实现简单:无需搭建额外服务
- 数据隔离:每个应用实例独享自己的缓存
常用实现
1. Guava Cache:Google提供的优秀本地缓存库
java
// Guava Cache 示例
LoadingCache<Long, Product> productCache = CacheBuilder.newBuilder()
.maximumSize(10000) // 最大缓存项数
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期
.recordStats() // 开启统计
.build(new CacheLoader<Long, Product>() {
@Override
public Product load(Long productId) {
// 当缓存未命中时,自动加载数据
return productDao.findById(productId);
}
});
// 使用缓存
public Product getProduct(Long id) {
try {
return productCache.get(id);
} catch (ExecutionException e) {
throw new RuntimeException("加载产品失败", e);
}
}
2. Caffeine:Guava Cache的现代替代品,性能更优
java
// Caffeine 示例(性能优于Guava Cache)
Cache<Long, Product> caffeineCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(1, TimeUnit.MINUTES) // 支持刷新,Guava不支持
.recordStats()
.build(productId -> productDao.findById(productId));
// 异步获取
public CompletableFuture<Product> getProductAsync(Long id) {
return caffeineCache.get(id, productId ->
CompletableFuture.supplyAsync(() -> productDao.findById(productId)));
}
适用场景
- 数据量不大(通常不超过10万条)
- 数据变化不频繁
- 对访问速度要求极致
- 如:配置信息、静态字典、用户会话信息(短期)
优缺点分析
- 优点:极速访问、零网络开销、实现简单
- 缺点:数据不一致(各节点独立)、内存限制、重启丢失
有些小伙伴在工作中可能会犯一个错误:在分布式系统中过度依赖本地缓存,导致各节点数据不一致。记住:本地缓存适合存储只读或弱一致性的数据。
03 分布式缓存之王:Redis的深度解析
当数据需要在多个应用实例间共享时,本地缓存就不够用了,这时需要分布式缓存。而Redis无疑是这一领域的王者。
Redis的核心优势
java
// Spring Boot + Redis 示例
@Component
public class ProductCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PRODUCT_KEY_PREFIX = "product:";
private static final Duration CACHE_TTL = Duration.ofMinutes(30);
// 缓存查询
public Product getProduct(Long id) {
String key = PRODUCT_KEY_PREFIX + id;
// 1. 先查缓存
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 2. 缓存未命中,查数据库
product = productDao.findById(id);
if (product != null) {
// 3. 写入缓存
redisTemplate.opsForValue().set(key, product, CACHE_TTL);
}
return product;
}
// 使用更高效的方式:缓存空值防止缓存穿透
public Product getProductWithNullCache(Long id) {
String key = PRODUCT_KEY_PREFIX + id;
String nullKey = PRODUCT_KEY_PREFIX + "null:" + id;
// 检查是否是空值(防缓存穿透)
if (Boolean.TRUE.equals(redisTemplate.hasKey(nullKey))) {
return null;
}
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
product = productDao.findById(id);
if (product == null) {
// 缓存空值,短时间过期
redisTemplate.opsForValue().set(nullKey, "", Duration.ofMinutes(5));
return null;
}
redisTemplate.opsForValue().set(key, product, CACHE_TTL);
return product;
}
}
Redis的丰富数据结构
Redis不只是简单的Key-Value存储,它的多种数据结构适应不同场景:
| 数据结构 | 适用场景 | 示例 |
|---|---|---|
| String | 缓存对象、计数器 | SET user:1 '{"name":"张三"}' |
| Hash | 存储对象属性 | HSET product:1001 name "手机" price 2999 |
| List | 消息队列、最新列表 | LPUSH news:latest "新闻标题" |
| Set | 标签、共同好友 | SADD user:100:tags "数码" "科技" |
| Sorted Set | 排行榜、延迟队列 | ZADD leaderboard 95 "玩家A" |
| Bitmap | 用户签到、活跃统计 | SETBIT sign:2023:10 1 1 |
集群模式选择

适用场景
- 会话存储(分布式Session)
- 排行榜、计数器
- 消息队列
- 分布式锁
- 热点数据缓存
有些小伙伴在工作中使用Redis时,只把它当简单的Key-Value用,这就像用瑞士军刀只开瓶盖一样浪费。
深入理解Redis的数据结构,能让你的系统设计更优雅高效。
04 Memcached:简单高效的分布式缓存
在Redis崛起之前,Memcached是分布式缓存的首选。
虽然现在Redis更流行,但Memcached在某些场景下仍有其价值。
Memcached vs Redis 核心区别
java
// Memcached 客户端示例(使用XMemcached)
public class MemcachedService {
private MemcachedClient memcachedClient;
public void init() throws IOException {
// 创建客户端
memcachedClient = new XMemcachedClientBuilder(
AddrUtil.getAddresses("server1:11211 server2:11211"))
.build();
}
public Product getProduct(Long id) throws Exception {
String key = "product_" + id;
// 从Memcached获取
Product product = memcachedClient.get(key);
if (product != null) {
return product;
}
// 缓存未命中
product = productDao.findById(id);
if (product != null) {
// 存储到Memcached,过期时间30分钟
memcachedClient.set(key, 30 * 60, product);
}
return product;
}
}
两者的核心差异对比:
| 特性 | Redis | Memcached |
|---|---|---|
| 数据结构 | 丰富(String、Hash、List等) | 简单(Key-Value) |
| 持久化 | 支持(RDB/AOF) | 不支持 |
| 线程模型 | 单线程 | 多线程 |
| 内存管理 | 多种策略,可持久化 | 纯内存,重启丢失 |
| 使用场景 | 缓存+多样化数据结构 | 纯缓存 |
何时选择Memcached?
- 纯缓存场景:只需要简单的Key-Value缓存
- 超大Value存储:Memcached对超大Value支持更好
- 多线程高并发:Memcached的多线程模型在极端并发下可能表现更好
05 CDN缓存:加速静态资源的利器
有些小伙伴可能会疑惑:CDN也算缓存吗?当然算,而且是地理位置最近的缓存。
CDN的工作原理
CDN(Content Delivery Network)通过在各地部署边缘节点,将静态资源缓存到离用户最近的节点。
java
// 在应用中生成CDN链接
public class CDNService {
private String cdnDomain = "https://cdn.yourcompany.com";
public String getCDNUrl(String relativePath) {
// 添加版本号或时间戳,防止缓存旧版本
String version = getFileVersion(relativePath);
return String.format("%s/%s?v=%s", cdnDomain, relativePath, version);
}
// 上传文件到CDN的示例(伪代码)
public void uploadToCDN(File file, String remotePath) {
// 1. 上传到源站
uploadToOrigin(file, remotePath);
// 2. 触发CDN预热(将文件主动推送到边缘节点)
preheatCDN(remotePath);
// 3. 刷新旧缓存(如果需要)
refreshCDNCache(remotePath);
}
}
CDN缓存策略配置
ruby
# Nginx中的CDN缓存配置示例
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 365d; # 缓存一年
add_header Cache-Control "public, immutable";
# 添加版本号作为查询参数
if ($query_string ~* "^v=\d+") {
expires max;
}
}
适用场景
- 静态资源:图片、CSS、JS文件
- 软件下载包
- 视频流媒体
- 全球访问的网站
06 浏览器缓存:最前端的性能优化
浏览器缓存是最容易被忽视但效果最直接的缓存层级。合理利用浏览器缓存,可以大幅减少服务器压力。
HTTP缓存头详解
java
// Spring Boot中设置HTTP缓存头
@RestController
public class ResourceController {
@GetMapping("/static/{filename}")
public ResponseEntity<Resource> getStaticFile(@PathVariable String filename) {
Resource resource = loadResource(filename);
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(7, TimeUnit.DAYS)) // 缓存7天
.eTag(computeETag(resource)) // ETag用于协商缓存
.lastModified(resource.lastModified()) // 最后修改时间
.body(resource);
}
@GetMapping("/dynamic/data")
public ResponseEntity<Object> getDynamicData() {
Object data = getData();
// 动态数据设置较短缓存
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS)) // 30秒
.body(data);
}
}
浏览器缓存的两种类型
最近为了帮助大家找工作,专门建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了20多家大厂的工作内推岗位。加苏三的微信:li_su223,备注:掘金+所在城市,即可进群。
最佳实践
- 静态资源:设置长时间缓存(如一年),通过文件名哈希处理更新
- 动态数据:根据业务需求设置合理缓存时间
- API响应:适当使用ETag和Last-Modified
07 数据库缓存:容易被忽略的内部优化
数据库自身也有缓存机制,理解这些机制能帮助我们写出更高效的SQL。
MySQL查询缓存(已废弃但值得了解)
sql
-- 查看查询缓存状态(MySQL 5.7及之前)
SHOW VARIABLES LIKE 'query_cache%';
-- 在8.0之前,可以通过以下方式利用查询缓存
SELECT SQL_CACHE * FROM products WHERE category_id = 10;
InnoDB缓冲池(Buffer Pool)
这是MySQL性能的关键,缓存的是数据页和索引页。
sql
-- 查看缓冲池状态
SHOW ENGINE INNODB STATUS;
-- 重要的监控指标
-- 缓冲池命中率 = (1 - (innodb_buffer_pool_reads / innodb_buffer_pool_read_requests)) * 100%
-- 命中率应尽可能接近100%
数据库级缓存最佳实践
- 合理设置缓冲池大小:通常是系统内存的50%-70%
- 优化查询:避免全表扫描,合理使用索引
- 预热缓存:重启后主动加载热点数据
- 监控命中率:持续优化
有些小伙伴可能会过度依赖应用层缓存,而忽略了数据库自身的缓存优化。
数据库缓存是最后一道防线,优化好它能让整个系统更健壮。
08 综合对比与选型指南
接下来,我给大家一个选型指南:

实战中的多级缓存架构
在实际的高并发系统中,我们往往会采用多级缓存策略:
java
// 多级缓存示例:本地缓存 + Redis
@Component
public class MultiLevelCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 一级缓存:本地缓存
private Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.SECONDS) // 本地缓存时间短
.build();
// 二级缓存:Redis
private static final Duration REDIS_TTL = Duration.ofMinutes(10);
public Product getProductWithMultiCache(Long id) {
// 1. 查本地缓存
Product product = localCache.getIfPresent(id);
if (product != null) {
return product;
}
// 2. 查Redis
String redisKey = "product:" + id;
product = (Product) redisTemplate.opsForValue().get(redisKey);
if (product != null) {
// 回填本地缓存
localCache.put(id, product);
return product;
}
// 3. 查数据库
product = productDao.findById(id);
if (product != null) {
// 写入Redis
redisTemplate.opsForValue().set(redisKey, product, REDIS_TTL);
// 写入本地缓存
localCache.put(id, product);
}
return product;
}
}
09 缓存常见问题与解决方案
在使用缓存的过程中,我们不可避免地会遇到一些问题:
1. 缓存穿透
问题:大量请求查询不存在的数据,绕过缓存直接击穿数据库。
解决方案:
java
// 缓存空值方案
public Product getProductSafe(Long id) {
String key = "product:" + id;
String nullKey = "product:null:" + id;
// 检查空值标记
if (redisTemplate.hasKey(nullKey)) {
return null;
}
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
product = productDao.findById(id);
if (product == null) {
// 缓存空值,短时间过期
redisTemplate.opsForValue().set(nullKey, "", Duration.ofMinutes(5));
return null;
}
redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
return product;
}
2. 缓存雪崩
问题:大量缓存同时过期,请求全部打到数据库。
解决方案:
java
// 差异化过期时间
private Duration getRandomTTL() {
// 基础30分钟 + 随机0-10分钟
long baseMinutes = 30;
long randomMinutes = ThreadLocalRandom.current().nextLong(0, 10);
return Duration.ofMinutes(baseMinutes + randomMinutes);
}
3. 缓存击穿
问题:热点Key过期瞬间,大量并发请求同时查询数据库。
解决方案:
java
// 使用互斥锁(分布式锁)
public Product getProductWithLock(Long id) {
String key = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
// 尝试获取分布式锁
String lockKey = "lock:product:" + id;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (locked) {
try {
// 双重检查
product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
product = productDao.findById(id);
if (product != null) {
redisTemplate.opsForValue()
.set(key, product, Duration.ofMinutes(30));
}
}
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,等待后重试
try { Thread.sleep(50); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return getProductWithLock(id); // 递归重试
}
}
return product;
}
10 总结
通过这篇文章,我们系统地探讨了工作中最常用的六种缓存技术。
每种缓存都有其独特的价值和应用场景:
- 本地缓存:适合进程内、变化不频繁的只读数据
- Redis:功能丰富的分布式缓存,适合大多数共享缓存场景
- Memcached:简单高效的分布式缓存,适合纯Key-Value场景
- CDN缓存:加速静态资源,提升全球访问速度
- 浏览器缓存:最前端的优化,减少不必要的网络请求
- 数据库缓存:最后一道防线,优化数据库访问性能
缓存使用的核心原则可以总结为以下几点:
- 分级缓存:合理利用多级缓存架构
- 合适粒度:根据业务特点选择缓存粒度
- 及时更新:设计合理的缓存更新策略
- 监控告警:建立完善的缓存监控体系
有些小伙伴在工作中使用缓存时,容易陷入两个极端:要么过度设计,所有数据都加缓存;要么忽视缓存,让数据库承受所有压力。
我们需要懂得在合适的地方使用合适的缓存,在性能和复杂性之间找到最佳平衡点。
记住,缓存不是银弹,而是工具箱中的一件利器。
最后说一句(求关注,别白嫖我)
如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
更多项目实战在我的技术网站:www.susan.net.cn/project