开发者

Redis+Caffeine实现高效两级缓存架构的详细指南

目录
  • 引言
  • 两级缓存架构概述
  • 两级缓存的优势
    • 性能优势
    • 系统稳定性
  • Caffeine简介
    • 版本演进
      • 版本1:直接侵入Service代码
      • 版本2:使用Spring Cache注解
      • 版本3:自定义注解+AOP实现
    • 关键配置
      • Caffeine 配置
      • Redis 配置
    • 性能优化建议
      • 总结

        引言

        在现代高并发系统中,缓存是提升系统性能的关键组件之一。传统的单一缓存方案往往难以同时满足高性能和高可用性的需求。本文将介绍如何结合 Redis 和 Caffeine 构建一个高效的两级缓存系统,并通过三个版本的演进展示如何逐步优化代码结构。

        项目源代码:github地址、gitee地址

        两级缓存架构概述

        两级缓存通常由本地缓存(如 Caffeine)和分布式缓存(如 Redis)组成:

        • 本地缓存(Caffeine):基于内存,访问速度极快,但容量有限且无法跨进程共享
        • 分布式缓存(Redis):可跨进程共享,容量更大,但访问速度相对较慢

        通过结合两者优势,我们可以构建一个既快速又具备一致性的缓存系统。

        两级缓存的优势

        性能优势

        缓存类型平均延迟延迟波动范围
        本地缓存0.05-1ms稳定
        远程缓存1-10ms受网络影响大
        数据库查询10-100ms取决于SQL复杂度

        典型案例:某电商平台商品详情页采用两级缓存后:

        • 单纯Redis方案:P99响应时间8ms
        • 两级缓存方案:P99响应时间降至2ms

        本地缓存的延迟是最低的,远远低于redis等远程缓存,而且本地缓存不受网络的影响,所以延迟的波动范围也是最稳定的。所以,二级缓存在性能上有极大的优势。

        系统稳定性

        1.抗流量洪峰能力

        假如电商环境中出现了秒杀场景,或者促销活动。会有大量的访问到同一个商品或者优惠券,以下是两种情景:

        纯Redis方案,所有请求直达Redis,容易导致:

        • 连接池耗尽
        • 带宽被打满
        • Redis CPU飙升

        两级缓存方案:

        • 80%以上请求被本地缓存拦截
        • Redis负载降低5-10倍
        • 系统整体更平稳

        2.故障容忍度

        由于Redis等远程缓存需要通过网络连接,如果网络出现异常,很容易出现访问不到数据的情况。本地缓存则不存在网络问题,所以对故障的容忍度是非常高的。

        网络分区场景测试

        模拟 机房网络抖动(丢包率30%):

        • 纯Redis方案:错误率飙升到85%
        • 两级缓存方案:核心接口仍保持92%成功率

        Caffeine简介

        Caffeine 是一个高性能的 Java 本地缓存库,可以理解为 Java 版的"内存临时储物柜"。它的核心特点可以用日常生活中的例子来理解:

        就像一个智能的文件柜:

        • 自动整理 - 会自己清理不常用的文件(基于大小或时间)
        • 快速查找 - 比去档案室(数据库)找资料快100倍
        • 空间管理 - 只保留最常用的1000份文件(可配置)

        技术特点:

        基于 Google Guava 缓存改进而来

        读写性能接近 HashMap(O(1)时间复杂度)

        提供多种淘汰策略:

        // 按数量淘汰(保留最近使用的1000个)
        Caffeine.newBuilder().maximumSize(1000)
        
        // 按时间淘汰(数据保存1小时)
        Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.HOURS)
        

        典型使用场景:

        // 创建缓存(相当于准备一个储物柜)
        Cache<String, User> cache = Caffeine.newBuilder()
            .maximumSize(100)  // 最多存100个用户
            .expireAfterWrite(10, TimeUnit.MINUTES) // 10分钟不用就清理
            .build();
        
        // 存数据(往柜子里放东西)
        cache.put("user101", new User("张三"));
        
        // 取数据(从柜子拿东西)
        User user = cache.getIfPresent("user101");
        
        // 取不到时自动加载(柜子没有就去仓库找)
        User user = cache.get("u编程客栈ser101", key -> userDao.getUser(key));
        

        优势对比:

        • 比 HashMap:支持自动清理和过期
        • 比 Redis:快100倍(无需网络IO)
        • 比 Guava Cache:内存效率更高,并发性能更好

        注意事项:

        • 仅适用于单机(不同服务器间的缓存不共享)
        • 适合缓存不易变的数据(如系统配置)
        • JVM重启后数据会丢失(如需持久化需配合Redis)

        版本演进

        版本1:直接侵入Service代码

        在第一个版本中,我们直接在 Service 层实现了两级缓存逻辑:

        @Override
        public Order getOrderById(Integer id) {
            String key = CacheConstant.ORDER + id;
            return (Order) orderCache.get(key, k -> {
                // 先查询 Redis
                Object obj = redisTemplate.opsForValue().get(key);
                if (obj != null) {
                    log.info("get data from redis");
                    if (obj instanceof Order) {
                        return (Order) obj;
                    } else {
                        log.warn("Unexpected type from Redis, expected Order but got {}", obj.getClass());
                    }
                }
        
                // Redis没有或类型不匹配则查询 DB
                log.info("get data from database");
                Order myOrder = orderMapper.getOrderById(id);
                redisTemplate.opsForValue().set(key, myOrder, 120, TimeUnit.SECONDS);
                return myOrder;
            });
        }
        

        优点

        • 实现简单直接
        • 缓存逻辑清晰可见

        缺点

        • 缓存代码与业务代码高度耦合
        • 难以复用缓存逻辑
        • 代码重复率高

        版本2:使用Spring Cache注解

        在spring项目中,提供了CacheManager接口和一些注解,允许让我们通过注解的方式来操作缓存。先来看一下常用的几个注解说明:

        1.@Cacheable- 缓存查询

        作用:将方法的返回值缓存起来,下次调用时直接返回缓存数据,避免重复计算或查询数据库。

        适用场景

        • 查询方法(如 getUserByIdfindProduct
        • 计算结果稳定的方法

        示例

        @Cacheable(value = "users", key = "#userId")  
        public User getUserById(Long userId) {
            // 如果缓存中没有,才执行此方法
            return userRepository.findById(userId).orElse(null);
        }
        

        参数说明

        • value / cacheNames:缓存名称(如 "users"
        • key:缓存键(支持 SpEL 表达式,如 #userId
        • condition:条件缓存(如 condition = "#userId > 100"
        • unless:排除某些返回值(如 unless = "#result == null"

        2.@CachePut- 更新缓存

        作用:方法执行后,更新缓存(通常用于 insertupdate 操作)。

        适用场景

        • 新增或修改数据后同步缓存
        • 避免缓存与数据库不一致

        示例

        @CachePut(value = "users", key = "#user.id")  
        public User updateUser(User user) {
            return userRepository.save(user); // 更新数据库后,自动更新缓存
        }
        

        注意

        @Cacheable 不同,@CachePut 一定会执行方法,并更新缓存。

        3.@CacheEvict- 删除缓存

        作用:方法执行后,删除缓存(适用于 delete 操作)。

        适用场景

        • 数据删除后清理缓存
        • 缓存失效策略

        示例

        @CacheEvict(value = "users", key = "#userId")  
        public void deleteUser(Long userId) {
            userRepository.deleteById(userId); // 删除数据库数据后,自动删除缓存
        }
        

        参数扩展

        • allEntries = true:清空整个缓存(如 @CacheEvict(value = "users", allEntries = true)
        • beforeInvocation = true:在方法执行前删除缓存(避免方法异常导致缓存未清理)

        第二个版本利用了 Spring 的缓存注解来简化代码,如果要使用上面这几个注解管理缓存的话,我们就不需要配置V1版本中的那个类型为Cache的Bean了,而是需要配置spring中的CacheManager的相关参数,具体参数的配置和之前一样。

        注意,在改进更新操作的时,这里和V1版本的代码有一点区别,在之前的更新操作方法中,是没有返回值的void类型,但是这里需要修改返回值的类型,否则会缓存一个空对象到缓存中对应的key上。当下次执行查询操作时,会直接返回空对象给调用方,而不会执行方法中查询数据库或Redis的操作。

        @Cacheable(value = "orderhttp://www.devze.com", key = "#id")
        @Override
        public Order getOrderById(Integer id) {
            String key = CacheConstant.ORDER + id;
            // 先查询 Redis
            Object obj = redisTemplate.opsForValue().get(key);
            if (obj != null) {
                log.info("get data from redis");
                if (obj instanceof Order) {
                    return (Order) obj;
                } else {
                    log.warn("Unexpected type from Redis, expected Order but got {}", obj.getClass());
                }
            }
        
            // Redis没有或类型不匹配则查询 DB
            log.info("get data from database");
            Order myOrder = orderMapper.getOrderById(id);
            redisTemplate.opsForValue().set(key, myOrder, 120, TimeUnit.SECONDS);
            return myOrder;
        }
        
        @Override
        @CachePut(cacheNames = "order",key = "#order.id")
        public Order updateOrder(Order order) {
            log.info("update order data");
            orderMapper.updateOrderById(order);
            //修改 Redis
            redisTemplate.opsForValue().set(CacheConstant.ORDER + order.getId(),编程
                                            order, 120, TimeUnit.SECONDS);
        
            return order;
        }
        
        @Override
        @CacheEvict(cacheNames = "order",key = "#id")
        public void deleteOrderById(Integer id) {
            log.info("delete order");
            orderMapper.deleteOrderById(id);
            redisTemplate.delete(CacheConstant.ORDER + id);
        }
        

        改进点

        • 使用 @Cacheable 注解管理 Caffeine 缓存
        • 减少了部分重复代码
        • 缓存配置更加集中

        遗留问题

        • Redis 操作仍需手动编写
        • 两级缓存的同步逻辑仍需在业务代码中处理

        版本3:自定义注解+AOP实现

        如果单纯只是使用Cache注解进行缓存,还是无法把Redis功能实现从server模块中剥离出去。如果按照spring对cache注解的思路,我们可以自定义注解再利用AOP切片操作,把对应的缓存功能切入到service的代码中,就能实现二者之间的解耦。

        首先,需要定义一个注解:

        /**
         * 双缓存注解,用于标记需要使用双缓存(通常为本地缓存和远程缓存)的方法
         */
        @Target(ElemenjavascripttType.METHOD)
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        public @interface DoubleCache {
            /**
             * 指定缓存的名称
             * @return 缓存名称
             */
            String cacheName();
        
            /**
             * 指定缓存的键,支持Spring EL表达式
             * @return 缓存键
             */
            String key(); //支持springEl表达式
        
            /**
             * 指定二级缓存的超时时间,单位默认根据实现确定(通常为秒)
             * 默认值为120
             * @return 二级缓存超时时间
             */
            long l2TimeOut() default 120;
        
            /**
             * 指定缓存类型
             * 默认值为 CacheType.FULL
             * @return 缓存类型
             */
            CacheType type() default CacheType.FULL;
        }
        

        定义一个枚举类型的变量,表示缓存操作的类型:

        public enum CacheType {
            FULL,   //存取
            PUT,    //只存
            DELETE  //删除
        www.devze.com}
        

        如果要支持springEL的表达式,还需要一个工具类来解析springEI的表达式:

        public class SpelExpressionUtils {
        
            /**
             * 解析 SpEL 表达式并替换变量
             * @param elString 表达式(如 "user.name")
             * @param map 变量键值对
             * @return 解析后的字符串
             */
            public static String parse(String elString, TreeMap<String, Object> map) {
                // 将输入的表达式包装为 SpEL 表达式格式
                elString = String.format("#{%s}", elString);
                // 创建 SpEL 表达式解析器
                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);
            }
        }
        

        定义切片,在切片操作中来实现Caffeine和Redis的缓存操作:

        @Slf4j
        @Component
        @ASPect
        @AllArgsConstructor
        public class CacheAspect {
        
            private final Cache<String, Object> cache;
            private final RedisTemplate<String, Object> redisTemplate;
        
            /**
             * 定义切点,匹配使用了 @DoubleCache 注解的方法
             */
            @Pointcut("@annotation(com.example.redis_caffeine.annonation.DoubleCache)")
            public void cacheAspect() {}
        
            /**
             * 环绕通知,处理缓存的读写、更新和删除操作
             * 
             * @param point 切入点对象,包含方法执行的相关信息
             * @return 方法执行的返回结果
             * @throws Throwable 方法执行过程中可能抛出的异常
             */
            @Around("cacheAspect()")
            public Object doAround(ProceedingJoinPoint point) throws Throwable {
                try {
                    // 获取方法签名和方法对象
                    MethodSignature signature = (MethodSignature) point.getSignature();
                    Method method = signature.getMethod();
        
                    // 解析参数,将参数名和参数值存入 TreeMap 中
                    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 注解
                    DoubleCache annotation = method.getAnnotation(DoubleCache.class);
                    // 解析 SpEL 表达式,得到最终的 key 片段
                    String elResult = SpelExpressionUtils.parse(annotation.key(), treeMap);
                    // 拼接完整的缓存 key
                    String realKey = annotation.cacheName() + CacheConstant.ORDER + elResult;
        
                    // 处理强制更新操作
                    if (annotation.type() == CacheType.PUT) {
                        // 执行目标方法
                        Object object = point.proceed();
                        // 将结果存入 Redis,并设置过期时间
                        redisTemplate.opsForValue().set(realKey, object, annotation.l2TimeOut(), TimeUnit.SECONDS);
                        // 将结果存入 Caffeine 缓存
                        cache.put(realKey, object);
                        return object;
                    }
        
                    // 处理删除操作
                    if (annotation.type() == CacheType.DELETE) {
                        // 从 Redis 中删除缓存
                        redisTemplate.delete(realKey);
                        // 从 Caffeine 缓存中删除缓存
                        cache.invalidate(realKey);
                        return point.proceed();
                    }
        
                    // 优先从 Caffeine 缓存中获取数据
                    Object caffeineCache = cache.getIfPresent(realKey);
                    if (caffeineCache != null) {
                        log.info("get data from caffeine");
                        return caffeineCache;
                    }
        
                    // 其次从 Redis 中获取数据
                    Object redisCache = redisTemplate.opsForValue().get(realKey);
                    if (redisCache != null) {
                        log.info("get data from redis");
                        // 将从 Redis 中获取的数据存入 Caffeine 缓存
                        cache.put(realKey, redisCache);
                        return redisCache;
                    }
        
                    // 最后查询数据库
                    log.info("get data from database");
                    Object object = point.proceed();
                    if (object != null) {
                        // 将数据库查询结果存入 Redis,并设置过期时间
                        redisTemplate.opsForValue().set(realKey, object, annotation.l2TimeOut(), TimeUnit.SECONDS);
                        // 将数据库查询结果存入 Caffeine 缓存
                        cache.put(realKey, object);
                    }
                    return object;
                } catch (Exception e) {
                    // 记录缓存切面处理过程中的错误
                    log.error("Cache aspect error", e);
                    throw e;
                }
            }
        }
        

        以上操作的主要工作总结下来是:

        • 定义切点:匹配使用 @DoubleCache 注解的方法
        • 参数解析与键生成:提取方法参数,解析 SpEL 表达式生成缓存键
        • 缓存更新策略:PUT 类型执行方法后同步更新 Redis 和本地缓存
        • 缓存删除策略:DELETE 类型先删除 Redis 和本地缓存,再执行方法
        • 多级查询策略:优先查本地缓存 → Redis → 数据库,查询结果写入两级缓存

        执行操作流程,以查询操作为例:

        拦截被 @DoubleCache 标记的目标方法

        生成缓存键 realKey

        依次查询Caffeine → Redis → 数据库

        将数据库结果写入两级缓存并返回

        若触发更新/删除操作,则同步清理或更新缓存

        /**
        * 根据订单ID获取订单信息
        * 使用 @DoubleCache 注解,类型为 FULL,会执行完整的缓存操作逻辑
        * @param id 订单ID
        * @return 订单对象
        */
        @Override
        @DoubleCache(cacheName = "order", key = "#id",
                     type = CacheType.FULL)
        public Order getOrderById(Integer id) {
            return orderMapper.getOrderById(id);
        }
        
        /**
        * 更新订单信息
        * 使用 @DoubleCache 注解,类型为 PUT,会执行缓存更新操作
        * @param order 订单对象
        */
        @Override
        @DoubleCache(cacheName = "order", key = "#id",
                     type = CacheType.PUT)
        public void updateOrder(Order order) {
            orderMapper.updateOrderById(order);
        }
        
        /**
        * 根据订单ID删除订单信息
        * 使用 @DoubleCache 注解,类型为 DELETE,会执行缓存删除操作
        * @param id 订单ID
        */
        @Override
        @DoubleCache(cacheName = "order", key = "#id",
                     type = CacheType.DELETE)
        public void deleteOrderById(Integer id) {
            orderMapper.deleteOrderById(id);
        }
        

        核心注解

        • @DoubleCache:自定义注解,用于标记需要两级缓存的方法
        • CacheType:枚举,定义缓存操作类型(FULL, PUT, DELETE)

        AOP实现要点

        • 解析注解参数
        • 根据操作类型执行不同的缓存逻辑
        • 处理缓存穿透、雪崩等问题
        • 保证两级缓存的一致性

        优势

        • 业务代码完全专注于业务逻辑
        • 缓存逻辑集中管理,便于维护
        • 注解配置灵活,可适应不同场景
        • 代码简洁,可读性高

        关键配置

        Caffeine 配置

        @Configuration
        @EnableCaching
        public class CaffeineConfig {
        
            //-----------------------------V1------V3-----------------------------------
            @Bean
            public Cache<String, Object> orderCache() {
                return Caffeine.newBuilder()
                        .initialCapacity(128)
                        .maximumSize(1024)
                        .expireAfterWrite(60, TimeUnit.SECONDS)
                        .build();
            }
        
            //-----------------------------V2------------------------------------------
            @Bean
            public CacheManager cacheManager(){
                CaffeineCacheManager cacheManager=new CaffeineCacheManager();
                cacheManager.setCaffeine(Caffeine.newBuilder()
                        .initialCapacity(128)
                        .maximumSize(1024)
                        .expireAfterWrite(60, TimeUnit.SECONDS));
                return cacheManager;
            }
        }
        

        Redis 配置

        @Bean
        public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        
            RedisTemplate<String, Object> template = new RedisTemplate<>();
        
            template.setConnectionFactory(factory);
        
            // 创建 ObjectMapper 实例,用于 jsON 序列化和反序列化
            ObjectMapper objectMapper = new ObjectMapper();
            // 注册 JavaTimeModule,用于支持 Java 8 日期时间类型的序列化和反序列化
            objectMapper.registerModule(new JavaTimeModule());
            // 禁用将日期写成时间戳的功能
            objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            // 启用默认类型信息,用于处理多态类型的序列化和反序列化
            objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),
                                               ObjectMapper.DefaultTyping.NON_FINAL);
        
            // 创建 GenericJackson2JsonRedisSerializer 实例,使用配置好的 ObjectMapper
            GenericJackson2JsonRedisSerializer serializer =
                new GenericJackson2JsonRedisSerializer(objectMapper);
        
            template.setKeySerializer(new StringRedisSerializer());
        
            template.setValueSerializer(serializer);
        
            template.setHashKeySerializer(new StringRedisSerializer());
        
            template.setHashValueSerializer(serializer);
        
        
            template.afterPropertiesSet();
            return template;
        }
        

        性能优化建议

        合理设置缓存过期时间

        • 本地缓存过期时间应短于 Redis 缓存
        • 根据数据更新频率调整过期策略

        缓存穿透防护

        • 对空结果也进行缓存
        • 使用布隆过滤器

        缓存雪崩防护

        • 设置随机过期时间
        • 实现熔断机制

        一致性保证

        • 考虑使用消息队列同步多节点本地缓存
        • 对于关键数据,可采用"先更新数据库,再删除缓存"策略

        总结

        通过三个版本的演进,我们实现了一个从强耦合到完全解耦的两级缓存系统。最终版本利用自定义注解和 AOP 技术,既保持了代码的简洁性,又提供了强大的缓存功能。这种架构特别适合读多写少、对性能要求较高的场景。

        在实际应用中,还需要根据具体业务特点调整缓存策略,并做好监控和指标收集,以便持续优化缓存效果。Redis + Caffeine 实现高效的两级缓存架构

        以上就是Redis+Caffeine实现高效两级缓存架构的详细指南的详细内容,更多关于Redis Caffeine两级缓存的资料请关注编程客栈(www.devze.com)其它相关文章!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新数据库

        数据库排行榜