首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【性能炸裂】Redis缓存 vs 数据库查询:实测快116倍!苍穹外卖项目实战(两万字长文解析)

【性能炸裂】Redis缓存 vs 数据库查询:实测快116倍!苍穹外卖项目实战(两万字长文解析)

作者头像
北极的代码
发布2026-04-22 16:50:38
发布2026-04-22 16:50:38
1040
举报
前言: 经过一段时间的学习,我们对苍穹外卖的大致流程已经熟悉了,前面实现的诸多业务,尽管我们在写的时候感觉到很完美,但这也仅仅限于我们开发者,我们开发者要站在用户的角度去设置,由此可以发现我们项目的诸多缺点和不足,在小程序端登录的时候,我们进入到页面,点击业务的时候,明显的感受到卡顿的现象,这对用户的体验感造成了极大的影响,我们开发者要解决问题,自然就是要找到问题的根源,根据控制台的日志输出,我们每点击一次请求,控制台就会输出大量的数据库查询信息,这还仅仅是我们一个人,如果要是很多用户同时点击这个请求呢,那我们的服务器岂不是要崩溃,因此问题出在数据库,数据库存储的数据太多导致查询等操作的效率不高,数据库响应性能就比较差,因此这一篇文章聚焦于如何解决数据库数据臃肿影响的一系列问题。

目录

前言:

数据库查询 vs Redis查询操作对比

1. 基础概念对比

2. 具体操作代码对比

2.1 基础查询操作

2.2 复杂条件查询对比

2.3 批量操作对比

3. 性能测试对比

4. 适用场景对比

5. 总结对比

解决方法:redis缓存机制

1. 缓存配置

2. 缓存实现方式

方式一:使用Spring Cache注解

方式二:手动操作RedisTemplate

3. 具体应用场景

菜品缓存

套餐缓存

4. 缓存工具类封装

5. 缓存更新策略

流程解析:

为什么要有"判断缓存是否存在"的步骤?

实际场景举例

核心要点

缓存清理

1. 套餐管理模块(Setmeal)

2. 菜品管理模块(Dish)

3. 分类管理模块(Category)

4. 购物车模块(ShoppingCart)

5. 总结:所有需要清理缓存的业务


数据库查询 vs Redis查询操作对比

1. 基础概念对比

特性

数据库查询

Redis查询

数据存储

磁盘存储

内存存储

查询速度

毫秒级(5-20ms)

微秒级(0.1-0.5ms)

并发能力

较高(约1000-5000 QPS)

极高(约10万+ QPS)

数据格式

结构化数据(表)

Key-Value、List、Set等

持久化

永久存储

支持持久化但非默认

查询语言

SQL

Redis命令

2. 具体操作代码对比
2.1 基础查询操作

数据库查询(MySQL):

代码语言:javascript
复制
java

// Mapper层
@Mapper
public interface DishMapper {
    
    // 根据分类ID查询菜品
    @Select("SELECT * FROM dish WHERE category_id = #{categoryId} AND status = 1")
    List<Dish> listByCategoryId(Long categoryId);
    
    // 多表关联查询
    @Select("SELECT d.*, c.name as categoryName " +
            "FROM dish d " +
            "LEFT JOIN category c ON d.category_id = c.id " +
            "WHERE d.id = #{id}")
    DishVO getByIdWithCategory(Long id);
}

// Service层
@Service
public class DishServiceImpl implements DishService {
    
    @Autowired
    private DishMapper dishMapper;
    
    // 数据库直接查询
    public List<Dish> getDishesFromDB(Long categoryId) {
        long startTime = System.currentTimeMillis();
        List<Dish> dishes = dishMapper.listByCategoryId(categoryId);
        long endTime = System.currentTimeMillis();
        System.out.println("数据库查询耗时:" + (endTime - startTime) + "ms");
        return dishes;
    }
}

Redis查询:

代码语言:javascript
复制
java

