苍穹外卖项目实战(day7-1)-缓存菜品和缓存套餐功能-记录实战教程、问题的解决方法以及完整代码


完整资料下载

通过网盘分享的文件:苍穹外卖

链接:

https://pan.baidu.com/s/1JJaFOodXOF_lNJSUiZ6qtw?pwd=ps2t

提取码: ps2t


目录

1、缓存菜品

(1)问题说明

(2)使用redis缓存部分数据

1-2、代码完善

(1)user包下DishController完善

(2)RedisConfiguration完善

(3)admin包下DishController

(4)测试功能

2、缓存套餐

[Spring Cache知识点](#Spring Cache知识点)

2-2、代码完善

(1)user/SetmealController完善

(2)admin/SetmealController完善

(3)功能测试


1、缓存菜品

(1)问题说明

(2)使用redis缓存部分数据

注意:

1、访问mysql是磁盘IO操作,访问redis是访问内存

2、用户点单的购物车中的菜品需要存储,这个储存不需要给服务器端,只有最后点付钱才会给服务器提交数据所以这里才有Redis来暂存

3、面试题:如何保持radis与数据库中的数据一致性:设置过期时间 延迟双删

4、在套菜中,套餐是一个集合list,我们将list集合进行序列化,转成Redis的String类型再存进Redis中;

redis的string不是java的string,redis的string算是object了,外部任何数据传入redis都会被转换为string

5、注意:小程序的图片默认访问的是老师的oss,直接去数据库把图片oss链接全改成自己的,或者在管理端(前端页面)重新上传图片

6、 从redis中获取商铺营业状态

需要提前在redis中设置key为shop_status,value为1或0,1表示营业中,0表示打烊中,否则会报错,错误提示为

"Cannot invoke "java.lang.Integer.intValue()" because "status" is null"

7、现在有两个问题;

(1)如果数据库数据变动了怎么办?

数据库的数据变动了,做新增修改的时候都需要先更新数据库,然后删除缓存,延时双删,每次查询再把数据存入redis。

(2)如果不断存入新数据,数据不会老化那么总有一天内存会爆满的,如何处理?

设置超时时间,到期了数据就会消失,还可以更改redis的淘汰策略

1-2、代码完善

(1)user包下DishController完善

位置:sky-server/src/main/java/com/sky/controller/user/DishController.java

完整代码:

复制代码
package com.sky.controller.user;
 
import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
 
@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;
 
    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        log.info("根据分类id查询菜品,categoryId={}", categoryId);
        // 查询Redis缓存
        //构造缓存key
        String key = "dish_" + categoryId;
 
        //从缓存中获取数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
 
        // 判断是否存在缓存,如果存在,则直接返回缓存数据,无需再次查询数据库
        if (list != null && list.size() > 0) {
            log.info("查询Redis缓存,key={},list={}", key, list);
            return Result.success(list);
        }
 
        // 查询数据库
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
 
        //如果缓存中没有数据,则查询数据库
        list = dishService.listWithFlavor(dish);
        log.info("查询数据库,categoryId={},list={}", categoryId, list);
 
        // 保存到Redis缓存
        redisTemplate.opsForValue().set(key, list);
 
        return Result.success(list);
    }
 
}

示意图:


启动项目时遇见的错误:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: java.util.ArrayList[0]->com.sky.vo.DishVO["updateTime"])


原因:

这个错误是因为 Jackson 默认不支持 Java 8 的日期时间类型(如LocalDateTime)序列化导致的。错误信息已经提示了解决方案:需要添加jackson-datatype-jsr310模块来支持 Java 8 日期时间类型的处理。


解决方法一:

在RedisTemplate,设置了键和值的序列化方式。从错误来看,这个配置中缺少了对 Java 8 日期时间类型(如LocalDateTime)的支持,需要添加JavaTimeModule来解决序列化问题。代码在下面的"(2)RedisConfiguration完善"有步骤

解决方法二:

取消对值的序列化,只保留对key的序列化,这样就可以避免日期时间类型(如LocalDateTime)序列化导致的错误

在小程序查看菜品是,Redis就会缓存对应的数据:值没有序列化

(2)RedisConfiguration完善

位置:sky-server/src/main/java/com/sky/config/RedisConfiguration.java

添加的代码:

复制代码
    // 注册JavaTimeModule以支持LocalDateTime等Java 8日期类型
    objectMapper.registerModule(new JavaTimeModule());

文件完整代码:

复制代码
package com.sky.config;
 
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
@Configuration
@Slf4j
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        log.info("初始化创建Redis模板对象...");
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
 
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
 
        // 注册JavaTimeModule以支持LocalDateTime等Java 8日期类型
        objectMapper.registerModule(new JavaTimeModule());
 
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
 
        // 设置key的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
 
        // 设置hash类型的序列化方式
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
 
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

示意图:

查看Redis数据库:键和值都已经序列化

(3)admin包下DishController

传入的菜品id,redis存的key是类别id,如果我们想删除菜品对应分类的缓存数据 需要我们去查询数据库 也需多次访问数据库 我们的目的是减去数据库操作压力,故我们选择清理所有缓存数据

