两级缓存 Caffeine + Redis 架构:原理、实现与实践

一、前言

在高性能服务架构设计里,缓存是关键环节。常规做法是将热点数据存于 Redis/MemCache 等远程缓存,缓存未命中时再查数据库,以此提升访问速度、降低数据库压力 。

随着发展,架构有了改进,部分场景下单纯远程缓存不够,需结合本地缓存(如 Guava cache、Caffeine ),形成本地缓存(一级缓存) + 远程缓存(二级缓存)的两级缓存架构,进一步提升程序响应与服务性能,其基础访问流程如下(暂不考虑并发等复杂问题):

此处插入原流程图片,替换路径为合理展示形式,如保留`./assets/2.jpeg`,若为线上展示需处理为可访问链接

二、优点与待解决问题

(一)优势

相比单纯远程缓存,两级缓存有以下优势:

  • 访问速度快:本地缓存基于内存,对变更 / 实时性要求低的数据,存入本地可大幅提升访问速度。
  • 减少网络开销:降低与远程缓存的数据交互,减少网络 I/O 耗时,节省网络通信成本。

(二)需解决问题

设计时要考虑诸多问题,核心是数据一致性

  • 两级缓存与数据库需同步,数据修改时,本地、远程缓存应同步更新。
  • 分布式环境下,一级缓存节点间存在一致性问题。一个节点修改本地缓存后,需通知其他节点刷新,否则会读过期数据,可借 Redis 发布 / 订阅功能解决。

此外,缓存过期时间、策略及多线程访问等问题也需关注,本文先聚焦两级缓存的代码实现。

三、准备工作

(一)技术选型

整合Caffeine(最强本地缓存) 为一级缓存、Redis(性能优) 为二级缓存,搭建 SpringBoot 项目,引入依赖:

XML 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.8.1</version>
</dependency>

(二)Redis 配置

配置RedisTemplate ,处理连接工厂、序列化等:

java 复制代码
/**
 * Redis缓存配置类
 * @author ZhuZiKai
 * @date 2022/3/31 0014
 */
@Configuration
@EnableCaching
@EnableAspectJAutoProxy 
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public FastJsonRedisSerializer fastJson2JsonRedisSerializer() {
        return new FastJsonRedisSerializer(Object.class);
    }

    @Bean
    public StringRedisSerializer StringRedisSerializer() {
        return new StringRedisSerializer();
    }

    @Bean("redis")
    @Primary
    public RedisTemplate initRedisTemplate(RedisConnectionFactory redisConnectionFactory,
                                           StringRedisSerializer stringRedisSerializer,
                                           FastJsonRedisSerializer fastJson2JsonRedisSerializer) throws Exception {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(fastJson2JsonRedisSerializer);
        redisTemplate.setDefaultSerializer(stringRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

四、代码实战:两级缓存管理实现

(一)V1.0 版本:Spring 缓存注解方案

Spring 提供CacheManager 接口与注解(@Cacheable/@CachePut/@CacheEvict ),简化缓存操作。

1. 注解说明
  • @Cacheable:按 key 查缓存,命中则直接返回;未命中则执行方法,结果入缓存。
  • @CachePut:无论缓存是否存在,执行方法并强制更新缓存。
  • @CacheEvict:执行方法后,移除对应缓存数据。
2. 配置CacheManager
java 复制代码
@Configuration
public class CacheManagerConfig {
    @Bean
    public CacheManager cacheManager(){
        CaffeineCacheManager cacheManager=new CaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(128)
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.SECONDS));
        return cacheManager;
    }
}

启动类添加@EnableCaching ,开启缓存支持。

3. Service 层改造
  • 查询方法 :用@Cacheable ,保留业务与 Redis 操作逻辑,Caffeine 缓存交 Spring 管理:
java 复制代码
@Cacheable(value = "order",key = "#id")
public Order getOrderById(Long id) {
    String key= CacheConstant.ORDER + id;
    // 查Redis
    Object obj = redisTemplate.opsForValue().get(key);
    if (Objects.nonNull(obj)){
        log.info("get data from redis");
        return (Order) obj;
    }
    // Redis无则查DB
    log.info("get data from database");
    Order myOrder = orderMapper.selectOne(new LambdaQueryWrapper<Order>()
            .eq(Order::getId, id));
    // 结果入Redis
    redisTemplate.opsForValue().set(key,myOrder,120, TimeUnit.SECONDS);
    return myOrder;
}
  • 更新方法 :用@CachePut ,移除手动更新 Caffeine 操作(由 Spring 管理):
java 复制代码
@CachePut(cacheNames = "order",key = "#order.id")
public Order updateOrder(Order order) {
    log.info("update order data");
    orderMapper.updateById(order);
    // 更新Redis
    redisTemplate.opsForValue().set(CacheConstant.ORDER + order.getId(),
            order, 120, TimeUnit.SECONDS);
    return order;
}

注意:方法需定义返回值,否则可能缓存空对象,影响后续查询。

  • 删除方法 :用@CacheEvict ,仅处理 Redis 缓存删除:
java 复制代码
@CacheEvict(cacheNames = "order",key = "#id")
public void deleteOrder(Long id) {
    log.info("delete order");
    orderMapper.deleteById(id);
    redisTemplate.delete(CacheConstant.ORDER + id);
}

此方案将本地缓存交 Spring 管理,Redis 缓存手动操作,降低业务入侵性。

(二)V2.0 版本:自定义注解 + 切面方案