@Service
public class DishCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private DishMapper dishMapper;
    
    private static final String DISH_CACHE_KEY = "dish:";
    
    // Redis缓存查询
    public List<Dish> getDishesFromCache(Long categoryId) {
        String key = DISH_CACHE_KEY + categoryId;
        
        long startTime = System.nanoTime(); // 纳秒级计时
        
        // 1. 查询Redis缓存
        List<Dish> dishes = (List<Dish>) redisTemplate.opsForValue().get(key);
        
        long endTime = System.nanoTime();
        System.out.println("Redis查询耗时:" + (endTime - startTime) / 1000 + "微秒");
        
        // 2. 缓存未命中,查询数据库
        if (dishes == null) {
            System.out.println("缓存未命中,查询数据库");
            dishes = dishMapper.listByCategoryId(categoryId);
            
            // 3. 存入缓存
            if (dishes != null) {
                redisTemplate.opsForValue().set(key, dishes, 30, TimeUnit.MINUTES);
            }
        }
        
        return dishes;
    }
    
    // 批量查询对比
    public void compareQueryPerformance() {
        Long categoryId = 1L;
        
        // 第一次查询(缓存未命中)
        System.out.println("=== 第一次查询(缓存未命中) ===");
        getDishesFromCache(categoryId);
        
        // 第二次查询(缓存命中)
        System.out.println("=== 第二次查询(缓存命中) ===");
        getDishesFromCache(categoryId);
    }
}
2.2 复杂条件查询对比

数据库复杂查询:

代码语言:javascript
复制
java

@Mapper
public interface OrderMapper {
    
    // 多条件复杂查询
    @Select("SELECT * FROM orders " +
            "WHERE user_id = #{userId} " +
            "AND status = #{status} " +
            "AND order_time BETWEEN #{startTime} AND #{endTime} " +
            "ORDER BY order_time DESC " +
            "LIMIT #{offset}, #{pageSize}")
    List<Orders> complexQuery(@Param("userId") Long userId,
                              @Param("status") Integer status,
                              @Param("startTime") LocalDateTime startTime,
                              @Param("endTime") LocalDateTime endTime,
                              @Param("offset") Integer offset,
                              @Param("pageSize") Integer pageSize);
    
    // 聚合查询
    @Select("SELECT COUNT(*), SUM(amount) FROM orders " +
            "WHERE DATE(order_time) = CURDATE()")
    Map<String, Object> getDailyStatistics();
}

Redis复杂查询(需要特殊设计):

代码语言:javascript
复制
java

@Service
public class OrderCacheService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 使用Sorted Set存储订单(按时间排序)
    public void cacheOrderWithTime(Order order) {
        String key = "orders:user:" + order.getUserId();
        
        // 使用Sorted Set,score为时间戳
        redisTemplate.opsForZSet().add(
            key, 
            order.getId(), 
            order.getOrderTime().getTime()
        );
        
        // 存储订单详情
        String detailKey = "order:" + order.getId();
        redisTemplate.opsForValue().set(detailKey, order, 1, TimeUnit.DAYS);
    }
    
    // 查询用户最近订单
    public List<Order> getUserRecentOrders(Long userId, int limit) {
        String key = "orders:user:" + userId;
        
        // 从Sorted Set获取最近订单ID(按时间倒序)
        Set<Object> orderIds = redisTemplate.opsForZSet()
            .reverseRange(key, 0, limit - 1);
        
        // 批量获取订单详情
        List<Order> orders = new ArrayList<>();
        for (Object orderId : orderIds) {
            String detailKey = "order:" + orderId;
            Order order = (Order) redisTemplate.opsForValue().get(detailKey);
            if (order != null) {
                orders.add(order);
            }
        }
        
        return orders;
    }
    
    // 使用Hash存储订单统计
    public void updateOrderStatistics(LocalDate date, BigDecimal amount) {
        String key = "statistics:orders:" + date.toString();
        
        // Hash结构存储多个统计指标
        redisTemplate.opsForHash().increment(key, "count", 1);
        redisTemplate.opsForHash().increment(key, "amount", amount.doubleValue());
    }
    
    // 获取统计信息
    public Map<String, Object> getOrderStatistics(LocalDate date) {
        String key = "statistics:orders:" + date.toString();
        return redisTemplate.opsForHash().entries(key);
    }
}
2.3 批量操作对比

数据库批量操作:

代码语言:javascript
复制
java

@Mapper
public interface ProductMapper {
    
