MySQL + Redis + Caffeine:Java后端通用三级缓存架构实战

MySQL + Redis + Caffeine:Java后端通用三级缓存架构实战

一句话定位:让十万并发打到你的服务时,90%的请求在纳秒级本地内存就被消化掉,数据库安坐钓鱼台。


一、为什么单级Redis不够用?先看一组残酷的压测数据

很多团队的架构长这样:Controller → Redis → MySQL。看起来很美,但一上压测就现原形------

场景 Redis单节点表现
热点Key打满 单核CPU飙到95%,QPS卡在8万
网络RT损耗 客户端↔Redis往返0.5~2ms,Redis处理仅0.1ms------95%的时间浪费在路上
缓存击穿 热点Key过期瞬间,2000并发同时穿透,MySQL直接跪
缓存雪崩 10万Key同一秒过期,所有请求直涌数据库,系统雪崩

结论是冰冷的:Redis单线程模型下,热点Key就是阿喀琉斯之踵。 你需要一层"贴着CPU的缓存"------这就是Caffeine存在的意义。


二、三级缓存架构全景:分层扛压,逐级兜底

复制代码
复制代码
`┌─────────────────────────────────────────────────────────┐
│  L1 本地缓存 (Caffeine)                                   │
│  · 存储介质:JVM堆内存                                    │
│  · 访问延迟:纳秒级(<0.1ms)                             │
│  · 容量:有限(建议单实例≤10000条)                       │
│  · 作用:拦截超级热点(首页Banner、秒杀商品、热门课程)      │
├─────────────────────────────────────────────────────────┤
│  L2 分布式缓存 (Redis)                                    │
│  · 存储介质:独立Redis集群                                 │
│  · 访问延迟:毫秒级(~2ms)                               │
│  · 容量:可扩展(集群分片)                                │
│  · 作用:全局共享数据(用户会话、商品详情、库存计数)        │
├─────────────────────────────────────────────────────────┤
│  L3 数据库 (MySQL)                                        │
│  · 存储介质:磁盘                                         │
│  · 访问延迟:十毫秒级(~15ms)                            │
│  · 容量:无限                                             │
│  · 作用:数据最终源头,强一致性保障                        │
└─────────────────────────────────────────────────────────┘
`

核心设计原则:读写分离,写请求穿透所有缓存层。

复制代码

java

复制代码
`// ❌ 写操作:扣减库存------不加任何缓存注解,直接穿透
@PostMapping("/seckill/deduct")
public Result deductStock(@RequestBody SeckillReq req) {
    Long remain = redisLuaUtil.deductStock(req.getSkuId(), req.getUserId(), req.getCount());
    writeFlowTable(req);
    mqProducer.send(new StockDeductedEvent(req));
    return Result.ok("排队中");
}

// ✅ 读操作:查询库存------双层缓存注解
@GetMapping("/stock/{skuId}")
@CaffeineCache(name = "stock", key = "#skuId", expireAfterWrite = 3, timeUnit = TimeUnit.SECONDS)
@RedisCache(name = "stock", key = "#skuId", expire = 5, timeUnit = TimeUnit.SECONDS)
public Result<Integer> getStock(@PathVariable Long skuId) {
    String stock = redisTemplate.opsForValue().get("seckill:stock:" + skuId);
    if (stock == null) {
        stock = String.valueOf(stockMapper.selectById(skuId));
        redisTemplate.opsForValue().set("seckill:stock:" + skuId, stock, 5, TimeUnit.MINUTES);
    }
    return Result.success(Integer.parseInt(stock));
}
`

三、技术选型与依赖配置

基于 Spring Boot 3.2.5 + JDK 17 的生产级技术栈:

组件 版本 选型理由
Caffeine 3.1.8 W-TinyLFU算法,命中率比Guava高15%~20%
Redis 7.2.4 IO多路复用+集群模式,单节点10万QPS
MySQL 8.0.36 事务+MVCC,ACID兜底
MyBatis-Plus 3.5.5.1 简化CRUD,聚焦业务逻辑
Lettuce 默认 Spring Boot 2.x起默认Redis客户端,支持异步
复制代码

xml

复制代码
`<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5.1</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
        <version>3.1.8</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
`

四、核心代码:装饰器模式实现多级缓存

别用继承------会炸出DBQuery → RedisDBQuery → CaffeineRedisDBQuery的类爆炸。用装饰器模式,动态组合,优雅扩展。

4.1 抽象组件

复制代码

java

复制代码
`public interface DataProvider {
    Object getData(String key);
}
`

4.2 具体组件:数据库查询

复制代码

java

复制代码
`@Component
public class MySQLDataProvider implements DataProvider {
    @Autowired private UserMapper userMapper;
    
    @Override
    public Object getData(String key) {
        Long userId = Long.parseLong(key.split(":")[1]);
        return userMapper.selectById(userId);
    }
}
`

4.3 装饰器:Redis缓存层

复制代码

java

复制代码
`@Component
public class RedisCacheDecorator implements DataProvider {
    @Autowired private RedisTemplate<String, Object> redisTemplate;
    @Autowired private DataProvider delegate;
    
    @Override
    public Object getData(String key) {
        Object cached = redisTemplate.opsForValue().get(key);
        if (cached != null) {
            log.info("Redis hit: {}", key);
            return cached;
        }
        Object dbData = delegate.getData(key);
        if (dbData != null) {
            redisTemplate.opsForValue().set(key, dbData, 30, TimeUnit.MINUTES);
        }
        return dbData;
    }
}
`

4.4 装饰器:Caffeine本地缓存层

复制代码

java

复制代码
`@Component
public class CaffeineCacheDecorator implements DataProvider {
    private final Cache<String, Object> cache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(30, TimeUnit.SECONDS)
        .recordStats()
        .build();
    
    @Autowired private DataProvider delegate;
    
    @Override
    public Object getData(String key) {
        Object cached = cache.getIfPresent(key);
        if (cached != null) {
            log.info("Caffeine hit: {}", key);
            return cached;
        }
        Object data = delegate.getData(key);
        if (data != null) {
            cache.put(key, data);
        }
        return data;
    }
}
`

4.5 组合:构建缓存链

复制代码

java

复制代码
`@Configuration
public class MultiLevelCacheConfig {
    @Autowired private MySQLDataProvider mysqlProvider;
    @Autowired private RedisCacheDecorator redisDecorator;
    @Autowired private CaffeineCacheDecorator caffeineDecorator;
    
    @Bean
    public DataProvider multiLevelCache() {
        // CaffeineDecorator(RedisDecorator(MySQLDataProvider))
        return new CaffeineCacheDecorator() {``{
            // 通过Setter注入delegate
        }};
    }
}
`

更优雅的方式:Spring Cache抽象 + 自定义CacheManager

复制代码

java

复制代码
`@Configuration
@EnableCaching
public class CacheConfig implements CachingConfigurer {
    
    @Bean
    @Override
    public CacheManager cacheManager() {
        MultiLevelCacheManager manager = new MultiLevelCacheManager(
            caffeineCacheManager(),   // L1
            redisCacheManager()       // L2
        );
        return manager;
    }
    
    @Bean
    public CacheManager caffeineCacheManager() {
        CaffeineCacheManager cm = new CaffeineCacheManager();
        cm.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(30, TimeUnit.SECONDS)
            .build());
        return cm;
    }
    
    @Bean
    public CacheManager redisCacheManager() {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(5))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer())
            );
        return RedisCacheManager.builder(redisConnectionFactory())
            .cacheDefaults(config)
            .build();
    }
}
`

使用时只需一个注解:

复制代码

java

