Spring Cache + Redis 缓存套餐数据,我是这样做的(附完整代码与踩坑记录)

张开发
2026/6/8 3:39:49 15 分钟阅读

分享文章

Spring Cache + Redis 缓存套餐数据,我是这样做的(附完整代码与踩坑记录)
Spring Cache与Redis实战外卖系统套餐缓存设计与避坑指南当外卖平台的日订单量突破10万时数据库查询压力陡增。某次促销活动中我们发现有78%的数据库负载都来自套餐信息的重复查询——这些数据每天变更不超过3次却要被读取数百万次。这正是Spring Cache与Redis的完美用武之地。1. 缓存架构设计与环境搭建1.1 依赖配置与基础环境在pom.xml中需要同时引入Redis和Spring Cache的starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependencyRedis配置示例application.ymlspring: redis: host: 127.0.0.1 port: 6379 password: lettuce: pool: max-active: 8 max-idle: 8 min-idle: 21.2 缓存序列化优化默认的JDK序列化会导致两个问题内存占用过大跨语言兼容性差推荐改用JSON序列化Configuration public class RedisConfig { Bean public RedisCacheConfiguration redisCacheConfiguration() { return RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); } }2. 核心缓存策略实现2.1 套餐查询缓存设计对于套餐这种读多写少的数据Cacheable是最佳选择Service public class SetmealServiceImpl implements SetmealService { Cacheable(value setmealCache, key #categoryId) public ListSetmeal listByCategory(Long categoryId) { // 数据库查询逻辑 return setmealMapper.listByCategoryId(categoryId); } }缓存键设计要点业务前缀避免冲突如setmealCache::1包含必要查询参数避免过于复杂的键结构2.2 数据更新时的缓存同步采用CacheEvict保证数据一致性PostMapping CacheEvict(value setmealCache, allEntries true) public Result addSetmeal(RequestBody SetmealDTO setmealDTO) { setmealService.saveWithDishes(setmealDTO); return Result.success(); }注意批量操作时建议使用allEntriestrue而非逐个删除避免Redis多次查询带来的延迟3. 生产环境常见问题解决方案3.1 缓存穿透防护当查询不存在的套餐ID时会导致大量请求穿透到数据库。解决方案Cacheable(value setmealCache, key #id, unless #result null) public Setmeal getById(Long id) { Setmeal setmeal setmealMapper.getById(id); if(setmeal null) { // 记录异常查询 log.warn(Attempt to query non-existent setmeal: {}, id); } return setmeal; }防护策略对比表策略实现复杂度效果适用场景空值缓存★★☆中明确的不存在查询布隆过滤器★★★高海量数据场景接口限流★★☆中突发流量场景3.2 缓存雪崩预防通过随机TTL避免大量缓存同时失效Bean public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { return builder - builder .withCacheConfiguration(setmealCache, RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30 new Random().nextInt(15)))); }3.3 热点Key发现与处理使用Redis命令识别热点Keyredis-cli --hotkeys解决方案本地缓存二级缓存CaffeineKey分片如setmealCache::1_shard1限流措施4. 高级优化技巧4.1 缓存预热策略在服务启动时自动加载高频访问数据PostConstruct public void preloadCache() { ListLong hotCategoryIds categoryService.getHotCategoryIds(); hotCategoryIds.forEach(id - setmealService.listByCategory(id)); }4.2 多级缓存架构graph TD A[客户端] -- B{Nginx缓存} B --|命中| C[返回数据] B --|未命中| D[应用服务] D -- E{本地缓存} E --|命中| F[返回数据] E --|未命中| G[Redis集群] G --|命中| H[返回并回填] G --|未命中| I[数据库查询]4.3 监控与告警配置Spring Boot Actuator提供缓存指标management: endpoints: web: exposure: include: health,metrics,caches metrics: tags: application: ${spring.application.name}关键监控指标cache.gets缓存查询次数cache.hits命中次数cache.evictions淘汰次数5. 实战中的经验教训在一次大促前我们发现缓存命中率突然从98%跌至65%。排查发现是某开发在更新逻辑中错误使用了// 错误示范缺少缓存清除 public void updateStatus(Long id, Integer status) { setmealMapper.updateStatus(id, status); }修正方案CacheEvict(value setmealCache, key #id) public void updateStatus(Long id, Integer status) { setmealMapper.updateStatus(id, status); }另一个常见问题是缓存与数据库的事务一致性。我们最终采用的方案是先更新数据库提交事务再删除缓存设置短暂延迟后二次删除双删策略对于特别重要的套餐数据我们还增加了缓存版本号机制Cacheable(value setmealCache, key v2_ #id) public Setmeal getByIdWithVersion(Long id) { // ... }

更多文章