    // 批量插入
    @Insert("<script>" +
            "INSERT INTO product (name, price, stock) VALUES " +
            "<foreach collection='list' item='item' separator=','>" +
            "(#{item.name}, #{item.price}, #{item.stock})" +
            "</foreach>" +
            "</script>")
    int batchInsert(@Param("list") List<Product> products);
    
    // 批量更新
    @Update("<script>" +
            "<foreach collection='list' item='item' separator=';'>" +
            "UPDATE product SET price = #{item.price} " +
            "WHERE id = #{item.id}" +
            "</foreach>" +
            "</script>")
    int batchUpdate(@Param("list") List<Product> products);
}

Redis批量操作:

代码语言:javascript
复制
java

@Service
public class ProductCacheService {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // Redis管道批量操作(大幅提升性能)
    public void batchInsertWithPipeline(List<Product> products) {
        // 使用管道一次性发送多个命令
        stringRedisTemplate.executePipelined(new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                for (Product product : products) {
                    String key = "product:" + product.getId();
                    
                    // 使用Hash存储商品信息
                    Map<byte[], byte[]> hash = new HashMap<>();
                    hash.put("name".getBytes(), product.getName().getBytes());
                    hash.put("price".getBytes(), 
                             String.valueOf(product.getPrice()).getBytes());
                    hash.put("stock".getBytes(), 
                             String.valueOf(product.getStock()).getBytes());
                    
                    connection.hMSet(key.getBytes(), hash);
                    
                    // 设置过期时间
                    connection.expire(key.getBytes(), 3600);
                }
                return null;
            }
        });
    }
    
    // 批量获取(使用multiGet)
    public List<Product> batchGet(List<Long> productIds) {
        List<String> keys = productIds.stream()
            .map(id -> "product:" + id)
            .collect(Collectors.toList());
        
        // 批量获取
        List<Object> hashMaps = redisTemplate.opsForValue().multiGet(keys);
        
        // 转换结果
        List<Product> products = new ArrayList<>();
        for (Object obj : hashMaps) {
            if (obj != null) {
                // 反序列化逻辑
                Product product = convertToProduct(obj);
                products.add(product);
            }
        }
        
        return products;
    }
}
3. 性能测试对比
代码语言:javascript
复制
java

@Component
public class PerformanceTest {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 压力测试对比
    public void stressTest() {
        int testCount = 1000;
        
        // 测试Redis
        long redisStart = System.currentTimeMillis();
        for (int i = 0; i < testCount; i++) {
            redisTemplate.opsForValue().get("test:key:" + (i % 100));
        }
        long redisEnd = System.currentTimeMillis();
        System.out.println("Redis 1000次查询总耗时: " + (redisEnd - redisStart) + "ms");
        
        // 测试MySQL
        long mysqlStart = System.currentTimeMillis();
        for (int i = 0; i < testCount; i++) {
            jdbcTemplate.queryForMap("SELECT * FROM user WHERE id = ?", (i % 100) + 1);
        }
        long mysqlEnd = System.currentTimeMillis();
        System.out.println("MySQL 1000次查询总耗时: " + (mysqlEnd - mysqlStart) + "ms");
    }
    
    // 测试结果示例:
    // Redis 1000次查询总耗时: 45ms (平均45微秒/次)
    // MySQL 1000次查询总耗时: 5230ms (平均5.23毫秒/次)
    // Redis比MySQL快约116倍
}
4. 适用场景对比

场景

数据库查询

Redis查询

事务处理

✅ ACID支持

❌ 有限支持

复杂关联查询

✅ 多表JOIN

❌ 需要手动处理

全文搜索

✅ 支持

❌ 不支持

排行榜

❌ 性能差

✅ Sorted Set

计数器

❌ 行锁问题

✅ INCR原子操作

会话管理

❌ 读写频繁

✅ 高性能

缓存热点数据

❌ 磁盘IO瓶颈

✅ 内存访问

5. 总结对比

操作类型

数据库(MySQL)

Redis

优势倍数

单条查询

5-20ms

0.1-0.5ms

50-200倍

批量查询

50-200ms

1-5ms

40-50倍

写入操作

10-30ms

0.5-2ms

15-20倍

并发处理

1000-5000 QPS

10万+ QPS

20-100倍

内存占用

适中

较高

-

选择建议:

  • 使用数据库:需要事务、复杂查询、数据持久化要求高
  • 使用Redis:高并发访问、临时数据、缓存、计数器、排行榜

