
外卖返利系统多级缓存架构设计Caffeine与Redis的协同作战在高并发的外卖返利业务场景下每一次用户请求都直接穿透到数据库或远程服务系统将迅速崩溃。为了在保证数据一致性的前提下最大化系统吞吐量构建一个高效的多级缓存架构至关重要。本文将基于baodanbao.com.cn的实际架构深入探讨如何利用 Caffeine本地缓存与 Redis分布式缓存协同工作打造一个高性能、低延迟的返利系统缓存层。一、 缓存架构设计双层防御体系我们的目标是将最热的数据如热门霸王餐活动配置、平台通用券信息尽可能靠近应用层减少网络开销。架构层级L1Caffeine 本地缓存。存储访问频率极高、更新不频繁的数据。命中则直接返回无网络开销。L2Redis 分布式缓存。存储全量热点数据。当 L1 未命中时查询 L2并将数据回填至 L1。L3DB / Remote API。当 L1 和 L2 均未命中时降级为从数据库或美团/饿了么API获取数据并异步更新至 L1 和 L2。二、 核心依赖与配置首先在pom.xml中引入 Caffeine 依赖并配置 Redis 连接池。1. Maven 依赖dependencies!-- Spring Boot Starter Data Redis --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency!-- Caffeine Cache --dependencygroupIdcom.github.ben-manes.caffeine/groupIdartifactIdcaffeine/artifactIdversion3.1.8/version/dependency/dependencies2. Java 配置类packagecom.baodanbao.config;importcom.github.benmanes.caffeine.cache.Caffeine;importorg.springframework.cache.CacheManager;importorg.springframework.cache.annotation.EnableCaching;importorg.springframework.cache.caffeine.CaffeineCacheManager;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.StringRedisSerializer;importjava.util.concurrent.TimeUnit;/** * author baodanbao.com.cn * 多级缓存配置中心 */ConfigurationEnableCachingpublicclassMultiLevelCacheConfig{/** * 配置 Caffeine 本地缓存管理器 */BeanpublicCacheManagercaffeineCacheManager(){CaffeineCacheManagercacheManagernewCaffeineCacheManager();// 设置全局过期时间防止内存溢出cacheManager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(10,TimeUnit.MINUTES).maximumSize(1000));// 限制本地缓存条目数returncacheManager;}/** * 配置 RedisTemplate用于操作分布式缓存 */BeanpublicRedisTemplateString,ObjectredisTemplate(RedisConnectionFactoryfactory){RedisTemplateString,ObjecttemplatenewRedisTemplate();template.setConnectionFactory(factory);// 设置序列化方式避免乱码template.setKeySerializer(newStringRedisSerializer());template.setValueSerializer(newGenericJackson2JsonRedisSerializer());returntemplate;}}三、 缓存穿透与击穿防护在多级缓存中必须解决两个核心问题缓存穿透查询不存在的数据和缓存击穿热点Key过期瞬间的并发查询。解决方案空值缓存对于查询结果为空的情况也在 Redis 和 Caffeine 中存储一个特殊标记如NullObject并设置较短的过期时间。互斥锁当缓存失效时使用 Redis 分布式锁Redisson控制只有一个线程去加载数据其他线程等待并读取旧缓存或等待锁释放。四、 业务服务层实现多级缓存读写逻辑以下是BdbCouponService的核心实现展示了如何手动控制 Caffeine 和 Redis 的交互逻辑。packagecom.baodanbao.service;importcom.baodanbao.entity.Coupon;importcom.baodanbao.repository.CouponRepository;importcom.github.benmanes.caffeine.cache.Cache;importcom.github.benmanes.caffeine.cache.Caffeine;importorg.redisson.api.RLock;importorg.redisson.api.RedissonClient;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Service;importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.TimeUnit;/** * author baodanbao.com.cn * 霸王餐优惠券服务 - 多级缓存实现 */ServicepublicclassBdbCouponService{AutowiredprivateRedisTemplateString,ObjectredisTemplate;AutowiredprivateCouponRepositorycouponRepository;// 假设是JPA或MyBatis Plus RepositoryAutowiredprivateRedissonClientredissonClient;// 手动构建 Caffeine Cache 实例privatefinalCacheString,CouponlocalCacheCaffeine.newBuilder().expireAfterWrite(5,TimeUnit.MINUTES).maximumSize(500).build();privatestaticfinalStringREDIS_COUPON_PREFIXcoupon:detail:;privatestaticfinalStringREDIS_LOCK_PREFIXlock:coupon:;/** * 获取优惠券详情多级缓存逻辑 * param couponId 优惠券ID * return 优惠券对象 */publicCoupongetCouponDetail(LongcouponId){StringlocalKeyL1:couponId;StringredisKeyREDIS_COUPON_PREFIXcouponId;StringlockKeyREDIS_LOCK_PREFIXcouponUpId;// 1. 先查本地缓存 (Caffeine)CouponcouponlocalCache.getIfPresent(localKey);if(coupon!null){// 如果是空对象标记直接返回nullif(couponinstanceofNullCouponPlaceholder){returnnull;}returncoupon;}// 2. 本地缓存未命中查Rediscoupon(Coupon)redisTemplate.opsForValue().get(redisKey);if(coupon!null){// 回填本地缓存localCache.put(localKey,coupon);returncoupon;}// 3. Redis也未命中需要加载数据防止击穿RLocklockredissonClient.getLock(lockKey);try{// 尝试获取锁最多等待100ms锁持有时间3秒if(lock.tryLock(100,3000,TimeUnit.MILLISECONDS)){// 双重检查防止多个线程排队获取锁后重复加载coupon(Coupon)redisTemplate.opsForValue().get(redisKey);if(coupon!null){localCache.put(localKey,coupon);returncoupon;}// 真正加载数据couponcouponRepository.findById(couponId).orElse(null);if(couponnull){// 防止缓存穿透设置空值标记过期时间短一点redisTemplate.opsForValue().set(redisKey,newNullCouponPlaceholder(),2,TimeUnit.MINUTES);localCache.put(localKey,newNullCouponPlaceholder());}else{// 写入Redis设置过期时间redisTemplate.opsForValue().set(redisKey,coupon,10,TimeUnit.MINUTES);// 回填本地缓存localCache.put(localKey,coupon);}returncoupon;}else{// 获取锁失败走降级逻辑直接查库或者返回默认值/报错// 这里为了简单直接查库实际生产环境建议返回旧数据或友好提示returncouponRepository.findById(couponId).orElse(null);}}catch(InterruptedExceptione){Thread.currentThread().interrupt();// 中断异常处理returncouponRepository.findById(couponId).orElse(null);}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}}/** * 更新优惠券信息主动失效缓存 * param coupon 更新后的优惠券 */publicvoidupdateCoupon(Couponcoupon){StringlocalKeyL1:coupon.getId();StringredisKeyREDIS_COUPON_PREFIXcoupon.getId();// 1. 先更新数据库couponRepository.save(coupon);// 2. 删除本地缓存注意如果是多实例部署这里需要通过MQ通知其他节点删除本地缓存localCache.invalidate(localKey);// 3. 删除Redis缓存redisTemplate.delete(redisKey);}}// 空对象占位符用于解决缓存穿透classNullCouponPlaceholder{}五、 缓存一致性保障在多实例部署的微服务架构中本地缓存Caffeine的一致性是一个难题。当一个节点更新了数据并删除了自己的本地缓存其他节点的本地缓存依然存在旧数据。解决方案缩短本地缓存过期时间利用 Caffeine 的expireAfterWrite让数据在本地尽快过期依赖 L2 Redis 的数据兜底。Redis Key 失效监听利用 Redis 的 Pub/Sub 或 KeySpace Notifications 功能当 Key 被删除或过期时发布消息通知所有应用节点清除对应的本地缓存。MQ 广播在更新数据时发送一条 MQ 消息如 RocketMQ所有订阅该 Topic 的应用实例收到消息后主动失效本地缓存。通过上述设计baodanbao.com.cn的外卖返利系统在面对高并发查询时能够有效利用多级缓存的优势将响应时间控制在毫秒级同时减轻了后端数据库和远程API的负载压力。本文著作权归 俱美开放平台 转载请注明出处