复制代码
`@Service
public class UserService {
    
    @Cacheable(value = {"users"}, key = "#id")  // 自动走 Caffeine → Redis → MySQL
    public User getUser(Long id) {
        return userMapper.selectById(id);
    }
    
    @CacheEvict(value = {"users"}, key = "#user.id")  // 自动删除 Redis + Caffeine
    public void updateUser(User user) {
        userMapper.updateById(user);
    }
}
`

五、数据一致性:这才是三级缓存的生死线

三级缓存最致命的问题不是性能,是数据不一致。一个节点更新了价格,其他节点的Caffeine里还是旧价格------用户看到的是脏数据。

5.1 写操作:Cache Aside + 延迟双删

复制代码

java

复制代码
`@Transactional
public void updateUser(User user) {
    // ① 先更新数据库
    userMapper.updateById(user);
    
    // ② 删除Redis(不是更新!)
    redisTemplate.delete("user:" + user.getId());
    
    // ③ 延迟双删:等其他节点从DB读完旧数据后,再删一次
    try {
        Thread.sleep(1000);  // 延迟 > 业务接口平均耗时
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    redisTemplate.delete("user:" + user.getId());
    
    // ④ 发送消息,通知所有节点清理本地Caffeine
    rocketMQTemplate.send("cache-invalidate-topic",
        MessageBuilder.withPayload("user:" + user.getId()).build());
}
`

5.2 消费消息:清理本地缓存

复制代码

java

复制代码
`@Service
@RocketMQMessageListener(topic = "cache-invalidate-topic", consumerGroup = "cache-group")
public class CacheInvalidateConsumer implements RocketMQListener<String> {
    
    @Autowired private CaffeineCache caffeineCache;
    
    @Override
    public void onMessage(String cacheKey) {
        String[] parts = cacheKey.split(":");
        if (parts.length == 2 && "user".equals(parts[0])) {
            Long userId = Long.parseLong(parts[1]);
            caffeineCache.invalidate(userId);
            log.info("Invalidated local cache for user: {}", userId);
        }
    }
}
`

5.3 架构总览

复制代码
复制代码
`更新请求
  │
  ▼
更新MySQL ──→ 删除Redis ──→ 延迟1s ──→ 再次删除Redis
  │                              │
  │                              ▼
  │                    发送RocketMQ消息
  │                              │
  ▼                              ▼
写流水表              所有服务节点消费消息 → 清理本地Caffeine
`

六、三大缓存灾难的防护方案

灾难 现象 解决方案
缓存穿透 查询不存在的id,每次打DB 布隆过滤器拦截 + 缓存空值(5分钟短TTL)
缓存击穿 热点Key过期,万请求打DB 互斥锁 + 超级热点Key永不过期(只在更新时删除)
缓存雪崩 大量Key同时过期,DB压力骤增 过期时间加随机值(1h±30min)+ Redis集群高可用 + 熔断降级

布隆过滤器实战

复制代码

java

复制代码
`@Bean
public BloomFilter<Long> userIdBloomFilter() {
    BloomFilter<Long> filter = BloomFilter.create(
        Funnels.longFunnel(),
        10_000_000,  // 预期元素数量
        0.01         // 误判率1%
    );
    // 启动时预热:加载所有用户ID
    userMapper.selectAllIds().forEach(filter::put);
    return filter;
}

public User getUser(Long id) {
    if (!bloomFilter.mightContain(id)) {
        return null;  // 布隆过滤器说不存在,直接返回,不查任何缓存
    }
    // ... 走 Caffeine → Redis → MySQL 流程
}
`

七、缓存预热:别让冷启动成为系统的墓志铭

系统刚启动时,Caffeine和Redis都是空的。十万用户同时进来,所有请求直打MySQL------这不是高并发,这是DDoS。

预热策略

复制代码

java