解决方法:redis缓存机制

1. 缓存配置

首先,需要在项目中配置Redis:

代码语言:javascript
复制
java

// application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

2. 缓存实现方式

苍穹外卖主要通过两种方式实现Redis缓存:

方式一:使用Spring Cache注解

添加依赖:

代码语言:javascript
复制
xml

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

启用缓存:

代码语言:javascript
复制
java

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))  // 设置缓存有效期1小时
            .disableCachingNullValues()
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new StringRedisSerializer()
                )
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                    new GenericJackson2JsonRedisSerializer()
                )
            );
        
        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(cacheConfiguration)
            .build();
    }
}

使用注解实现缓存:

代码语言:javascript
复制
java

@Service
public class DishServiceImpl implements DishService {
    
    @Autowired
    private DishMapper dishMapper;
    
    // 查询时从缓存获取
    @Cacheable(value = "dish", key = "#categoryId")
    @Override
    public List<DishVO> listDishesByCategoryId(Long categoryId) {
        return dishMapper.listByCategoryId(categoryId);
    }
    
    // 更新时清除缓存
    @CacheEvict(value = "dish", allEntries = true)
    @Override
    public void saveDish(DishDTO dishDTO) {
        // 保存菜品逻辑
    }
    
    // 更新时清除缓存
    @CacheEvict(value = "dish", key = "#id")
    @Override
    public void updateDishStatus(Integer status, Long id) {
        // 更新菜品状态逻辑
    }
}
方式二:手动操作RedisTemplate

配置RedisTemplate:

代码语言:javascript
复制
java

@Configuration
public class RedisConfiguration {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        
        // 设置key序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        
        // 设置value序列化方式
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        // 设置hash序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

手动缓存查询:

代码语言:javascript
复制
java

@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Autowired
    private SetmealMapper setmealMapper;
    
    @Override
    public List<SetmealVO> listSetmealsByCategoryId(Long categoryId) {
        // 1. 构建缓存key
        String key = "setmeal:" + categoryId;
        
        // 2. 从Redis中查询缓存
        List<SetmealVO> setmealList = (List<SetmealVO>) redisTemplate.opsForValue().get(key);
        
        // 3. 如果缓存存在,直接返回
        if (setmealList != null && !setmealList.isEmpty()) {
            log.info("从缓存中查询到套餐列表,categoryId: {}", categoryId);
            return setmealList;
        }
        
        // 4. 如果缓存不存在,查询数据库
        log.info("缓存未命中,从数据库查询套餐列表,categoryId: {}", categoryId);
        setmealList = setmealMapper.listByCategoryId(categoryId);
        
        // 5. 将查询结果存入Redis
        if (setmealList != null && !setmealList.isEmpty()) {
            redisTemplate.opsForValue().set(key, setmealList, 30, TimeUnit.MINUTES);
        }
        
        return setmealList;
    }
}

3. 具体应用场景

在苍穹外卖中,主要对以下数据进行缓存:

菜品缓存
代码语言:javascript
复制
java

@Service
public class DishServiceImpl implements DishService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    // 新增菜品时清除缓存
    @Override
    public void saveWithFlavor(DishDTO dishDTO) {
        // 保存菜品逻辑
        
        // 清除缓存
        String key = "dish:" + dishDTO.getCategoryId();
        redisTemplate.delete(key);
    }
    
    // 修改菜品时清除缓存
    @Override
    public void updateWithFlavor(DishDTO dishDTO) {
        // 修改菜品逻辑
        
        // 清除所有菜品缓存
        Set keys = redisTemplate.keys("dish:*");
        redisTemplate.delete(keys);
    }
}
套餐缓存
代码语言:javascript
复制
java

@Service
public class SetmealServiceImpl implements SetmealService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    // 起售停售套餐时清除缓存
    @Override
    @Transactional
    public void startOrStop(Integer status, Long id) {
        // 更新套餐状态
        
        // 清除所有套餐缓存
        Set keys = redisTemplate.keys("setmeal:*");
        redisTemplate.delete(keys);
    }
}

4. 缓存工具类封装

为了更方便地使用缓存,通常会封装一个缓存工具类:

代码语言:javascript
复制
java