位置:sky-server/src/main/java/com/sky/controller/admin/DishController.java

新增代码:

复制代码
/**
 * 清除所有Redis缓存
 * @return
 */
 private void clearRedisCache(String pattern) {
    Set keys = redisTemplate.keys(pattern);
    redisTemplate.delete(keys);
 }

完整代码:

复制代码
package com.sky.controller.admin;
 
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
 
import java.util.List;
import java.util.Set;
 
/**
 * 后台菜品管理
 * @author sky
 */
@RestController//声明当前类是一个控制器
@RequestMapping("/admin/dish")//声明当前类下所有请求的前缀
@Api(tags = "后台菜品管理")
@Slf4j//声明当前类使用log4j日志
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 新增菜品
     * (1)@RequestBody:注解用于将请求体中的json数据绑定到对象中,这里的对象就是DishDTO
     * 而且前端发送的数据是Json格式,所以需要将请求体中的json数据绑定到DishDTO对象中
     * (2)使用DishDTO对象来接收前端发送的json数据的原因是:
     * 1. 方便后续处理,比如校验数据,保存到数据库等
     * 2. 前端发送的json数据可能有多余的字段,这些字段在后续的业务逻辑中可能并不需要,
     * 所以使用DishDTO对象来接收前端发送的json数据,可以过滤掉多余的字段,提高后续业务逻辑的效率
     * @param dishDTO
     * @return
     */
    @PostMapping//声明当前方法是一个HTTP POST请求
    @ApiOperation(value = "新增菜品")
    public Result save(@RequestBody DishDTO dishDTO){
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavors(dishDTO);
        //删除Redis缓存
        clearRedisCache("dish_" + dishDTO.getCategoryId());
 
        return Result.success();
    }
 
    /**
     * 分页查询菜品
     * @param dishPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分页查询菜品")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
        log.info("分页查询菜品:{}", dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }
 
    /**
     * 菜品批量删除
     *传入的菜品id,redis存的key是类别id,如果我们想删除菜品对应分类的缓存数据
     * 需要我们去查询数据库 也需多次访问数据库
     * 我们的目的是减去数据库操作压力,故我们选择清理所有缓存数据
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}", ids);
        dishService.deleteBatch(ids);
        //删除所有Redis缓存
        clearRedisCache("dish_*");
 
        return Result.success();
    }
 
 
    /**
     * 根据ID查询菜品详情
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据ID查询菜品详情")
    public Result<DishVO> getById(@PathVariable Long id){
        log.info("查询菜品:{}", id);
        DishVO dishVO = dishService.getByIdWithFlavors(id);
        return Result.success(dishVO);
    }
 
    /**
     * 更新(修改)菜品
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("更新菜品")
    public Result update(@RequestBody DishDTO dishDTO){
        log.info("更新菜品:{}", dishDTO);
        dishService.updateWithFlavors(dishDTO);
//        //删除所有Redis缓存
//        Set keys = redisTemplate.keys("dish_*");
//        redisTemplate.delete(keys);
        //删除所有Redis缓存
        clearRedisCache("dish_*");
 
        return Result.success();
    }
 
    /**
     * 根据分类id查询套餐
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    public Result<List<Dish>> list(Long categoryId){
        log.info("根据分类id查询套餐:{}", categoryId);
        List<Dish> list = dishService.list(categoryId);
        return Result.success(list);
    }
 
    /**
     * 菜品起售停售
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("菜品起售停售")
    public Result<String> startOrStop(@PathVariable Integer status, Long id){
        dishService.startOrStop(status,id);
        //删除所有Redis缓存
        clearRedisCache("dish_*");
        return Result.success();
    }
 
    /**
     * 清除所有Redis缓存
     * @return
     */
     private void clearRedisCache(String pattern) {
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
     }
 
}

示意图:

(4)测试功能

小程序查询菜品分类,如"传统主食",打开Redis数据库,查看缓存的数据,key为"dish_12"

打开前端网页:菜品管理

找到菜品分类为"传统主食"的"馒头",点击修改,修改价格,保存

回到Redis数据库,可以发现缓存全没了,成功!

测试"新增菜品",清理缓存功能,先出小程序,查询"传统主食"及任意多个菜品分类,打开Redis数据库出现"dish_12",去前端网页新增菜品,点击保存,最后查看数据库"dish_12"是否还存在,没有则表示成功!

2、缓存套餐

Spring Cache知识点

SpringCache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。SpringCache提供了一层抽象,底层可以切换不同的缓存实现,例如:

1、EHCache

2、Caffeine
3、Redis

依赖导入:(源文件已有)

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.7.3</version>
</dependency>

我们的项目使用的是Redis。pom文件导入对应的缓存类型坐标,即可切换不同的缓存实现

如:

复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.0.0</version>
</dependency>

2-2、代码完善

源文件的pom文件已导入Redis和spring cache依赖

(1)user/SetmealController完善

位置:sky-server/src/main/java/com/sky/controller/user/SetmealController.java

