Spring Cache + Redis 缓存套餐数据,我是怎么在苍穹外卖项目里用起来的?
Spring Cache + Redis 在苍穹外卖项目中的实战应用
1. 为什么选择Spring Cache与Redis组合
在开发苍穹外卖这样的高并发餐饮系统时,数据库查询压力往往成为性能瓶颈。特别是套餐数据这类变化频率较低但访问量极高的内容,每次请求都直接查询数据库显然不是最优解。
Spring Cache作为Spring生态的缓存抽象层,提供了一套简洁的注解驱动缓存方案。而Redis作为内存数据库,其极高的读写性能使其成为缓存后端的理想选择。两者的结合能够:
- 减少数据库访问次数,降低系统负载
- 提升响应速度,改善用户体验
- 通过注解简化开发,提高代码可维护性
典型应用场景:
- 菜单/套餐展示(高频读取)
- 用户信息缓存(减少重复查询)
- 店铺营业状态(快速访问)
2. 项目集成实战
2.1 基础环境配置
首先确保项目中已包含必要依赖:
<!-- Redis Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Spring Cache Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>在application.yml中配置Redis连接:
spring: redis: host: 127.0.0.1 port: 6379 password: database: 0 lettuce: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0启动类添加@EnableCaching注解激活缓存功能:
@SpringBootApplication @EnableCaching public class SkyApplication { public static void main(String[] args) { SpringApplication.run(SkyApplication.class, args); } }2.2 缓存套餐数据实战
假设我们有套餐服务MealService,以下是典型缓存应用:
@Service public class MealServiceImpl implements MealService { @Autowired private MealMapper mealMapper; @Cacheable(value = "mealCache", key = "#categoryId") public List<Meal> getMealsByCategory(Long categoryId) { // 实际数据库查询 return mealMapper.findByCategoryId(categoryId); } @CachePut(value = "mealCache", key = "#meal.categoryId") public Meal addMeal(Meal meal) { mealMapper.insert(meal); return meal; } @CacheEvict(value = "mealCache", key = "#categoryId") public void deleteMeal(Long id, Long categoryId) { mealMapper.deleteById(id); } }关键注解解析:
| 注解 | 作用 | 适用场景 |
|---|---|---|
@Cacheable | 方法执行前检查缓存,存在则直接返回 | 查询操作 |
@CachePut | 总是执行方法,并将结果存入缓存 | 新增/更新操作 |
@CacheEvict | 清除指定缓存 | 删除操作 |
2.3 缓存Key设计策略
合理的Key设计对缓存效率至关重要。苍穹外卖项目中我们采用以下策略:
- 业务前缀:如
mealCache:: - 参数组合:对于多参数方法,使用SpEL表达式组合
@Cacheable(value = "mealCache", key = "#shopId+':'+#categoryId") - 自定义Key生成器(复杂场景):
@Configuration public class CacheConfig { @Bean public KeyGenerator mealKeyGenerator() { return (target, method, params) -> { StringBuilder sb = new StringBuilder(); sb.append("meal_"); sb.append(params[0]); return sb.toString(); }; } }
3. 性能优化与问题解决
3.1 缓存穿透防护
当查询不存在的数据时,可能导致大量请求直接穿透到数据库。解决方案:
- 空值缓存:
@Cacheable(value = "mealCache", key = "#id", unless = "#result == null") public Meal getById(Long id) { Meal meal = mealMapper.selectById(id); if(meal == null) { // 缓存空对象,设置较短过期时间 return new NullMeal(); } return meal; } - 布隆过滤器:在缓存层前增加过滤
3.2 缓存雪崩预防
大量缓存同时失效导致数据库压力骤增:
- 差异化过期时间:
@Configuration public class RedisConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .computePrefixWith(name -> name + "::"); return RedisCacheManager.builder(factory) .cacheDefaults(config) .withInitialCacheConfigurations(Map.of( "mealCache", config.entryTtl(Duration.ofHours(1)), "userCache", config.entryTtl(Duration.ofDays(1)) )) .build(); } } - 热点数据永不过期:配合异步刷新
3.3 缓存一致性保障
数据库与缓存数据不一致的解决方案:
- 双写模式:
@Transactional public void updateMeal(Meal meal) { mealMapper.updateById(meal); redisTemplate.opsForValue().set( "meal::"+meal.getId(), meal, Duration.ofHours(1) ); } - 失效模式(更推荐):
@Transactional @CacheEvict(value = "mealCache", key = "#meal.id") public void updateMeal(Meal meal) { mealMapper.updateById(meal); }
4. 高级应用技巧
4.1 条件化缓存
通过condition和unless参数实现条件缓存:
@Cacheable(value = "mealCache", key = "#id", condition = "#id != null", unless = "#result.price > 100") public Meal getMealById(Long id) { return mealMapper.selectById(id); }4.2 多级缓存策略
结合本地缓存与Redis实现多级缓存:
- Caffeine本地缓存配置:
@Configuration public class CacheConfig { @Bean public CaffeineCacheManager caffeineCacheManager() { Caffeine<Object, Object> caffeine = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES); return new CaffeineCacheManager("localMealCache", "localUserCache") .setCaffeine(caffeine); } } - 自定义缓存解析器实现多级缓存逻辑
4.3 缓存监控与统计
通过Redis命令或Spring Boot Actuator监控缓存命中率:
management: endpoints: web: exposure: include: health,info,caches定制缓存统计Endpoint:
@Component public class CacheMetricsEndpoint extends AbstractEndpoint<Map<String, Object>> { @Autowired private CacheManager cacheManager; public CacheMetricsEndpoint() { super("cachemetrics"); } @Override public Map<String, Object> invoke() { Map<String, Object> metrics = new HashMap<>(); if(cacheManager instanceof RedisCacheManager) { // 获取Redis特定指标 } return metrics; } }5. 实际项目经验分享
在苍穹外卖项目中,我们遇到了几个值得注意的情况:
套餐分类变更时的缓存更新:
@Transactional @Caching(evict = { @CacheEvict(value = "mealCache", key = "#oldCategoryId"), @CacheEvict(value = "mealCache", key = "#meal.categoryId") }) public void changeCategory(Meal meal, Long oldCategoryId) { mealMapper.updateById(meal); }批量操作时的缓存处理:
@CacheEvict(value = "mealCache", allEntries = true) public void batchUpdate(List<Meal> meals) { mealMapper.batchUpdate(meals); }缓存预热策略:
@PostConstruct public void preloadPopularMeals() { List<Long> popularIds = mealMapper.selectPopularIds(); popularIds.forEach(id -> { Meal meal = mealMapper.selectById(id); redisTemplate.opsForValue().set( "meal::"+id, meal, Duration.ofHours(2) ); }); }
对于特别热门的套餐数据,我们最终采用了本地缓存+Redis的双层结构,将缓存命中率从75%提升到了98%,数据库查询量减少了近90%。