复制代码
`@Component
public class CacheWarmupRunner implements ApplicationRunner {
    
    @Autowired private UserMapper userMapper;
    @Autowired private RedisTemplate<String, Object> redisTemplate;
    @Autowired private CaffeineCache caffeineCache;
    
    @Override
    public void run(ApplicationArguments args) {
        log.info("🔥 开始缓存预热...");
        
        // 第一层:从MySQL加载Top1000热门数据到Redis
        List<User> hotUsers = userMapper.selectTopHot(1000);
        hotUsers.forEach(u -> 
            redisTemplate.opsForValue().set("user:" + u.getId(), u, 60, TimeUnit.MINUTES)
        );
        
        // 第二层:Top10超级热点加载到每台服务器的Caffeine
        List<User> superHotUsers = userMapper.selectSuperHot(10);
        superHotUsers.forEach(u -> 
            caffeineCache.put(String.valueOf(u.getId()), u)
        );
        
        log.info("✅ 预热完成:Redis {}条,Caffeine {}条", hotUsers.size(), superHotUsers.size());
    }
}
`

八、压测数据说话

在Spring Boot 3.2.5 + JDK 17环境下,对同一课程查询接口压测:

架构 并发 平均RT QPS 数据库QPS
单MySQL 2000 150ms 130 130
MySQL + Redis 2000 8ms 2500 50
MySQL + Redis + Caffeine 2000 0.3ms 6500 12
MySQL + Redis + Caffeine 10000 0.5ms 18000 35
MySQL + Redis + Caffeine 50000 0.8ms 42000 80

数据库QPS从130降到12------降低90%。 这就是三级缓存的威力。


九、最佳实践Checklist

  • 读写分离:写接口不加缓存注解,直接穿透
  • 过期时间分层:Caffeine 30s < Redis 5min < MySQL永久
  • 过期加随机值1h + Random(0~30min),避免雪崩
  • 超级热点永不过期:首页Top10数据只在更新时主动删除
  • 布隆过滤器前置:拦截99%的无效查询
  • 缓存空值:不存在的Key也缓存5分钟,防止穿透
  • 延迟双删 + MQ广播:保证多实例Caffeine一致性
  • 启动预热:Top1000进Redis,Top10进Caffeine
  • 监控埋点:Caffeine.recordStats() + Redis INFO stats,实时看命中率

十、写在最后

三级缓存不是银弹,它是一套分层抗压的哲学------让热点数据尽可能靠近CPU,让无效请求尽可能在门口就被拦住。Caffeine负责纳秒级的最后一公里,Redis负责毫秒级的分布式共享,MySQL负责十毫秒级的最终真相。

当十万用户同时涌来,你的服务不是在"扛",而是在"卸"------90%的请求在本地内存就被消化,Redis只承10%,数据库只看到零星的冷数据查询。

这,才是高并发架构该有的样子。

相关推荐
yuanpan1 小时前
Python 桌面 GUI 入门开发:从 tkinter 窗口到简易记事本
开发语言·python
User_芊芊君子1 小时前
聊聊自由开发者常用的学习机会全解析
开发语言·人工智能·python
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第40题:Java中的深拷贝和浅拷贝有什么区别
java·开发语言·后端·面试
xh didida2 小时前
算法 -- 位运算
开发语言·c++·算法
谙弆悕博士2 小时前
快速学C语言——第2章:编程规范与代码风格
服务器·c语言·开发语言·经验分享·程序人生·学习方法·业界资讯
byzh_rc3 小时前
[AI编程从入门到入土] 装饰器decorator
开发语言·python·ai编程
贫民窟的勇敢爷们3 小时前
Java 与 Python 如何选型与融合
java·开发语言·python
流氓也是种气质 _Cookie3 小时前
Chrome Performance常见名词解释(FP, FCP, LCP, DCL, FMP, TTI, TBT, FID, CLS)
开发语言·javascript·ecmascript
gihigo19983 小时前
基于MATLAB的LTE物理层仿真系统
开发语言·matlab