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%。
更多推荐

所有评论(0)