Spring Cache + Redis 缓存套餐数据,我是这样在Spring Boot项目里实战的
Spring Cache Redis 在外卖系统中的实战优化1. 从业务痛点出发的缓存改造去年接手一个日订单量突破5万单的外卖平台重构项目时我们遇到了一个典型的性能瓶颈——每到午晚高峰时段套餐列表接口的响应时间就从平时的200ms飙升到2秒以上。通过Arthas工具追踪发现80%的耗时都集中在数据库查询上每次请求都要执行SELECT * FROM meal WHERE status 1这样的全表扫描而套餐数据实际上每天只会变更1-2次。这种场景正是Spring Cache的用武之地。与直接使用RedisTemplate相比Spring Cache的注解驱动方式让缓存集成变得异常简单。但实际落地时我们发现需要解决几个关键问题// 原始未优化的查询代码 GetMapping(/meals) public ListMeal listActiveMeals() { return mealMapper.selectByStatus(1); // 每次请求都查库 }2. 缓存配置的黄金组合2.1 依赖引入与基础配置在pom.xml中需要同时引入两个starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependencyapplication.yml中的关键配置项spring: cache: type: redis redis: time-to-live: 3600000 # 1小时过期 key-prefix: CACHE_ use-key-prefix: true redis: host: 127.0.0.1 lettuce: pool: max-active: 20提示生产环境建议配置不同的缓存命名空间cacheNames比如menu_cache和promotion_cache分开配置TTL2.2 自定义缓存序列化默认的JDK序列化会导致两个问题可视化调试困难二进制格式不同语言服务间的兼容性问题通过自定义配置解决Configuration public class RedisConfig { Bean public RedisCacheConfiguration cacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith( RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())) .entryTtl(Duration.ofHours(1)); } }3. 注解使用的实战技巧3.1 基础缓存操作改造后的套餐服务示例Cacheable(cacheNames meal_cache, key active_meals) public ListMeal getActiveMeals() { log.info(缓存未命中查询数据库...); return mealMapper.selectByStatus(1); } CacheEvict(cacheNames meal_cache, key active_meals) public void updateMeal(Meal meal) { mealMapper.updateById(meal); }3.2 复杂场景处理多条件查询缓存Cacheable(cacheNames meal_cache, key #categoryId_#minPrice_#maxPrice) public ListMeal search(Integer categoryId, BigDecimal minPrice, BigDecimal maxPrice) { // 复杂查询逻辑 }批量删除技巧CacheEvict(cacheNames meal_cache, allEntries true) public void batchUpdateStatus(ListLong ids, Integer status) { // 批量更新操作 }4. 性能优化与问题排查4.1 缓存命中率监控通过Spring Boot Actuator暴露缓存指标management: endpoints: web: exposure: include: health,metrics,caches metrics: tags: application: ${spring.application.name}访问/actuator/metrics/cache.gets可以获取如下关键指标指标名称健康阈值说明cache.gets-缓存总请求数cache.gets.miss30%缓存未命中数cache.evictions告警阈值缓存驱逐次数4.2 常见问题解决方案缓存穿透防护Cacheable(cacheNames meal_cache, key #id, unless #result null) public Meal getById(Long id) { Meal meal mealMapper.selectById(id); if(meal null) { // 记录异常查询 abnormalQueryService.recordNullQuery(id); } return meal; }缓存雪崩预防spring: cache: redis: time-to-live: 3600000 # 基础TTL randomization: 0.2 # ±20%随机浮动5. 与MyBatis的协作模式5.1 二级缓存整合在mapper.xml中配置cache evictionLRU flushInterval60000 size1024 readOnlytrue/注意需要确保实体类实现Serializable接口5.2 事务一致性保障Transactional CacheEvict(cacheNames meal_cache, key #meal.id) public void updateWithCache(Meal meal) { // 先更新数据库 mealMapper.updateById(meal); // 异常时会回滚数据库且不清理缓存 }6. 效果验证与对比测试使用JMeter进行压测100并发场景QPS平均响应时间错误率无缓存128780ms0.2%基础缓存210045ms0%优化后缓存350028ms0%关键优化点带来的提升序列化改为JSON提升15%吞吐合理的TTL设置降低30%数据库负载本地缓存配合Redis形成二级缓存7. 扩展应用场景组合查询优化Caching( cacheable { Cacheable(cacheNames meal_detail, key #id) }, put { CachePut(cacheNames meal_stats, key #result.categoryId) } ) public Meal getDetailWithStats(Long id) { // 复杂查询逻辑 }定时缓存预热Scheduled(cron 0 0 6 * * ?) public void preloadCache() { ListMeal meals mealMapper.selectAll(); meals.forEach(meal - redisTemplate.opsForValue() .set(meal:meal.getId(), meal, 12, HOURS)); }在实际项目中我们通过这套方案将高峰期的数据库负载降低了72%同时因为减少了重复查询Redis的内存使用量反而下降了15%。最意外的是由于缓存命中率提升Redis的CPU利用率也从40%降到了22%左右。