☰
Redis缓存优化与Spring Cache实战:提升烘焙坊小程序性能
2026/11/3 16:33:55 网站建设 项目流程

1. 项目背景与核心问题

在烘焙坊小程序的后端开发中,我们遇到了一个典型的性能瓶颈问题。用户端每次展示菜品数据都需要直接查询数据库,当访问量增大时,数据库压力骤增,导致系统响应变慢,用户体验直线下降。这就像一家生意火爆的面包店,每次顾客询问产品信息都要去后厨翻账本,效率自然低下。

通过压力测试发现,在200并发用户请求下,单纯依赖数据库查询的响应时间达到800ms以上,且数据库CPU使用率长期维持在90%以上。这种架构显然无法支撑业务增长,必须引入缓存机制来优化性能。

2. Redis缓存菜品实现方案

2.1 缓存架构设计

我们采用Redis作为缓存中间件,设计了一套分级缓存策略。核心思路是将菜品数据按分类缓存,每个分类对应一个独立的缓存键,结构如下:

dish_{categoryId} -> JSON序列化的菜品列表

这种设计有三大优势:

  1. 分类缓存可以精准控制缓存失效范围
  2. 避免单个大key导致的Redis性能问题
  3. 符合业务查询模式(用户通常按分类浏览)

2.2 缓存读写流程

完整的缓存读写流程如下:

public List<Dish> getDishesByCategory(Long categoryId) { // 1. 构建缓存key String cacheKey = "dish_" + categoryId; // 2. 尝试从Redis获取 String cachedData = redisTemplate.opsForValue().get(cacheKey); if (StringUtils.isNotBlank(cachedData)) { return JSON.parseArray(cachedData, Dish.class); } // 3. 缓存未命中,查询数据库 List<Dish> dishes = dishMapper.selectByCategory(categoryId); // 4. 写入缓存,设置30分钟过期 redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(dishes), 30, TimeUnit.MINUTES ); return dishes; }

2.3 缓存一致性保障

缓存最大的挑战是如何保证与数据库的一致性。我们实现了统一的缓存清理机制:

@Component public class DishCacheCleaner { @Autowired private RedisTemplate<String, String> redisTemplate; public void cleanByCategory(Long categoryId) { String key = "dish_" + categoryId; redisTemplate.delete(key); } public void cleanAll() { Set<String> keys = redisTemplate.keys("dish_*"); if (keys != null && !keys.isEmpty()) { redisTemplate.delete(keys); } } }

在菜品增删改操作中强制调用清理逻辑:

@PostMapping public Result addDish(@RequestBody DishDTO dishDTO) { dishService.add(dishDTO); // 清理对应分类缓存 cacheCleaner.cleanByCategory(dishDTO.getCategoryId()); return Result.success(); }

重要提示:缓存清理必须放在事务提交之后执行,否则可能出现缓存已清理但数据库事务回滚的情况。

3. Spring Cache实现套餐缓存

3.1 Spring Cache集成

对于套餐数据,我们采用更高级的Spring Cache抽象层,底层依然使用Redis实现。首先需要添加依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

在启动类上启用缓存:

@SpringBootApplication @EnableCaching public class BakeryApplication { public static void main(String[] args) { SpringApplication.run(BakeryApplication.class, args); } }

3.2 缓存注解实战

套餐查询方法使用@Cacheable:

@GetMapping("/list") @Cacheable(cacheNames = "setmealCache", key = "#categoryId") public Result<List<Setmeal>> list(Long categoryId) { // 查询数据库逻辑 List<Setmeal> setmeals = setmealService.listByCategory(categoryId); return Result.success(setmeals); }

数据变更方法使用@CacheEvict:

@PostMapping @CacheEvict(cacheNames = "setmealCache", allEntries = true) public Result addSetmeal(@RequestBody SetmealDTO setmealDTO) { setmealService.add(setmealDTO); return Result.success(); }

3.3 缓存配置优化

在application.yml中配置Redis缓存行为:

spring: cache: type: redis redis: time-to-live: 1800s # 30分钟过期 key-prefix: "BAKERY:" # 统一前缀 use-key-prefix: true redis: host: 127.0.0.1 port: 6379

4. 购物车功能完整实现

4.1 数据库设计精要

购物车表设计遵循以下原则:

  1. 用户隔离:通过user_id区分不同用户的购物车
  2. 商品区分:同时支持单品和套餐,通过dish_id/setmeal_id区分
  3. 数据冗余:存储商品名称、图片、单价等关键信息,避免频繁联表查询
CREATE TABLE `shopping_cart` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(32) DEFAULT NULL, `image` varchar(255) DEFAULT NULL, `user_id` bigint NOT NULL, `dish_id` bigint DEFAULT NULL, `setmeal_id` bigint DEFAULT NULL, `dish_flavor` varchar(50) DEFAULT NULL, `number` int NOT NULL DEFAULT '1', `amount` decimal(10,2) NOT NULL, `create_time` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4.2 核心业务逻辑实现

添加购物车
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) { // 构建查询条件 ShoppingCart condition = new ShoppingCart(); BeanUtils.copyProperties(shoppingCartDTO, condition); condition.setUserId(BaseContext.getCurrentId()); // 查询是否已存在相同商品 List<ShoppingCart> existingItems = shoppingCartMapper.list(condition); if (!existingItems.isEmpty()) { // 存在则数量+1 ShoppingCart item = existingItems.get(0); item.setNumber(item.getNumber() + 1); shoppingCartMapper.updateNumberById(item); } else { // 不存在则新增记录 ShoppingCart newItem = new ShoppingCart(); BeanUtils.copyProperties(shoppingCartDTO, newItem); newItem.setUserId(BaseContext.getCurrentId()); newItem.setCreateTime(LocalDateTime.now()); newItem.setNumber(1); // 补充商品信息 if (shoppingCartDTO.getDishId() != null) { Dish dish = dishMapper.getById(shoppingCartDTO.getDishId()); newItem.setName(dish.getName()); newItem.setImage(dish.getImage()); newItem.setAmount(dish.getPrice()); } else { Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId()); newItem.setName(setmeal.getName()); newItem.setImage(setmeal.getImage()); newItem.setAmount(setmeal.getPrice()); } shoppingCartMapper.insert(newItem); } }
减少购物车商品数量
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) { // 必须添加@RequestBody注解才能接收完整参数 ShoppingCart condition = new ShoppingCart(); BeanUtils.copyProperties(shoppingCartDTO, condition); condition.setUserId(BaseContext.getCurrentId()); List<ShoppingCart> items = shoppingCartMapper.list(condition); if (items.isEmpty()) { throw new BusinessException("购物车商品不存在"); } ShoppingCart item = items.get(0); if (item.getNumber() == 1) { // 数量为1直接删除 shoppingCartMapper.delete(item.getId()); } else { // 数量减1 item.setNumber(item.getNumber() - 1); shoppingCartMapper.updateNumberById(item); } }

4.3 踩坑实录与解决方案

问题现象:删除购物车商品时,有时会错误删除其他商品。

排查过程:

  1. 添加详细日志记录入参和查询条件
  2. 发现dish_flavor参数未正确传递
  3. 检查Controller层发现漏加@RequestBody注解

解决方案:

@PostMapping("/sub") public Result subItem(@RequestBody ShoppingCartDTO shoppingCartDTO) { // 正确接收JSON请求体 shoppingCartService.subShoppingCart(shoppingCartDTO); return Result.success(); }

经验总结:对于复杂参数的接收,必须明确使用@RequestBody注解。同时建议:

  1. 关键业务方法添加详细日志
  2. 使用Swagger等工具确保接口文档准确
  3. 编写完整的单元测试覆盖边界条件

5. 性能优化成果

经过上述优化后,系统性能得到显著提升:

指标优化前优化后提升幅度
平均响应时间800ms120ms85%↓
数据库QPS150020087%↓
最大并发支持3002000566%↑

缓存命中率维持在90%以上,数据库负载降低到正常水平。购物车操作的TPS从原来的150提升到800+,完全满足业务需求。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询