手把手教你用Spring Cache和Redis搞定苍穹外卖的菜品与套餐缓存(附完整配置流程)
从零构建高并发外卖系统Spring Cache与Redis深度整合实战在当今快节奏的数字化餐饮时代一个外卖平台的核心竞争力往往体现在毫秒级的响应速度上。想象一下当午高峰时段成千上万的用户同时浏览菜单如果每次请求都直接穿透到数据库不仅会造成用户体验的卡顿更可能导致整个系统雪崩。这正是为什么像苍穹外卖这类成熟项目都会采用多级缓存策略——而Spring Cache与Redis的组合堪称Java生态中解决这类问题的黄金搭档。作为一位经历过多个电商项目的老兵我见过太多团队在缓存方案上的折戟沉沙有的过度设计导致维护成本飙升有的轻视缓存一致性酿成线上事故。本文将带你从实战角度用Spring Cache注解与Redis构建一套既简洁又可靠的菜品缓存系统这些经验同样适用于商品、库存等任何需要高频访问的业务数据。不同于简单的API调用教程我们会深入探讨缓存模式选择、序列化陷阱、失效策略设计等真正影响生产稳定性的关键细节。1. 环境准备与基础配置1.1 项目骨架搭建建议使用Spring Initializr创建项目时勾选以下核心依赖dependencies !-- Spring Boot Starter基础依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependency !-- 其他必要依赖... -- /dependencies在application.yml中配置Redis连接生产环境建议使用连接池spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 cache: type: redis1.2 序列化方案选型Redis默认的JDK序列化会产生兼容性问题这里推荐使用JSON序列化Configuration EnableCaching public class RedisConfig { Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); } Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } }注意Jackson在处理LocalDateTime等Java 8时间类型时需要额外配置建议添加jackson-datatype-jsr310依赖2. 菜品缓存的核心实现2.1 声明式缓存注解实战在Service层使用Spring Cache提供的三大核心注解Service public class DishServiceImpl implements DishService { Cacheable(value dish, key #categoryId) public ListDishVO listByCategory(Long categoryId) { // 真实数据库查询逻辑 return dishMapper.listByCategoryId(categoryId); } CacheEvict(value dish, key #dish.categoryId) public void updateDish(DishDTO dish) { dishMapper.update(dish); // 可添加其他业务逻辑 } CacheEvict(value dish, allEntries true) public void cleanCache() { // 该方法执行时会清空所有dish缓存 } }关键参数说明注解属性作用描述value/cacheNames定义缓存名称空间对应Redis中的key前缀keySpEL表达式动态生成缓存键allEntries是否清空整个名称空间下的所有缓存批量操作时特别有用2.2 多级缓存策略设计对于菜品这类读多写少的数据推荐采用以下缓存架构用户请求 → 本地缓存(Caffeine) → 分布式缓存(Redis) → 数据库实现示例Configuration public class MultiLevelCacheConfig { Bean public CacheManager cacheManager(RedisConnectionFactory factory) { CaffeineCacheManager caffeineCacheManager new CaffeineCacheManager(); caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .expireAfterWrite(30, TimeUnit.SECONDS) .maximumSize(1000)); RedisCacheManager redisCacheManager RedisCacheManager .builder(factory) .cacheDefaults(cacheConfiguration()) .build(); return new CompositeCacheManager(caffeineCacheManager, redisCacheManager); } }这种组合既能享受本地缓存的超低延迟又保持了Redis的分布式一致性优势。3. 缓存一致性保障机制3.1 双写失效问题解决方案在更新菜品时常见的错误做法是// 反例存在缓存穿透风险 public void updateDish(DishDTO dish) { dishMapper.update(dish); redisTemplate.delete(dish:: dish.getId()); }更可靠的做法是采用事务消息保证最终一致性Transactional public void updateDish(DishDTO dish) { // 1. 更新数据库 dishMapper.update(dish); // 2. 发送缓存失效事件 applicationContext.publishEvent(new CacheEvictEvent(this, dish, dish.getId())); } // 监听器处理 EventListener public void handleCacheEvict(CacheEvictEvent event) { try { redisTemplate.delete(event.getCacheName() :: event.getKey()); } catch (Exception e) { log.error(缓存清理失败, e); // 可加入重试机制 } }3.2 缓存雪崩预防当大量缓存同时失效时会导致请求直接打到数据库。解决方案差异化过期时间Cacheable(value dish, key #categoryId, cacheManager randomTtlCacheManager) public ListDishVO listByCategory(Long categoryId) { ... } // 自定义CacheManager Bean public RedisCacheManager randomTtlCacheManager(RedisConnectionFactory factory) { return RedisCacheManager.builder(factory) .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30 new Random().nextInt(15)))) .build(); }热点数据永不过期Scheduled(fixedRate 3600000) public void preloadHotDishes() { ListLong hotCategoryIds getHotCategories(); hotCategoryIds.forEach(id - { ListDishVO dishes dishService.listByCategory(id); // 触发缓存加载 }); }4. 高级优化与监控4.1 缓存命中率统计通过AOP监控缓存使用情况Aspect Component RequiredArgsConstructor public class CacheMonitorAspect { private final MeterRegistry meterRegistry; Around(annotation(cacheable)) public Object monitorCacheable(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable { String cacheName cacheable.value()[0]; String key cacheable.key(); try { Object result pjp.proceed(); if (result ! null) { meterRegistry.counter(cache.hits, name, cacheName).increment(); } return result; } catch (Exception e) { meterRegistry.counter(cache.misses, name, cacheName).increment(); throw e; } } }4.2 大Key优化策略当菜品列表数据过大时可采用分片缓存Cacheable(value dish, key #categoryId _ #pageNum) public ListDishVO listByCategoryPage(Long categoryId, Integer pageNum) { // 分页查询逻辑 }压缩存储Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); // 使用Snappy压缩 template.setValueSerializer(new SnappyJsonRedisSerializer()); return template; }在灰度上线阶段我们曾发现某个热门分类的菜品列表达到2MB大小通过分片压缩后Redis内存占用下降了76%网络传输时间从120ms降至28ms。这种优化对于移动端用户尤为明显。