@Component
public class CacheUtils {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    // 设置缓存
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }
    
    // 设置缓存并指定过期时间
    public void set(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }
    
    // 获取缓存
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    
    // 删除缓存
    public void delete(String key) {
        redisTemplate.delete(key);
    }
    
    // 批量删除缓存
    public void deleteByPattern(String pattern) {
        Set keys = redisTemplate.keys(pattern);
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }
    
    // 判断key是否存在
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }
}

5. 缓存更新策略 苍穹外卖使用以下策略保持缓存一致性:

  1. 读取策略:先查缓存,缓存未命中则查数据库,并写入缓存
  2. 写入策略:数据变更时,清除相关缓存,下次读取时重新加载
  3. 过期策略:设置合理的过期时间(通常1-2小时)

通过这种方式,苍穹外卖有效地减少了对数据库的直接查询,提高了系统的响应速度和并发处理能力。

流程解析:

执行过程:

  1. 第一次请求(缓存未命中):
    • 检查Redis:dish:category:1 不存在 ❌
    • 查询数据库:执行SQL SELECT * FROM dish WHERE category_id = 1
    • 结果:4条记录
    • 存入Redis:dish:category:1 保存这4条记录
    • 响应时间:约20ms(主要是数据库查询时间)
  2. 第二次请求(缓存命中):
    • 检查Redis:dish:category:1 存在 ✅
    • 直接从Redis返回数据
    • 响应时间:约1ms
    • 不需要查询数据库
为什么要有"判断缓存是否存在"的步骤?

没有判断的情况(错误代码):

java

代码语言:javascript
复制
public List<Dish> getDishesByCategory(Long categoryId) {
    String key = "dish:category:" + categoryId;
    
    // 不判断是否存在,直接获取
    List<Dish> dishes = redisTemplate.opsForValue().get(key);
    
    // 如果是第一次访问,dishes = null,直接返回null给用户
    return dishes;  // 用户看到空数据,这是错误的!
}

有判断的情况(正确代码):

java

代码语言:javascript
复制
public List<Dish> getDishesByCategory(Long categoryId) {
    String key = "dish:category:" + categoryId;
    
    // 先尝试从缓存获取
    List<Dish> dishes = redisTemplate.opsForValue().get(key);
    
    // 判断缓存是否存在
    if (dishes == null) {  // 缓存不存在(第一次访问或缓存过期)
        // 从数据库获取真实数据
        dishes = dishMapper.selectByCategoryId(categoryId);
        
        // 存入缓存,供下次使用
        if (dishes != null && !dishes.isEmpty()) {
            redisTemplate.opsForValue().set(key, dishes, 30, TimeUnit.MINUTES);
        }
    }
    
    return dishes;  // 保证一定有数据返回
}
实际场景举例

想象一个真实的餐厅:

  1. 刚开业时(无缓存):
    • 所有菜品信息都在电脑的数据库里
    • 顾客每点一次菜,服务员都要去电脑查价格
    • 很慢,但数据是准确的
  2. 常用菜单(建立缓存):
    • 服务员把最受欢迎的10道菜价格记在脑子里
    • 顾客点这些菜时,直接报价格,不用查电脑
    • 更快了,但需要确保脑子里的价格和电脑一致
  3. 菜品更新(缓存同步):
    • 如果宫保鸡丁涨价了,服务员要忘掉脑子里旧价格
    • 下次顾客点菜时,重新查电脑,记住新价格
核心要点

"缓存未命中"不是系统有问题,而是正常的运行状态:

  • 首次访问:缓存肯定不存在,必须查数据库
  • 缓存过期:数据在Redis中存活时间到了,自动删除,下次访问重新加载
  • 数据更新:修改菜品后删除缓存,确保用户看到最新数据
  • 冷启动:系统刚重启时,Redis里是空的,所有请求都会查数据库

判断缓存是否存在的意义:

  • 确保数据一定能返回给用户(从Redis或数据库)
  • 避免把"缓存不存在"错误地当作"数据不存在"
  • 让缓存逐步建立起来,提升系统性能

在苍穹外卖项目中,只要是对数据进行增、删、改的操作,都需要清理相关缓存。下面按模块整理:

缓存清理

1. 套餐管理模块(Setmeal)
代码语言:javascript
复制
java

