Caffeine+Redis两级缓存架构实战:从手动实现到自定义注解的完整方案

在高并发场景下,单机Redis已经无法满足性能需求?Caffeine+Redis两级缓存架构 是性能优化的利器!本文将手把手教你搭建本地缓存+分布式缓存的完整架构,包含三种实现方式:手动编码、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 两级缓存架构优缺点

优点

  1. 访问速度极快:Caffeine基于应用内存,访问速度是纳秒级
  2. 减少网络I/O:避免频繁的Redis远程访问,降低网络通信耗时
  3. 降低Redis压力:大量请求由本地缓存处理,减轻Redis负担
  4. 提升系统吞吐量:减少外部依赖,提高整体性能

缺点

  1. 数据一致性问题:需要保证两级缓存与数据库的数据一致性
  2. 分布式一致性问题:多节点下,一级缓存之间的数据同步
  3. 内存占用:本地缓存占用应用内存
  4. 过期策略复杂:需要合理设置过期时间

二、项目环境准备

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 核心要点

  1. 两级缓存架构:Caffeine(一级)+ Redis(二级)+ DB
  2. 查询顺序:Caffeine → Redis → DB
  3. 写入顺序:DB → Redis → 清除Caffeine → 通知其他节点
  4. 过期策略: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, 缓存穿透, 缓存雪崩

如果本文对你有帮助,欢迎点赞、收藏、关注!有任何缓存架构问题,欢迎在评论区留言讨论。

相关推荐
Solis程序员2 小时前
滑动窗口热键探测与三级缓存设计
java·spring·缓存
kcuwu.2 小时前
Claw Code 项目架构万字解读
人工智能·架构
真实的菜2 小时前
【无标题】Redis 从入门到精通(七):缓存设计与最佳实践 —— 穿透、击穿、雪崩与一致性终极指南
数据库·redis·缓存
念何架构之路2 小时前
存储技术Redis
数据库·redis·缓存
Rain5093 小时前
mini-cc 终端 UI:用 React 写 CLI 是什么体验
前端·人工智能·react.js·ui·架构·前端框架·ai编程
愚公搬代码3 小时前
【愚公系列】《移动端AI应用开发》014-DeepSeek API开发与集成(处理多轮对话与动态请求)
人工智能·中间件·架构
2603_954708313 小时前
微电网协调控制系统柜的应用场景有哪些?
分布式·安全·架构·能源·需求分析
LONGZETECH3 小时前
汽车仿真教学软件技术实现深度解析:从三维建模到学情数据闭环
c语言·3d·unity·架构·汽车
AI科技星3 小时前
精细结构常数α的多维度物理比值特性及空间螺旋模型研究
人工智能·线性代数·架构·概率论·学习方法