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设计对缓存效率至关重要。苍穹外卖项目中我们采用以下策略:

  1. 业务前缀 :如 mealCache::
  2. 参数组合 :对于多参数方法,使用SpEL表达式组合
    @Cacheable(value = "mealCache", key = "#shopId+':'+#categoryId")
    
  3. 自定义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 缓存穿透防护

当查询不存在的数据时,可能导致大量请求直接穿透到数据库。解决方案:

  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;
    }
    
  2. 布隆过滤器 :在缓存层前增加过滤

3.2 缓存雪崩预防

大量缓存同时失效导致数据库压力骤增:

  1. 差异化过期时间
    @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();
        }
    }
    
  2. 热点数据永不过期 :配合异步刷新

3.3 缓存一致性保障

数据库与缓存数据不一致的解决方案:

  1. 双写模式
    @Transactional
    public void updateMeal(Meal meal) {
        mealMapper.updateById(meal);
        redisTemplate.opsForValue().set(
            "meal::"+meal.getId(), 
            meal,
            Duration.ofHours(1)
        );
    }
    
  2. 失效模式 (更推荐):
    @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实现多级缓存:

  1. 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);
        }
    }
    
  2. 自定义缓存解析器 实现多级缓存逻辑

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. 实际项目经验分享

在苍穹外卖项目中,我们遇到了几个值得注意的情况:

  1. 套餐分类变更时的缓存更新

    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "mealCache", key = "#oldCategoryId"),
        @CacheEvict(value = "mealCache", key = "#meal.categoryId")
    })
    public void changeCategory(Meal meal, Long oldCategoryId) {
        mealMapper.updateById(meal);
    }
    
  2. 批量操作时的缓存处理

    @CacheEvict(value = "mealCache", allEntries = true)
    public void batchUpdate(List<Meal> meals) {
        mealMapper.batchUpdate(meals);
    }
    
  3. 缓存预热策略

    @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%。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