微服务多级缓存架构设计

1 需求背景

系统读多写少场景,大量热点字典、基础业务信息,请求全部打到 Redis,Redis CPU / 带宽压力高。 引入本地内存缓存,缩短访问链路;同时解决多实例本地缓存脏数据问题。

非目标

  1. 不用于强一致性业务(库存、余额),这类数据禁止使用 L1 本地缓存
  2. 不替代数据库,只做查询加速

2 总体架构

2.1 层级定义

  • L1:Caffeine JVM 堆内存缓存,纳秒级读取,单机隔离;弱一致性,TTL 兜底;热点只读数据
  • L2:Redis 分布式缓存,毫秒级,多实例共享
  • L3:MySQL,源数据

2.2 读写流程

读流程

  1. 查询 L1 本地缓存,命中直接返回
  2. L1 未命中,查询 Redis;命中则回填 L1 并返回
  3. Redis 未命中,查询数据库;DB 结果回填 Redis+L1,返回结果
  4. DB 不存在数据,写入空标记到 Redis,短 TTL,防止缓存穿透

写流程(Cache Aside)

  1. 更新数据库
  2. 删除 Redis 对应 key
  3. Redis 发布失效广播消息
  4. 所有应用实例收到消息,删除本机 L1 缓存

原则:更新 DB,删缓存,不更新缓存。

2.3 一致性策略

  • 一致性模型:最终一致性,允许短暂脏数据
  • 保障手段:失效广播 + L1 短 TTL 兜底;消息丢失时,脏数据最多存活 L1 TTL 时长
  • 强一致业务:禁用 L1 本地缓存,只使用 Redis

3 缓存数据分类与准入规范

表格

数据类型 是否启用 L1 示例
静态基础字典 ✅ 地区、枚举、配置
低频变更热点业务数据 ✅ 商品基础信息
高频变更业务数据 ❌ 订单状态、库存
冷数据 ❌ 历史归档数据

4 缓存策略设计

4.1 TTL 设计

  • L1 Caffeine:写入过期,推荐 2~5min
  • Redis:基础 TTL + 随机偏移,防止雪崩;推荐 30min + 0~300s 随机
  • 空值缓存:短 TTL,30s

4.2 缓存问题防护方案

  1. 缓存穿透:DB 不存在数据写入空标记;超大 key 集合场景增加布隆过滤器
  2. 缓存击穿:热点 key 采用后台异步刷新;或使用 Redis 分布式锁,单请求加载 DB
  3. 缓存雪崩:TTL 随机;Redis 集群;服务熔断降级;本地缓存容量上限防 OOM

4.3 本地缓存失效方案

  • 方案:Redis Pub/Sub 广播失效消息
  • 缺点:PubSub 消息不可靠,消息丢失会产生脏数据;必须搭配 L1 短 TTL 兜底
  • 高可靠场景:替换为 RocketMQ 消息做失效通知

5 组件设计

5.1 MultiCacheManager 核心组件

能力:多级 get、缓存失效 evict、缓存统计埋点、本地缓存开关降级

不直接复用 Spring Cache 注解,自定义封装,原生 Spring Cache 不支持多级联动失效广播。

5.2 缓存 Key 规范

格式:cache:{业务域}:{key} 示例:cache:user:1

5.3 序列化方案

使用 FastJSON2 序列化对象,禁止 JDK 序列化。

6 监控指标设计

表格

指标 说明 告警阈值
caffeine.hitRate L1 缓存命中率 低于 70% 告警
redis.hitRate Redis 缓存命中率 低于 60% 告警
cache.pass.through.count 缓存穿透次数 突增告警
cache.invalid.msg.fail 失效消息消费失败 >0 告警
redis.cpu Redis CPU 使用率 >80% 告警

7 实施与上线流程

  1. 业务梳理:识别热点数据,划分是否允许 L1 缓存
  2. 代码开发:接入 MultiCacheManager;埋点监控
  3. 单元测试:验证命中、失效、穿透场景
  4. 压测:高并发验证击穿、雪崩
  5. 灰度上线:小流量开启;配置开关可一键关闭 L1
  6. 全量上线,持续监控指标

8 风险与应急预案

风险清单

  1. 本地缓存脏数据:PubSub 消息丢失;预案:配置中心一键关闭 L1 本地缓存,降级为纯 Redis
  2. Caffeine 内存溢出:预案:强制配置 maximumSize,限制缓存条目上限
  3. Redis 故障:预案:熔断,直接访问 DB(限流保护)
  4. 大 key:禁止大对象存入缓存;大 key 拆分

9 使用约束

  1. 高频更新数据禁止开启 L1 本地缓存
  2. 强一致性业务禁用 L1
  3. L1 缓存必须设置 maxSize,防止 OOM
  4. 不使用缓存更新策略,统一删缓存
  5. L1 TTL 不能设置过长

