【java】缓存机制详解
一文彻底理解 Java 缓存机制:原理、实现、缓存击穿详解
在实际开发中,我们经常会听到一句话:
“不要让数据库承受不该承受的压力。”
在高并发系统中,如果每一个请求都直接访问数据库,系统性能会迅速下降,甚至导致数据库崩溃。
为了解决这个问题,缓存(Cache)机制应运而生。
缓存是高性能系统设计中最核心的技术之一。
本文将从以下几个方面逐步讲解:
- 什么是缓存
- 为什么需要缓存
- Java 中常见缓存实现
- 缓存工作流程
- 缓存穿透 / 击穿 / 雪崩(重点:缓存击穿)
- Java 实际使用示例
- 面试常考问题总结
一、什么是缓存(Cache)
缓存本质上是一种“空间换时间”的策略。
缓存是指:
将访问频率高的数据存储在访问速度更快的存储介质中,从而减少计算或数据库访问的次数。
例如:
用户请求 -> 缓存 -> 数据库
流程:
1 用户请求数据
2 先查询缓存
3 如果缓存存在 -> 直接返回
4 如果缓存不存在 -> 查询数据库
5 将数据写入缓存
6 返回结果
示意图:
+--------+
| User |
+---+----+
|
v
+---+----+
| Cache |
+---+----+
/ \
命中 / \ 未命中
v v
返回数据 查询数据库
|
v
写入缓存
二、为什么需要缓存
缓存的核心价值有三个:
1 提升访问速度
内存访问速度远远快于数据库。
大致速度对比:
| 存储类型 | 访问速度 |
|---|---|
| CPU缓存 | 纳秒 |
| 内存 | 微秒 |
| Redis | 毫秒 |
| 数据库 | 毫秒~秒 |
因此:
缓存命中 -> 微秒
数据库查询 -> 毫秒
性能差距 1000倍以上。
2 减轻数据库压力
假设一个系统:
每秒请求:10000
数据库承受能力:1000
如果没有缓存:
数据库直接崩溃
如果有缓存:
90%命中缓存
数据库只处理1000请求
系统即可稳定运行。
3 提高系统并发能力
缓存可以将系统的瓶颈从:
数据库IO
转移到:
内存读取
并发能力提升非常明显。
三、Java中的缓存实现
Java中缓存实现主要有三种:
1 本地缓存
缓存存在JVM内存中
常见实现:
| 工具 | 说明 |
|---|---|
| HashMap | 最简单缓存 |
| ConcurrentHashMap | 线程安全 |
| Guava Cache | Google缓存库 |
| Caffeine | 高性能缓存库 |
示例:
Map<String, User> cache = new ConcurrentHashMap<>();
public User getUser(String id) {
User user = cache.get(id);
if(user == null){
user = database.query(id);
cache.put(id, user);
}
return user;
}
优点:
速度极快
无网络开销
缺点:
无法多机器共享
重启数据丢失
2 分布式缓存
缓存存储在独立服务器
常见实现:
| 技术 | 说明 |
|---|---|
| Redis | 最常见 |
| Memcached | 高性能缓存 |
架构:
应用服务器
|
v
Redis缓存
|
v
数据库
优点:
支持分布式
多服务器共享
缺点:
网络开销
需要维护
3 多级缓存
大型系统通常采用:
本地缓存 + Redis
架构:
用户
|
v
本地缓存
|
v
Redis
|
v
数据库
访问流程:
1 查本地缓存
2 没有查Redis
3 没有查数据库
优点:
性能最高
减少Redis压力
四、缓存的典型工作流程
最经典缓存模式:
Cache Aside Pattern(旁路缓存)
流程:
读取流程
1 查询缓存
2 缓存存在 -> 返回
3 缓存不存在 -> 查询数据库
4 写入缓存
5 返回数据
写入流程:
1 更新数据库
2 删除缓存
示例代码:
public User getUser(String id){
User user = redis.get(id);
if(user == null){
user = database.query(id);
redis.set(id, user);
}
return user;
}
五、缓存三大问题
缓存系统中有三个经典问题:
| 问题 | 说明 |
|---|---|
| 缓存穿透 | 查询不存在的数据 |
| 缓存击穿 | 热点数据缓存失效 |
| 缓存雪崩 | 大量缓存同时失效 |
其中:
缓存击穿是面试最常问的问题之一
六、缓存击穿
什么是缓存击穿
缓存击穿指:
某个热点数据在缓存过期的一瞬间,大量请求同时访问数据库。
示例:
某商品数据
缓存过期时间:10分钟
10分钟时:
10000请求同时访问
缓存失效
结果:
10000请求 -> 数据库
数据库瞬间压力爆炸。
示意图:
请求1 \
请求2 \
请求3 ---> 缓存失效 ---> 数据库
请求4 /
请求5 /
七、如何解决缓存击穿
常见解决方案:
1 互斥锁(最常见)
当缓存失效时:
只允许一个线程查询数据库
其他线程等待。
示例:
public User getUser(String id){
User user = redis.get(id);
if(user == null){
synchronized(this){
user = redis.get(id);
if(user == null){
user = database.query(id);
redis.set(id,user);
}
}
}
return user;
}
优点:
简单有效
缺点:
性能可能下降
2 热点数据永不过期
对于热点数据:
不设置过期时间
优点:
不会击穿
缺点:
需要手动更新
3 逻辑过期(推荐)
缓存结构:
value + expireTime
示例:
{
data: user,
expire: 1710000000
}
读取逻辑:
1 判断逻辑时间
2 过期 -> 异步更新
3 返回旧数据
优点:
不会阻塞
不会击穿
八、缓存穿透
缓存穿透指:
查询数据库中不存在的数据
例如:
用户ID = -1
数据库没有该数据:
缓存 -> 没有
数据库 -> 没有
攻击者大量请求:
数据库被打爆
解决方案:
1 缓存空对象
redis.set(id, null);
2 布隆过滤器
提前判断:
数据是否存在
九、缓存雪崩
缓存雪崩指:
大量缓存同时过期
示例:
10000缓存
同时10点过期
结果:
数据库瞬间压力巨大
解决方案:
1 过期时间随机
redis.set(key, value, 3600 + random);
2 多级缓存
本地缓存 + Redis
十、Java缓存工具推荐
实际项目中推荐使用:
Caffeine(最强本地缓存)
特点:
高性能
支持LRU
支持过期
示例:
Cache<String, User> cache =
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
User user = cache.get(id, key -> database.query(key));
十一、面试高频问题
1 什么是缓存?
缓存是:
将热点数据存储在访问更快的存储中
减少数据库访问
2 什么是缓存击穿?
热点数据缓存过期
大量请求同时访问数据库
解决方案:
互斥锁
逻辑过期
热点永不过期
3 缓存穿透和缓存击穿区别
| 类型 | 特点 |
|---|---|
| 穿透 | 查询不存在数据 |
| 击穿 | 热点数据过期 |
| 雪崩 | 大量缓存同时过期 |
4 常见缓存模式
Cache Aside
Read Through
Write Through
Write Back
最常用:
Cache Aside
十二、总结
缓存是高性能系统的核心技术之一。
本文核心知识:
缓存作用
提高性能
减少数据库压力
提升并发能力
缓存实现
本地缓存
Redis缓存
多级缓存
缓存三大问题
缓存穿透
缓存击穿
缓存雪崩
更多推荐


所有评论(0)