在高并发场景下,单机Redis已经无法满足性能需求?Caffeine+Redis两级缓存架构 是性能优化的利器!本文将手把手教你搭建本地缓存+分布式缓存的完整架构,包含三种实现方式:手动编码、Spring注解、自定义注解+AOP切面,并深入讲解分布式环境下的缓存一致性解决方案。这是全网最完整的两级缓存实战指南!
📋 文章目录
- 一、两级缓存架构概述
- [1.1 为什么需要两级缓存](#1.1 为什么需要两级缓存)
- [1.2 两级缓存架构优缺点](#1.2 两级缓存架构优缺点)
- 二、项目环境准备
- 三、手动实现两级缓存
- 四、Spring注解方式实现
- 五、自定义注解+AOP实现
- 六、缓存一致性问题与解决方案
- 七、总结与最佳实践
一、两级缓存架构概述
1.1 为什么需要两级缓存
在微服务架构中,热点数据的访问路径通常是:
应用 → Redis → 数据库
问题:
- 每次访问Redis都有网络I/O开销
- Redis的性能虽然很高,但仍有毫秒级延迟
解决方案 :
引入**本地缓存(Caffeine)+ 分布式缓存(Redis)**的两级缓存架构:
应用 → Caffeine(一级缓存)→ Redis(二级缓存)→ 数据库
1.2 两级缓存架构图
┌─────────────────────────────────────────────────────────────┐
│ 应用节点 │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ Caffeine本地缓存 │ │ 业务逻辑 │ │
│ │ (一级缓存) │ │ │ │
│ │ - 访问速度:纳秒级 │ │ 1. 先查Caffeine │ │
│ │ - 容量:小 │ │ 2. 再查Redis │ │
│ │ - 数据:热点数据 │ │ 3. 最后查DB │ │
│ └─────────────────────┘ └─────────────────────────────┘ │
└──────────────────────────────┬──────────────────────────────┘
│ 未命中
▼
┌─────────────────────────────────────────────────────────────┐
│ Redis分布式缓存 │
│ (二级缓存)- 多个应用节点共享 │
│ - 访问速度:毫秒级 │
│ - 容量:较大 │
└──────────────────────────────┬──────────────────────────────┘
│ 未命中
▼
┌─────────────────────────────────────────────────────────────┐
│ MySQL数据库 │
└─────────────────────────────────────────────────────────────┘
1.3 两级缓存架构优缺点
优点:
- 访问速度极快:Caffeine基于应用内存,访问速度是纳秒级
- 减少网络I/O:避免频繁的Redis远程访问,降低网络通信耗时
- 降低Redis压力:大量请求由本地缓存处理,减轻Redis负担
- 提升系统吞吐量:减少外部依赖,提高整体性能
缺点:
- 数据一致性问题:需要保证两级缓存与数据库的数据一致性
- 分布式一致性问题:多节点下,一级缓存之间的数据同步
- 内存占用:本地缓存占用应用内存
- 过期策略复杂:需要合理设置过期时间
二、项目环境准备
2.1 数据库表结构
sql
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT(11) NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
INSERT INTO user (id, name, age, email) VALUES
(1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
2.2 Maven依赖
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.14</version>
</dependency>
<!-- Caffeine缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
2.3 配置文件
properties
# 数据源配置
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mp?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
# Redis配置
spring.redis.host=localhost
spring.redis.port=6379
# MyBatis Plus日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.4 实体类
java
@Data
@ToString
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
三、手动实现两级缓存
3.1 Caffeine配置
java
@Configuration
public class CaffeineConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
.initialCapacity(128) // 初始大小
.maximumSize(1024) // 最大数量
.expireAfterWrite(15, TimeUnit.SECONDS) // 过期时间15秒
.build();
}
}
3.2 Service层实现
java
@Service
@Slf4j
@AllArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final Cache<String, Object> cache;
private final RedisTemplate<String, Object> redisTemplate;
/**
* Caffeine + Redis 两级缓存查询
*/
public User queryWithTwoLevelCache(long userId) {
String key = "user-" + userId;
// 从Caffeine获取,如果不存在则执行加载逻辑
User user = (User) cache.get(key, k -> {
// 1. 先查询Redis(二级缓存)
Object obj = redisTemplate.opsForValue().get(key);
if (Objects.nonNull(obj)) {
log.info("Get data from Redis: {}", key);
return obj;
}
// 2. Redis没有则查询数据库
User userFromDb = userMapper.selectById(userId);
log.info("Get data from database: {}", userId);
// 3. 写入Redis,设置30秒过期
redisTemplate.opsForValue().set(key, userFromDb, 30, TimeUnit.SECONDS);
return userFromDb;
});
return user;
}
}
3.3 流程解析
查询流程:
│
├─ 1. 从Caffeine查询
│ ├─ 命中 → 直接返回 ✓
│ └─ 未命中 → 继续
│
├─ 2. 从Redis查询
│ ├─ 命中 → 写入Caffeine,返回 ✓
│ └─ 未命中 → 继续
│
├─ 3. 从数据库查询
│ └─ 写入Redis + Caffeine,返回 ✓
四、Spring注解方式实现
4.1 启用缓存
在启动类上添加@EnableCaching注解:
java
@SpringBootApplication
@EnableCaching // 启用缓存
@MapperScan("com.msb.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
4.2 CacheManager配置
java
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.initialCapacity(128)
.maximumSize(1024)
.expireAfterWrite(15, TimeUnit.SECONDS));
return cacheManager;
}
}
4.3 Service层实现
java
@Service
@Slf4j
@AllArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
/**
* 使用@Cacheable注解实现两级缓存
*/
@Cacheable(value = "user", key = "#userId")
public User queryWithAnnotation(long userId) {
String key = "user-" + userId;
// 1. 查询Redis(二级缓存)
Object obj = redisTemplate.opsForValue().get(key);
if (Objects.nonNull(obj)) {
log.info("Get data from Redis: {}", key);
return (User) obj;
}
// 2. Redis没有则查询数据库
User user = userMapper.selectById(userId);
log.info("Get data from database: {}", userId);
// 3. 写入Redis
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);
return user;
}
/**
* 更新数据时清除缓存
*/
@CacheEvict(value = "user", key = "#userId")
public void deleteUser(long userId) {
// 删除数据库数据
userMapper.deleteById(userId);
// 删除Redis缓存
redisTemplate.delete("user-" + userId);
}
}
4.4 缓存注解说明
| 注解 | 作用 | 使用场景 |
|---|---|---|
@Cacheable |
先查缓存,不存在则执行方法并缓存 | 查询操作 |
@CachePut |
执行方法并更新缓存 | 更新操作 |
@CacheEvict |
清除缓存 | 删除操作 |
4.5 缓存注解属性
| 属性 | 说明 | 示例 |
|---|---|---|
value |
缓存名称 | @Cacheable(value="user") |
key |
缓存key(支持SpEL) | @Cacheable(key="#userId") |
condition |
缓存条件 | @Cacheable(condition="#userId%2==0") |
unless |
不缓存的条件 | @Cacheable(unless="#result==null") |
常用SpEL表达式:
#root.methodName # 当前方法名
#root.method.name # 当前方法
#root.target # 当前被调用对象
#root.args[0] # 第一个参数
#result # 方法返回值
#userId # 参数userId
4.6 条件缓存示例
java
// 只有当userId为偶数时才缓存
@Cacheable(value = "user", key = "#userId", condition = "#userId%2==0")
public User queryWithCondition(long userId) {
// ...
}
4.7 清除缓存示例
java
// 清除所有缓存
@CacheEvict(value = "user", allEntries = true)
public void deleteAll() {
// ...
}
// 方法执行前清除缓存
@CacheEvict(value = "user", key = "#userId", beforeInvocation = true)
public void deleteBefore(long userId) {
// ...
}
五、自定义注解+AOP实现
5.1 自定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {
String cacheName(); // 缓存名称
String key(); // 缓存key(支持SpEL)
long l2TimeOut() default 120; // 二级缓存过期时间(秒)
CacheType type() default CacheType.FULL; // 操作类型
}
// 缓存类型枚举
public enum CacheType {
FULL, // 存取(默认)
PUT, // 只存(强制更新)
DELETE // 删除
}
5.2 SpEL表达式解析器
java
public class ElParser {
public static String parse(String elString, TreeMap<String, Object> map) {
elString = String.format("#{%s}", elString);
// 创建表达式解析器
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
// 设置变量
map.forEach(context::setVariable);
// 解析表达式
Expression expression = parser.parseExpression(elString, new TemplateParserContext());
return expression.getValue(context, String.class);
}
}
5.3 AOP切面实现
java
@Slf4j
@Component
@Aspect
@AllArgsConstructor
public class CacheAspect {
private final Cache<String, Object> cache;
private final RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(com.msb.caffeine.cache.DoubleCache)")
public void cacheAspect() {}
@Around("cacheAspect()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 解析SpEL表达式,组装缓存key
String[] paramNames = signature.getParameterNames();
Object[] args = point.getArgs();
TreeMap<String, Object> treeMap = new TreeMap<>();
for (int i = 0; i < paramNames.length; i++) {
treeMap.put(paramNames[i], args[i]);
}
DoubleCache annotation = method.getAnnotation(DoubleCache.class);
String elResult = ElParser.parse(annotation.key(), treeMap);
String realKey = annotation.cacheName() + ":" + elResult;
// 根据操作类型处理
if (annotation.type() == CacheType.PUT) {
// 强制更新
Object result = point.proceed();
redisTemplate.opsForValue().set(realKey, result, annotation.l2TimeOut(), TimeUnit.SECONDS);
cache.put(realKey, result);
return result;
} else if (annotation.type() == CacheType.DELETE) {
// 删除缓存
redisTemplate.delete(realKey);
cache.invalidate(realKey);
return point.proceed();
}
// 存取操作(CacheType.FULL)
// 1. 查询Caffeine
Object caffeineCache = cache.getIfPresent(realKey);
if (Objects.nonNull(caffeineCache)) {
log.info("Get data from Caffeine");
return caffeineCache;
}
// 2. 查询Redis
Object redisCache = redisTemplate.opsForValue().get(realKey);
if (Objects.nonNull(redisCache)) {
log.info("Get data from Redis");
cache.put(realKey, redisCache);
return redisCache;
}
// 3. 查询数据库
log.info("Get data from database");
Object result = point.proceed();
if (Objects.nonNull(result)) {
// 写入Redis和Caffeine
redisTemplate.opsForValue().set(realKey, result, annotation.l2TimeOut(), TimeUnit.SECONDS);
cache.put(realKey, result);
}
return result;
}
}
5.4 使用自定义注解
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 查询 - 使用两级缓存
*/
@DoubleCache(cacheName = "user", key = "#userId", type = CacheType.FULL)
public User query(Long userId) {
return userMapper.selectById(userId);
}
/**
* 更新 - 强制更新缓存
*/
@DoubleCache(cacheName = "user", key = "#user.id", type = CacheType.PUT)
public int update(User user) {
return userMapper.updateById(user);
}
/**
* 删除 - 清除缓存
*/
@DoubleCache(cacheName = "user", key = "#userId", type = CacheType.DELETE)
public void delete(Long userId) {
userMapper.deleteById(userId);
}
}
5.5 三种实现方式对比
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手动实现 | 灵活可控 | 代码侵入性强 | 简单项目 |
| Spring注解 | 简洁优雅 | 功能相对简单 | 标准CRUD |
| 自定义注解 | 高度灵活、可扩展 | 实现复杂 | 复杂业务 |
六、缓存一致性问题与解决方案
6.1 问题描述
在分布式多节点环境下:
-
节点A 修改了数据,更新了Redis
-
节点B 的Caffeine缓存仍然是旧数据
-
数据不一致!
节点A Redis 节点B
│ │ │
├─ 修改数据 ───────────►│ │
├─ 更新Redis ──────────►│ │
│ │ │
│ │◄──────────────────────┤ 查询(Caffeine命中,返回旧数据)
│ │ │
│ │ (数据不一致!) │
6.2 解决方案:Redis发布订阅
使用Redis的**发布订阅(Pub/Sub)**机制,实现缓存同步:
节点A(修改数据) Redis 节点B(订阅者)
│ │ │
├─ 修改数据 ───────────►│ │
├─ 更新Redis ──────────►│ │
├─ 发布消息 ───────────►│ │
│ ├─ 推送消息 ───────────►│
│ │ ├─ 清除本地Caffeine缓存
│ │ │
│ │◄──────────────────────┤ 重新查询(同步成功)
6.3 核心实现代码
java
@Component
@Slf4j
@AllArgsConstructor
public class CacheMessageListener implements MessageListener {
private final Cache<String, Object> caffeineCache;
@Override
public void onMessage(Message message, byte[] pattern) {
String key = new String(message.getBody());
log.info("Received cache invalidation message: {}", key);
// 清除本地Caffeine缓存
caffeineCache.invalidate(key);
log.info("Caffeine cache invalidated: {}", key);
}
}
@Configuration
public class RedisPubSubConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
MessageListener cacheMessageListener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(cacheMessageListener, new PatternTopic("cache:invalidate:*"));
return container;
}
}
6.4 修改数据时发送通知
java
@Service
@Slf4j
@AllArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final Cache<String, Object> cache;
private final RedisTemplate<String, Object> redisTemplate;
private final StringRedisTemplate stringRedisTemplate;
public void updateUser(User user) {
String key = "user:" + user.getId();
// 1. 更新数据库
userMapper.updateById(user);
// 2. 更新Redis
redisTemplate.opsForValue().set(key, user, 30, TimeUnit.SECONDS);
// 3. 清除本地缓存
cache.invalidate(key);
// 4. 发布缓存失效消息(通知其他节点)
stringRedisTemplate.convertAndSend("cache:invalidate:user", key);
log.info("Published cache invalidation message: {}", key);
}
}
6.5 缓存一致性策略总结
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Cache Aside | 先更新DB,再删除缓存 | 读多写少 |
| Read/Write Through | 读写都经过缓存 | 读写均衡 |
| Write Behind | 先写缓存,异步写DB | 写多读少 |
| Pub/Sub同步 | 多节点缓存同步 | 分布式环境 |
七、总结与最佳实践
7.1 核心要点
- 两级缓存架构:Caffeine(一级)+ Redis(二级)+ DB
- 查询顺序:Caffeine → Redis → DB
- 写入顺序:DB → Redis → 清除Caffeine → 通知其他节点
- 过期策略:Caffeine过期时间 < Redis过期时间
7.2 最佳实践
缓存过期时间设置:
Caffeine过期时间:15秒 ~ 1分钟
Redis过期时间:30秒 ~ 5分钟
适用场景:
- 读多写少的热点数据
- 实时性要求不高的配置数据
- 计算成本高的复杂查询结果
不适用场景:
- 实时性要求极高的数据
- 频繁变更的数据
- 数据一致性要求严格的场景
7.3 常见问题
Q1: 为什么会出现缓存不一致?
分布式环境下,各节点的本地缓存独立,一个节点修改数据后,其他节点的本地缓存无法自动感知。
Q2: 如何保证缓存一致性?
使用Redis发布订阅机制,修改数据时发送消息通知其他节点清除本地缓存。
Q3: 缓存穿透怎么解决?
使用布隆过滤器,或缓存空值(设置较短过期时间)。
Q4: 缓存雪崩怎么解决?
设置不同的过期时间,使用互斥锁防止大量请求同时访问DB。
7.4 完整配置速查表
java
// Caffeine配置
Caffeine.newBuilder()
.initialCapacity(128) // 初始容量
.maximumSize(1024) // 最大容量
.maximumWeight(10000) // 最大权重
.expireAfterWrite(60, TimeUnit.SECONDS) // 写入后过期
.expireAfterAccess(30, TimeUnit.SECONDS) // 访问后过期
.refreshAfterWrite(10, TimeUnit.SECONDS) // 自动刷新
.weakValues() // 值使用弱引用
.softValues() // 值使用软引用
.recordStats() // 开启统计
.removalListener((k, v, cause) -> {}) // 清除监听
.build();
// Redis配置
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
关键词:两级缓存, Caffeine, Redis, 本地缓存, 分布式缓存, 缓存一致性, Spring Cache, 自定义注解, AOP, 缓存穿透, 缓存雪崩
如果本文对你有帮助,欢迎点赞、收藏、关注!有任何缓存架构问题,欢迎在评论区留言讨论。