10 后续优化方向

  1. 热点 key 异步刷新
  2. 布隆过滤器接入,拦截不存在 key
  3. MQ 替换 PubSub,实现可靠失效通知
  4. 接入 Prometheus 指标埋点
  5. 缓存预热组件

1. SpringBoot 完整可运行多级缓存 Demo

技术栈:SpringBoot 2.7.x/ 3.x,Caffeine,Spring Data Redis,Redisson(可选分布式锁),Redis PubSub 做本地缓存失效广播 能力:L1=Caffeine 本地缓存,L2=Redis 分布式缓存;Cache Aside;空值防穿透;缓存埋点;开关控制本地缓存;Redis PubSub 失效广播;可一键关闭 L1 降级。 注意:SpringBoot3 需要使用 jakarta 包,下面代码默认 SpringBoot2,按需替换。

Maven pom.xml

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath/>
    </parent>
    <groupId>com.cache</groupId>
    <artifactId>multi-level-cache-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>multi-level-cache-demo</name>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.benmanes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>3.1.8</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.32</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

application.yml

复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0
multi-cache:
  # 本地缓存开关,生产故障时一键关闭L1
  enable-local-cache: true
  # Caffeine 最大缓存条目
  caffeine-max-size: 10000
  # Caffeine 写入后过期时间,单位秒
  caffeine-expire-write: 120
  # Redis基础过期时间,单位秒
  redis-base-ttl: 1800
  # Redis随机偏移,防止雪崩,单位秒
  redis-random-ttl: 300
  # 空值缓存TTL,防穿透
  null-cache-ttl: 30
cache-topic: multi:cache:invalidate:topic

配置类:MultiCacheConfig.java

复制代码
package com.cache.config;

import com.alibaba.fastjson2.JSON;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.concurrent.TimeUnit;

@Slf4j
@Configuration
public class MultiCacheConfig {

    @Value("${multi-cache.enable-local-cache}")
    private boolean enableLocalCache;

    @Value("${multi-cache.caffeine-max-size}")
    private long caffeineMaxSize;

    @Value("${multi-cache.caffeine-expire-write}")
    private long caffeineExpireWrite;

    @Value("${cache-topic}")
    private String cacheInvalidTopic;

    /**
     * Caffeine L1本地缓存
     */
    @Bean
    public com.github.benmanes.caffeine.cache.Caffeine<Object, Object> caffeineBuilder() {
        return Caffeine.newBuilder()
                .maximumSize(caffeineMaxSize)
                .expireAfterWrite(caffeineExpireWrite, TimeUnit.SECONDS)
                .recordStats();
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setValueSerializer(stringSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * Redis Pub/Sub 消息容器,监听缓存失效消息
     */
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory,
                                                                         MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(listenerAdapter, new ChannelTopic(cacheInvalidTopic));
        return container;
    }

    @Bean
    public MessageListenerAdapter messageListenerAdapter(CacheInvalidMessageHandler messageHandler) {
        return new MessageListenerAdapter(messageHandler, "onMessage");
    }
}

失效消息处理器:CacheInvalidMessageHandler.java

复制代码
package com.cache.config;

import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class CacheInvalidMessageHandler {

    @Autowired
    private Cache<Object, Object> caffeineCache;

    /**
     * 收到广播消息,清除本机L1缓存
     */
    public void onMessage(String key) {
        log.info("收到缓存失效广播,清除本地缓存key:{}", key);
        caffeineCache.invalidate(key);
    }
}

多级缓存核心工具类:MultiCacheManager.java

复制代码
package com.cache.manager;

import com.alibaba.fastjson2.JSON;
import com.github.benmanes.caffeine.cache.Cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

@Slf4j
@Component
public class MultiCacheManager {

    private static final String NULL_FLAG = "NULL_CACHE";

    @Autowired
    private Cache<Object, Object> caffeineCache;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Value("${multi-cache.enable-local-cache}")
    private boolean enableLocalCache;

    @Value("${multi-cache.redis-base-ttl}")
    private long redisBaseTtl;

    @Value("${multi-cache.redis-random-ttl}")
    private long redisRandomTtl;

    @Value("${multi-cache.null-cache-ttl}")
    private long nullCacheTtl;

    @Value("${cache-topic}")
    private String cacheInvalidTopic;

    private final Random random = new Random();