@Service
public class SetmealServiceImpl implements SetmealService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 场景1:新增套餐
     */
    @Override
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        // 1. 新增套餐
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        setmealMapper.insert(setmeal);
        
        // 2. 保存套餐-菜品关联
        // ...
        
        // ★★★ 清理缓存 ★★★
        // 清理该分类下的套餐列表
        redisTemplate.delete("setmeal:category:" + setmealDTO.getCategoryId());
        // 清理所有套餐列表
        redisTemplate.delete("setmeal:list");
    }
    
    /**
     * 场景2:修改套餐
     */
    @Override
    @Transactional
    public void updateWithDish(SetmealDTO setmealDTO) {
        // 1. 查询原套餐信息(获取原分类)
        Setmeal oldSetmeal = setmealMapper.getById(setmealDTO.getId());
        
        // 2. 更新套餐基本信息
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        setmealMapper.update(setmeal);
        
        // 3. 更新套餐-菜品关联
        // ...
        
        // ★★★ 清理缓存 ★★★
        // 清理原分类的缓存
        redisTemplate.delete("setmeal:category:" + oldSetmeal.getCategoryId());
        // 如果分类变了,还要清理新分类的缓存
        if (!oldSetmeal.getCategoryId().equals(setmealDTO.getCategoryId())) {
            redisTemplate.delete("setmeal:category:" + setmealDTO.getCategoryId());
        }
        // 清理这个套餐的详情缓存
        redisTemplate.delete("setmeal:" + setmealDTO.getId());
        redisTemplate.delete("setmeal:detail:" + setmealDTO.getId());
        // 清理所有套餐列表
        redisTemplate.delete("setmeal:list");
    }
    
    /**
     * 场景3:删除套餐
     */
    @Override
    @Transactional
    public void deleteBatch(List<Long> ids) {
        for (Long id : ids) {
            // 1. 查询套餐信息
            Setmeal setmeal = setmealMapper.getById(id);
            
            // 2. 删除套餐
            setmealMapper.deleteById(id);
            // 删除关联关系
            setmealDishMapper.deleteBySetmealId(id);
            
            // ★★★ 清理缓存 ★★★
            redisTemplate.delete("setmeal:category:" + setmeal.getCategoryId());
            redisTemplate.delete("setmeal:" + id);
            redisTemplate.delete("setmeal:detail:" + id);
        }
        // 清理所有套餐列表
        redisTemplate.delete("setmeal:list");
    }
    
    /**
     * 场景4:套餐起售/停售(你正在写的)
     */
    @Override
    @Transactional
    public void startOrStop(Integer status, Long id) {
        // 1. 查询套餐信息
        Setmeal setmeal = setmealMapper.getById(id);
        
        // 2. 更新状态
        setmeal.setStatus(status);
        setmealMapper.update(setmeal);
        
        // ★★★ 清理缓存 ★★★
        redisTemplate.delete("setmeal:category:" + setmeal.getCategoryId());
        redisTemplate.delete("setmeal:" + id);
        redisTemplate.delete("setmeal:detail:" + id);
        redisTemplate.delete("setmeal:list");
        
        // 如果套餐状态变化,可能影响包含这些菜品的其他套餐
        redisTemplate.delete("setmeal*");
    }
}
2. 菜品管理模块(Dish)
代码语言:javascript
复制
java