进一步解耦,通过自定义注解与切面,实现对业务无入侵的缓存管理。

1. 自定义注解

定义@DoubleCache ,支持缓存存取、删除,适配 SpringEL 表达式:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {
    String cacheName();
    String key(); // 支持SpringEL
    long l2TimeOut() default 120;
    CacheType type() default CacheType.FULL;
}

// 缓存操作类型枚举
public enum CacheType {
    FULL,   // 存取
    PUT,    // 只存
    DELETE  // 删除
}
2. SpringEL 表达式解析

解析注解中key 的表达式,适配方法参数:

java 复制代码
public static String parse(String elString, TreeMap<String,Object> map){
    elString=String.format("#{%s}",elString);
    ExpressionParser parser = new SpelExpressionParser();
    EvaluationContext context = new StandardEvaluationContext();
    map.entrySet().forEach(entry->
        context.setVariable(entry.getKey(),entry.getValue())
    );
    Expression expression = parser.parseExpression(elString, new TemplateParserContext());
    return expression.getValue(context, String.class);
}
3. 配置 Caffeine 缓存
java 复制代码
@Configuration
public class CaffeineConfig {
    @Bean
    public Cache<String,Object> caffeineCache(){
        return Caffeine.newBuilder()
                .initialCapacity(128)// 初始容量
                .maximumSize(1024)// 最大容量
                .expireAfterWrite(60, TimeUnit.SECONDS)// 过期时间
                .build();
    }
}
4. 切面实现缓存逻辑

通过切面拦截@DoubleCache 注解,统一处理缓存读写、删除:

java 复制代码
@Slf4j 
@Component 
@Aspect 
@AllArgsConstructor
public class CacheAspect {
    private final Cache<String,Object> cache;
    private final RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.cn.dc.annotation.DoubleCache)")
    public void cacheAspect() {}

    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        
        // 解析方法参数,封装SpringEL上下文
        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() + CacheConstant.COLON + elResult;

        // 按操作类型处理
        if (annotation.type()== CacheType.PUT){
            Object object = point.proceed();
            redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
            cache.put(realKey, object);
            return object;
        } else if (annotation.type()== CacheType.DELETE){
            redisTemplate.delete(realKey);
            cache.invalidate(realKey);
            return point.proceed();
        }

        // 读写流程:先查Caffeine
        Object caffeineCache = cache.getIfPresent(realKey);
        if (Objects.nonNull(caffeineCache)) {
            log.info("get data from caffeine");
            return caffeineCache;
        }

        // 再查Redis
        Object redisCache = redisTemplate.opsForValue().get(realKey);
        if (Objects.nonNull(redisCache)) {
            log.info("get data from redis");
            cache.put(realKey, redisCache);
            return redisCache;
        }

        // 都无则查数据库,结果入缓存
        log.info("get data from database");
        Object object = point.proceed();
        if (Objects.nonNull(object)){
            redisTemplate.opsForValue().set(realKey, object,annotation.l2TimeOut(), TimeUnit.SECONDS);
            cache.put(realKey, object);        
        }
        return object;
    }
}
5. Service 层简化

只需添加自定义注解,专注业务逻辑:

java 复制代码
@DoubleCache(cacheName = "order", key = "#id",type = CacheType.FULL)
public Order getOrderById(Long id) {
    return orderMapper.selectOne(new LambdaQueryWrapper<Order>().eq(Order::getId, id));
}

@DoubleCache(cacheName = "order",key = "#order.id",type = CacheType.PUT)
public Order updateOrder(Order order) {
    orderMapper.updateById(order);
    return order;
}

@DoubleCache(cacheName = "order",key = "#id",type = CacheType.DELETE)
public void deleteOrder(Long id) {
    orderMapper.deleteById(id);
}

五、总结与思考

本文按业务入侵程度,介绍两种两级缓存实现:

  • V1.0:借 Spring 缓存注解,半入侵式管理,本地缓存交 Spring,Redis 手动操作。
  • V2.0:自定义注解 + 切面,完全解耦缓存逻辑,业务代码更简洁。

实际项目中,是否用两级缓存需结合业务。若 Redis 满足需求,无需强制引入,因实际使用涉及并发、事务回滚、缓存策略适配等复杂问题。需权衡数据特性(如哪些适合一级 / 二级缓存)、一致性保障成本,再决定架构方案,让缓存真正成为服务性能的 "助推器" 而非 "负担" 。

相关推荐
暮乘白帝过重山3 分钟前
为什么要写RedisUtil这个类
redis·开发·暮乘白帝过重山
数据狐(DataFox)23 分钟前
SQL参数化查询:防注入与计划缓存的双重优势
数据库·sql·缓存
森焱森39 分钟前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
大只鹅1 小时前
Springboot3整合ehcache3缓存--XML配置和编程式配置
spring boot·缓存
go54631584652 小时前
修改Spatial-MLLM项目,使其专注于无人机航拍视频的空间理解
人工智能·算法·机器学习·架构·音视频·无人机
持之以恒的天秤3 小时前
Redis—哨兵模式
redis·缓存
凌辰揽月4 小时前
8分钟讲完 Tomcat架构及工作原理
java·架构·tomcat
绝无仅有4 小时前
对接三方SDK开发过程中的问题排查与解决
后端·面试·架构
搬砖的小码农_Sky4 小时前
XILINX Ultrascale+ Kintex系列FPGA的架构
fpga开发·架构
芥子沫4 小时前
Redis 持久化详解、使用及注意事项
redis·内存数据库