新增代码:

复制代码
/**
 * 条件查询
 *
 * @param categoryId
 * @return
 */
@Cacheable(cacheNames = "setmealList", key = "#categoryId")
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
public Result<List<Setmeal>> list(Long categoryId) {
    Setmeal setmeal = new Setmeal();
    setmeal.setCategoryId(categoryId);
    setmeal.setStatus(StatusConstant.ENABLE);
 
    List<Setmeal> list = setmealService.list(setmeal);
    return Result.success(list);
}

完整代码:

复制代码
package com.sky.controller.user;
 
import com.sky.constant.StatusConstant;
import com.sky.entity.Setmeal;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
 
import java.util.List;
 
@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;
 
    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @Cacheable(cacheNames = "setmealList", key = "#categoryId")
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);
 
        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }
 
    /**
     * 根据套餐id查询包含的菜品列表
     *
     * @param id
     * @return
     */
    @GetMapping("/dish/{id}")
    @ApiOperation("根据套餐id查询包含的菜品列表")
    public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
        List<DishItemVO> list = setmealService.getDishItemById(id);
        return Result.success(list);
    }
}

示意图:

注意:爆红是因为导错包,选"org.springframework.cache.annotation.Cacheable;"

(2)admin/SetmealController完善

位置:sky-server/src/main/java/com/sky/controller/admin/SetmealController.java

新增代码:

复制代码
@CacheEvict(value = "setmealCache", key = "#setmealDTO.categoryId")//精确匹配key,清理缓存

    @CacheEvict(value = "setmealCache", allEntries = true)//清理所有缓存

完整代码:

复制代码
package com.sky.controller.admin;
 
import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.SetmealVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.web.bind.annotation.*;
 
import java.util.List;
 
/**
 * 套餐管理
 */
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {
    @Autowired
    private SetmealService setmealService;
    /**
     * 新增套餐
     * @param setmealDTO
     * @return
     */
    @CacheEvict(value = "setmealCache", key = "#setmealDTO.categoryId")//精确匹配key,清理缓存
    @PostMapping
    @ApiOperation("新增套餐")
    //由于SetmealDTO中有List<SetmealDish> setmealDishes = new ArrayList<>();,所以这里需要使用@RequestBody注解
    //使用SetmealDTOwenden @RequestBody注解,将json数据绑定到SetmealDTO对象中
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        log.info("新增套餐:{}", setmealDTO);
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
 
    /**
     * 套餐分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分页查询")
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }
 
    /**
     * 批量删除套餐
     * @param ids
     * @return
     */
    @CacheEvict(value = "setmealCache", allEntries = true)
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("批量删除套餐:{}", ids);
        setmealService.delete(ids);
        return Result.success();
    }
 
    /**
     * 根据id获取套餐
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id获取套餐")
    public Result<SetmealVO> getById(@PathVariable("id") Long id){
        log.info("根据id获取套餐:{}", id);
        SetmealVO setmealVO = setmealService.getById(id);
        return Result.success(setmealVO);
    }
 
    /**
     * 更新套餐1
     * @param setmealDTO
     * @return
     */
    @CacheEvict(value = "setmealCache", allEntries = true)
    @PutMapping
    @ApiOperation("更新套餐")
    public Result Update(@RequestBody SetmealDTO setmealDTO) {
        log.info("更新套餐:{}", setmealDTO);
        setmealService.update(setmealDTO);
        return Result.success();
    }
 
    /**
     * 套餐起售停售
     * @param status
     * @param id
     * @return
     */
    @CacheEvict(value = "setmealCache", allEntries = true)
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    public Result startOrStop(@PathVariable Integer status, Long id) {
        setmealService.startOrStop(status, id);
        return Result.success();
    }
 
}

(3)功能测试

1、全部清理

打开小程序,任意点击多个菜品分类,查看Redis数据库,打开前端网页,修改菜品信息,保存,查看数据库,若菜品数据全清空,则表示功能完成!

2、精确清除缓存(新增菜品)

与前面一样的操作,只删除对应的dish_?,如dish_12,即算完成!

至此,缓存菜品和缓存套餐功能已完成!

相关推荐
Java微观世界3 小时前
匿名内部类和 Lambda 表达式为何要求外部变量是 final 或等效 final?原理与解决方案
java·后端
SimonKing3 小时前
全面解决中文乱码问题:从诊断到根治
java·后端·程序员
你三大爷3 小时前
再探volatile原理
java
2301_781668613 小时前
Redis 面试
java·redis·面试
郑洁文3 小时前
基于SpringBoot的天气预报系统的设计与实现
java·spring boot·后端·毕设
吐泡泡_4 小时前
Redis(缓存)
redis
optimistic_chen4 小时前
【Java EE进阶 --- SpringBoot】Spring DI详解
spring boot·笔记·后端·spring·java-ee·mvc·di
沃夫上校4 小时前
MySQL 中文拼音排序问题
java·mysql
要一起看日出4 小时前
MVCC-多版本并发控制
数据库·mysql·mvcc