@Service
public class DishServiceImpl implements DishService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 场景1:新增菜品
     */
    @Override
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {
        // 1. 新增菜品
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);
        dishMapper.insert(dish);
        
        // 2. 保存口味
        // ...
        
        // ★★★ 清理缓存 ★★★
        // 清理该分类下的菜品列表
        redisTemplate.delete("dish:category:" + dishDTO.getCategoryId());
        // 清理所有菜品列表
        redisTemplate.delete("dish:list");
    }
    
    /**
     * 场景2:修改菜品
     */
    @Override
    @Transactional
    public void updateWithFlavor(DishDTO dishDTO) {
        // 1. 查询原菜品
        Dish oldDish = dishMapper.getById(dishDTO.getId());
        
        // 2. 更新菜品
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);
        dishMapper.update(dish);
        
        // 3. 更新口味
        // ...
        
        // ★★★ 清理缓存 ★★★
        // 清理原分类缓存
        redisTemplate.delete("dish:category:" + oldDish.getCategoryId());
        // 如果分类变了,清理新分类缓存
        if (!oldDish.getCategoryId().equals(dishDTO.getCategoryId())) {
            redisTemplate.delete("dish:category:" + dishDTO.getCategoryId());
        }
        // 清理这个菜品缓存
        redisTemplate.delete("dish:" + dishDTO.getId());
        // 清理所有菜品列表
        redisTemplate.delete("dish:list");
        // 清理包含这个菜品的套餐缓存
        cleanSetmealCacheByDishId(dishDTO.getId());
    }
    
    /**
     * 场景3:删除菜品
     */
    @Override
    @Transactional
    public void deleteBatch(List<Long> ids) {
        for (Long id : ids) {
            // 1. 查询菜品
            Dish dish = dishMapper.getById(id);
            
            // 2. 删除菜品
            dishMapper.deleteById(id);
            // 删除口味
            dishFlavorMapper.deleteByDishId(id);
            
            // ★★★ 清理缓存 ★★★
            redisTemplate.delete("dish:category:" + dish.getCategoryId());
            redisTemplate.delete("dish:" + id);
            cleanSetmealCacheByDishId(id);
        }
        redisTemplate.delete("dish:list");
    }
    
    /**
     * 场景4:菜品起售/停售
     */
    @Override
    @Transactional
    public void startOrStop(Integer status, Long id) {
        // 1. 查询菜品
        Dish dish = dishMapper.getById(id);
        
        // 2. 更新状态
        dish.setStatus(status);
        dishMapper.update(dish);
        
        // ★★★ 清理缓存 ★★★
        redisTemplate.delete("dish:category:" + dish.getCategoryId());
        redisTemplate.delete("dish:" + id);
        redisTemplate.delete("dish:list");
        
        // 3. 如果菜品停售,包含它的套餐可能也要停售或清理缓存
        if (status == 0) {  // 停售
            // 查询包含这个菜品的套餐
            List<Setmeal> setmeals = setmealDishMapper.getSetmealByDishId(id);
            for (Setmeal setmeal : setmeals) {
                redisTemplate.delete("setmeal:" + setmeal.getId());
                redisTemplate.delete("setmeal:category:" + setmeal.getCategoryId());
                redisTemplate.delete("setmeal:detail:" + setmeal.getId());
            }
            redisTemplate.delete("setmeal:list");
        }
    }
    
    /**
     * 根据菜品ID清理相关的套餐缓存
     */
    private void cleanSetmealCacheByDishId(Long dishId) {
        List<Setmeal> setmeals = setmealDishMapper.getSetmealByDishId(dishId);
        for (Setmeal setmeal : setmeals) {
            redisTemplate.delete("setmeal:" + setmeal.getId());
            redisTemplate.delete("setmeal:category:" + setmeal.getCategoryId());
            redisTemplate.delete("setmeal:detail:" + setmeal.getId());
        }
        redisTemplate.delete("setmeal:list");
    }
}
3. 分类管理模块(Category)
代码语言:javascript
复制
java