    /**
     * 多级缓存查询
     * @param key 缓存key
     * @param clazz 返回对象类型
     * @param dbLoader 数据库查询逻辑
     * @return 业务对象
     */
    public <T> T get(String key, Class<T> clazz, Supplier<T> dbLoader) {
        //1.优先读取L1本地缓存
        if (enableLocalCache) {
            Object localVal = caffeineCache.getIfPresent(key);
            if (localVal != null) {
                log.debug("L1缓存命中 key={}", key);
                return (T) localVal;
            }
        }

        //2.读取Redis L2缓存
        String redisStr = redisTemplate.opsForValue().get(key);
        if (redisStr != null) {
            if (NULL_FLAG.equals(redisStr)) {
                log.debug("Redis命中空缓存 key={}", key);
                return null;
            }
            T val = JSON.parseObject(redisStr, clazz);
            //回填本地缓存
            if (enableLocalCache) {
                caffeineCache.put(key, val);
            }
            log.debug("Redis缓存命中 key={}", key);
            return val;
        }

        log.debug("缓存全部未命中,查询DB key={}", key);
        //3.查询数据库
        T dbData = dbLoader.get();
        if (dbData == null) {
            //写入空值,防穿透
            redisTemplate.opsForValue().set(key, NULL_FLAG, nullCacheTtl, TimeUnit.SECONDS);
            return null;
        }

        //4.回填Redis,增加随机TTL,防止雪崩
        long realTtl = redisBaseTtl + random.nextLong(redisRandomTtl);
        redisTemplate.opsForValue().set(key, JSON.toJSONString(dbData), realTtl, TimeUnit.SECONDS);
        //回填本地缓存
        if (enableLocalCache) {
            caffeineCache.put(key, dbData);
        }
        return dbData;
    }

    /**
     * 更新/删除数据后,失效缓存
     * @param key 缓存key
     */
    public void evict(String key) {
        // 删除Redis缓存
        redisTemplate.delete(key);
        // 删除本机本地缓存
        if (enableLocalCache) {
            caffeineCache.invalidate(key);
        }
        //广播失效消息,通知其他应用实例清本地缓存
        redisTemplate.convertAndSend(cacheInvalidTopic, key);
        log.info("缓存失效完成,广播消息 key={}", key);
    }

    /**
     * 获取缓存统计信息,用于监控
     */
    public String getCacheStats() {
        if (!enableLocalCache) {
            return "local cache disabled";
        }
        return caffeineCache.stats().toString();
    }
}

测试实体:User.java

复制代码
package com.cache.entity;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String phone;
}

Service 测试层:UserService.java

复制代码
package com.cache.service;

import com.cache.entity.User;
import com.cache.manager.MultiCacheManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {

    private final MultiCacheManager multiCacheManager;

    //模拟DB
    public User getById(Long id) {
        String cacheKey = "cache:user:" + id;
        return multiCacheManager.get(cacheKey, User.class, () -> {
            //模拟数据库查询逻辑
            if (id == 1L) {
                User user = new User();
                user.setId(1L);
                user.setUsername("test");
                user.setPhone("13800138000");
                return user;
            }
            return null;
        });
    }

    public void updateUser(Long id) {
        //1.更新DB(省略)
        //2.失效缓存
        String cacheKey = "cache:user:" + id;
        multiCacheManager.evict(cacheKey);
    }
}

Controller 测试入口:CacheTestController.java

复制代码
package com.cache.controller;

import com.cache.entity.User;
import com.cache.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class CacheTestController {

    private final UserService userService;

    @GetMapping("/user/get")
    public User getUser(@RequestParam Long id) {
        return userService.getById(id);
    }

    @PostMapping("/user/update")
    public String updateUser(@RequestParam Long id) {
        userService.updateUser(id);
        return "ok";
    }

    @GetMapping("/cache/stats")
    public String stats() {
        return userService.getCacheStats();
    }
}

Demo 说明:

  1. 多实例启动时,更新接口触发 evict,Redis 发布消息,所有实例收到消息清除本机 Caffeine
  2. enable-local-cache 配置项可以关闭 L1,降级为纯 Redis 缓存
  3. 空值缓存防止穿透;Redis TTL 随机防雪崩
  4. 没有内置分布式锁;热点 key 击穿场景,可自行在 get 方法增加 Redisson 锁
相关推荐
闻哥2 小时前
分布式任务调度框架:XXL-Job / ElasticJob / DolphinScheduler / SchedulerX 架构对比与选型
分布式·架构·wpf
天远API2 小时前
零信任架构实战:基于天远风控经营异常预警构建自动化供应商店铺巡检网关
人工智能·架构·自动化·dubbo
AI行业应用研究2 小时前
会务信息载体的演进:纸质、PDF 与结构化数据源的分发成本与版本一致性
大数据·人工智能·安全·小程序·架构·pdf
JPower_mr.g2 小时前
SmartCall 音色管理技术解析:基于 SPI 的可扩展音色注册架构
java·开发语言·人工智能·ai·架构·开源
我不是阵雨3 小时前
微服务幂等性深度解剖
微服务·云原生·架构
七夜zippoe3 小时前
Agent 中间件架构:钩子链、插件注册与横切治理
ai·中间件·架构·agent·钩子链
欣欣之王来了3 小时前
开发环境准备:Node.js、npm、VS Code安装配置
前端·学习·架构·项目·vue3教程
Dawson Zhu3 小时前
《Agentic Design Patterns》第 11 章导读:目标设定与监控(Goal Setting and Monitoring)
人工智能·语言模型·架构·aigc·agi
Wang's Blog3 小时前
Java 项目部署之 Docker工具快速入门: Docker 架构拆解:镜像、容器、守护进程与 Registry
java·docker·架构