java基于Google的Guava本地缓存、Redis和MySQL设计三级缓存架构.
在高性能、高并发的Java应用中,基于 Guava Cache (一级缓存)、Redis (二级缓存) 和 MySQL (三级/底层存储) 构建三级缓存架构,是解决超高并发、降低数据库压力、应对热点数据突发的经典方案。
下面为你详细拆解这个架构的设计思路、核心流程、代码实现以及防坑指南。
## 核心架构设计与角色定位
-
L1 一级缓存:Guava Cache (本地/进程内缓存)
-
特点:速度极快(微秒级),因为数据直接存在JVM内存中,无网络IO。但容量受限于JVM内存,且分布式部署时各节点数据不共享。
-
定位:存放超热点数据或配置数据,抗住第一波最猛烈的流量冲击。
-
-
L2 二级缓存:Redis (分布式缓存)
-
特点:速度快(毫秒级),独立于应用层。容量大(可集群扩展),支持高可用,分布式节点间数据共享。
-
定位:存放普通热点数据,作为全量数据的缓冲层。
-
-
L3 三级存储:MySQL (关系型数据库)
-
特点:持久化存储,事务支持,但并发承载能力低(通常单机读写在几千TPS)。
-
定位:最终的数据源,保证数据的准确性和持久化。
-
## 核心读写流程
1. 读取流程 (Read-Through)
当请求进来时,遵循 “逐级向下查找,逐级向上回填” 的原则:
-
查 Guava:命中则直接返回。
-
未命中,查 Redis:命中后,将数据回填至 Guava,然后返回。
-
未命中,查 MySQL:
-
命中后,将数据异步/同步回填至 Redis 和 Guava,然后返回。
-
若 MySQL 也没有,返回空(注意防止缓存穿透)。
-
2. 更新流程 (Write-Through / Cache Aside)
为了保证各级缓存与数据库的一致性,推荐采用修改数据库 + 淘汰缓存的策略:
-
更新 MySQL 数据库。
-
删除 Redis 中的缓存(使其中断)。
-
广播通知所有 Java 节点清除 Guava 本地缓存(可以通过 Redis Pub/Sub 或 MQ 实现)。
## 核心代码实现
以下是一个基于 Spring Boot 的整合实现示例:
1. 引入依赖 (Maven)
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2. 缓存配置与核心逻辑实现
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class MultiLevelCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private UserMapper userMapper; // 假设这是MySQL的DAO
// 1. 初始化 L1 Guava 缓存
private final Cache<String, Object> guavaCache = CacheBuilder.newBuilder()
.initialCapacity(100) // 初始容量
.maximumSize(1000) // 最大容量,超过后按LRU淘汰
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.build();
/**
* 查询数据(三级缓存核心逻辑)
*/
public Object getProductData(String key) {
// Step 1: 从 L1 Guava 缓存读取
Object val = guavaCache.getIfPresent(key);
if (val != null) {
return val; // L1 命中
}
// Step 2: L1 未命中,加锁防止并发击穿到数据库
synchronized (this) {
// 双重检查 (DCL),防止并发时重复查Redis/DB
val = guavaCache.getIfPresent(key);
if (val != null) return val;
// Step 3: 从 L2 Redis 缓存读取
val = redisTemplate.opsForValue().get(key);
if (val != null) {
// L2 命中,回填 L1
guavaCache.put(key, val);
return val;
}
// Step 4: 从 L3 MySQL 读取
val = userMapper.selectById(key);
if (val != null) {
// L3 命中,回填 L2 和 L1
redisTemplate.opsForValue().set(key, val, 1, TimeUnit.HOURS); // Redis缓存1小时
guavaCache.put(key, val);
} else {
// 防止缓存穿透:如果DB也没有,存入一个空值/占位符,设置短过期时间
redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
}
}
return val;
}
/**
* 更新数据
*/
public void updateProductData(String key, Object newData) {
// 1. 更新三级存储 MySQL
userMapper.updateById(newData);
// 2. 传统/删除二级缓存 Redis
redisTemplate.delete(key);
// 3. 清除当前节点的一级缓存
guavaCache.invalidate(key);
// 4. (关键) 广播给其他分布式节点清除分布式 L1 缓存
// 可以使用 redisTemplate.convertAndSend("cache_invalidate_channel", key);
}
}
## 分布式环境下的痛点与解决方案
在多节点部署(分布式环境)下,引入 Guava 会带来一个致命问题:本地缓存如何保持一致性? 当 A 节点修改了数据并删除了自己的 Guava 缓存,B 节点的 Guava 缓存里还是老数据。
解决方案:Redis Pub/Sub(发布订阅)
利用 Redis 的消息机制来同步删除各节点的 L1 缓存。
-
配置订阅监听器:
@Configuration
public class RedisMessageConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic("cache:clear"));
return container;
}
@Bean
MessageListenerAdapter listenerAdapter(CacheMessageReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
}
-
接收消息并清除 L1:
@Component
public class CacheMessageReceiver {
@Autowired
private MultiLevelCacheService cacheService;
public void receiveMessage(String key) {
// 收到过期通知,清除本地 Guava 缓存
cacheService.getGuavaCache().invalidate(key);
}
}
## 三级缓存防坑指南(生产级考量)
1. 缓存击穿(热点Key过期) 当一个超热点 Key 在 Guava 和 Redis 同时过期,大流量会瞬间压垮 MySQL。
-
解法:在查 DB 处加本地锁 (synchronized/ReentrantLock) 或 分布式锁。如上面代码中的双重检查锁(DCL),确保只有一个线程去查 DB,其余线程等待后直接转为读缓存。
2. 缓存穿透(查询不存在的数据) 故意查询海量不存在的 ID,导致三级缓存全部穿透,直接轰炸 MySQL。
-
解法:
-
布隆过滤器 (Bloom Filter):在流量进入 Guava 之前,用布隆过滤器拦截不存在的 Key。
-
缓存空对象:如果 MySQL 查不到,往 Redis 和 Guava 里塞一个
""或固定标识,设置 3~5 分钟的短过期时间。
-
3. 缓存雪崩(大批Key同时过期) Redis 中大量 Key 同时到期,流量全沉降到 MySQL。
-
解法:设置 Redis 过期时间时,加上一个随机扰动值(如
1小时 + Math.random() * 10分钟),防止缓存集体交班。
4. 内存溢出 (OOM) Guava 是吃 JVM 堆内存的。如果设置不当,会导致频繁 Full GC 甚至 OOM。
-
解法:严格限制 Guava 的
maximumSize(最大条数)或maximumWeight(最大权重),并配合expireAfterWrite,让老数据及时被 LRU 算法淘汰。
## 总结与演进方向
在实际企业级开发中,如果觉得纯手写这套逻辑比较繁琐且容易出错,可以考虑以下演进和替代方案:
-
Redisson 的 LocalCachedMap:Redisson 官方自带了本地缓存与 Redis 自动同步的功能,封装了发布订阅逻辑,开箱即用。
-
JetCache 或 Layering-Cache:国内开源的优秀多级缓存框架,通过注解(如
@Cached(cacheType = CacheType.BOTH))即可完美实现 Guava + Redis 的两级缓存自动切换与同步。
Google Guava 是 Google 开发的一个开源 Java 核心库,它极大地扩展了 Java 标准库(JDK)的功能。Guava 的设计初衷是减少重复造轮子、提高代码可读性、增强开发效率并提供高性能的常用工具。
在 Java 开发中,Guava 扮演着“瑞士军刀”的角色,其核心功能主要集中在以下几个方面:
## 1. 强大的集合工具与新集合类型 (Collections)
Guava 对 JDK 集合框架进行了极大的补充,不仅提供了便捷的静态工厂方法,还引入了许多实用的新集合类型。
-
不可变集合 (Immutable Collections):
创建后无法修改的集合(如
ImmutableList、ImmutableSet、ImmutableMap)。它们线程安全、内存利用率高,能有效防止数据被恶意或意外修改。 -
新集合类型:
-
Multiset:可以重复添加元素,并能方便地统计每个元素出现的次数(相当于Map<K, Integer>,但更好用)。 -
BiMap:双向映射 Map。不仅可以用 Key 查 Value,还能用 Value 查 Key(要求 Value 也必须唯一)。 -
Table:双键 Map(相当于Map<R, Map<C, V>>),适用于表格化数据结构(行、列、值)。 -
Multimap:一个 Key 可以对应多个 Value 的 Map(相当于Map<K, List<V>>)。
-
## 2. 高性能本地缓存 (Guava Cache)
在不需要分布式缓存(如 Redis)的场景下,Guava Cache 是 Java 本地缓存的首选方案。
-
特点:全内存存储、线程安全、高并发性能好。
-
丰富的功能:
-
支持三种淘汰机制:基于容量限制(LRU)、基于时间(写入时间/访问时间过期)、基于引用(弱引用/软引用)。
-
支持自动加载(
CacheLoader),当缓存未命中时自动从数据库或数据源加载数据。 -
提供缓存命中率、驱逐次数等统计信息,方便监控。
-
## 3. 字符串处理工具 (Strings)
JDK 的字符串操作有时比较繁琐,Guava 提供了极其优雅的连接、分割和修饰工具。
-
Joiner(连接器):跳过
null值或将null替换为特定字符串,并用指定分隔符拼接集合。Java// 结果: "Harry,Ron,Hermione" String result = Joiner.on(",").skipNulls().join("Harry", null, "Ron", "Hermione"); -
Splitter(分割器):比
String.split更安全,可以自动去掉结果中的空格,并过滤掉空字符串。Java// 结果包含: "a", "b" List<String> list = Splitter.on(',').trimResults().omitEmptyStrings().splitToList("a, ,b,"); -
CharMatcher(字符匹配器):用于对字符进行复杂的修剪(trim)、塌陷(collapse)、移除或替换。
## 4. 基础工具与优雅的非空校验 (Preconditions & Optional)
Guava 提供了大量简化基础代码的工具,帮助编写更具防御性的代码。
-
Preconditions(前置条件检查):用于方法参数校验,如果不满足条件直接抛出对应异常(如
NullPointerException或IllegalArgumentException),替代繁琐的if-else。Java// 如果 count <= 0,直接抛异常并输出自定义信息 Preconditions.checkArgument(count > 0, "must be positive: %s", count); -
Optional:在 Java 8 引入
java.util.Optional之前,Guava 就率先实现了Optional,用于优雅地处理null值,避免臭名昭著的空指针异常(NPE)。 -
Objects / MoreObjects:简化了
equals、hashCode和toString()方法的重写。
## 5. 并发编程扩展 (Concurrency)
Guava 扩展了 JDK 的 java.util.concurrent 包,让异步编程和多线程处理更加简单。
-
ListenableFuture:允许你注册回调函数(Callback)。当异步任务完成后,自动触发回调,避免了原生
Future需要调用get()进行阻塞等待的问题。(Java 8 后的CompletableFuture吸取了这一设计思想)。 -
RateLimiter(限流器):基于令牌桶算法(Token Bucket)实现的限流工具,常用于接口防刷、高并发流量控制(如限制每秒只允许 100 个请求通过)。
## 6. 其它实用模块
-
I/O 工具:
Source和Sink抽象,简化了文件、流的读取和写入操作。 -
Hashing(哈希散列):提供了比 JDK 更丰富、更易用的 Hash 算法实现(如 MD5、SHA-1、SHA-256、MurmurHash 等)。
-
EventBus(事件总线):一个进程内的发布-订阅模式(Publish-Subscribe)组件,用于组件之间的解耦。
## 总结:为什么要用 Guava?
| 解决痛点 | Guava 的优雅实现 |
| 防止集合被意外修改 | 提供真正的不可变集合(Immutable 系列) |
| 繁琐的参数判空和校验 | 使用 Preconditions 一行代码搞定 |
| 复杂的字符串拼接/裁剪 | Joiner 和 Splitter 链式调用,天然过滤 null 和空格 |
| 高并发接口被刷爆 | 使用 RateLimiter 轻松实现平滑限流 |
| 原生 Future 阻塞拿结果 | 使用 ListenableFuture 实现纯异步回调 |
一句话概括:Guava 填补了 Java 标准库在集合、缓存、并发、I/O 和字符串处理等方面的空白,是写出更少、更稳健、更可读的 Java 代码的必备利器。

浙公网安备 33010602011771号