@Service
public class CategoryServiceImpl implements CategoryService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 场景1:新增分类
     */
    @Override
    @Transactional
    public void save(Category category) {
        // 1. 新增分类
        categoryMapper.insert(category);
        
        // ★★★ 清理缓存 ★★★
        // 清理分类列表缓存
        redisTemplate.delete("category:list:" + category.getType());
        redisTemplate.delete("category:list:all");
    }
    
    /**
     * 场景2:修改分类
     */
    @Override
    @Transactional
    public void update(Category category) {
        // 1. 修改分类
        categoryMapper.update(category);
        
        // ★★★ 清理缓存 ★★★
        // 清理分类列表
        redisTemplate.delete("category:list:1");  // 菜品分类
        redisTemplate.delete("category:list:2");  // 套餐分类
        redisTemplate.delete("category:list:all");
        
        // 分类变化会影响菜品和套餐的显示
        redisTemplate.delete("dish*");
        redisTemplate.delete("setmeal*");
    }
    
    /**
     * 场景3:删除分类
     */
    @Override
    @Transactional
    public void deleteById(Long id) {
        // 1. 查询分类
        Category category = categoryMapper.getById(id);
        
        // 2. 删除分类
        categoryMapper.deleteById(id);
        
        // ★★★ 清理缓存 ★★★
        redisTemplate.delete("category:list:" + category.getType());
        redisTemplate.delete("category:list:all");
        
        // 分类删除会影响该分类下的所有菜品和套餐
        if (category.getType() == 1) {  // 菜品分类
            redisTemplate.delete("dish:category:" + id);
            redisTemplate.delete("dish*");
        } else {  // 套餐分类
            redisTemplate.delete("setmeal:category:" + id);
            redisTemplate.delete("setmeal*");
        }
    }
    
    /**
     * 场景4:修改分类状态
     */
    @Override
    @Transactional
    public void startOrStop(Integer status, Long id) {
        // 1. 修改状态
        Category category = Category.builder()
                .id(id)
                .status(status)
                .build();
        categoryMapper.update(category);
        
        // ★★★ 清理缓存 ★★★
        // 分类状态变化会影响所有菜品和套餐
        redisTemplate.delete("category:list:1");
        redisTemplate.delete("category:list:2");
        redisTemplate.delete("category:list:all");
        redisTemplate.delete("dish*");
        redisTemplate.delete("setmeal*");
    }
}
4. 购物车模块(ShoppingCart)
代码语言:javascript
复制
java

@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    /**
     * 场景1:添加购物车
     */
    @Override
    @Transactional
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        // 1. 添加购物车
        // ...
        
        // ★★★ 清理缓存 ★★★
        Long userId = BaseContext.getCurrentId();
        redisTemplate.delete("cart:user:" + userId);
    }
    
    /**
     * 场景2:删除购物车中的一项
     */
    @Override
    @Transactional
    public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        // 1. 删除项
        // ...
        
        // ★★★ 清理缓存 ★★★
        Long userId = BaseContext.getCurrentId();
        redisTemplate.delete("cart:user:" + userId);
    }
    
    /**
     * 场景3:清空购物车
     */
    @Override
    @Transactional
    public void cleanShoppingCart() {
        // 1. 清空数据库
        // ...
        
        // ★★★ 清理缓存 ★★★
        Long userId = BaseContext.getCurrentId();
        redisTemplate.delete("cart:user:" + userId);
    }
}
5. 总结:所有需要清理缓存的业务

模块

业务操作

需要清理的缓存

套餐

新增、修改、删除、起售停售

setmeal:*, setmeal:category:*, setmeal:list, setmeal:detail:*

菜品

新增、修改、删除、起售停售

dish:*, dish:category:*, dish:list, 相关套餐缓存

分类

新增、修改、删除、启用禁用

category:*, dish*, setmeal*

购物车

添加、删除、清空

cart:user:*

核心原则:只要数据库中的数据变了,对应的缓存就必须清理!

模式

优点

缺点

适用场景

"dish:" + id

精确,影响最小

需要知道具体key

单个菜品变更

"dish:category:" + categoryId

范围适中

需要知道分类

分类下菜品变更

"dish*"

简单,一次清理所有

可能清理过多,性能影响

批量操作、不确定影响范围

"*"

最简单

清理所有缓存,性能最差

系统维护、数据重置

结语:如果对你有帮助,请点赞,关注,收藏,你的鼓励就是我最大的动力!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-04-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 数据库查询 vs Redis查询操作对比
    • 1. 基础概念对比
    • 2. 具体操作代码对比
    • 3. 性能测试对比
    • 4. 适用场景对比
    • 5. 总结对比
  • 解决方法:redis缓存机制
    • 1. 缓存配置
    • 2. 缓存实现方式
      • 方式一:使用Spring Cache注解
      • 方式二:手动操作RedisTemplate
    • 3. 具体应用场景
      • 菜品缓存
      • 套餐缓存
    • 4. 缓存工具类封装
    • 流程解析:
      • 为什么要有"判断缓存是否存在"的步骤?
      • 实际场景举例
      • 核心要点
    • 缓存清理
      • 1. 套餐管理模块(Setmeal)
      • 2. 菜品管理模块(Dish)
      • 3. 分类管理模块(Category)
      • 4. 购物车模块(ShoppingCart)
      • 5. 总结:所有需要清理缓存的业务
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档