完整资料下载
通过网盘分享的文件:苍穹外卖
链接:
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知识点
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 {@Autowiredprivate DishService dishService;@Autowiredprivate RedisTemplate redisTemplate;/*** 根据分类id查询菜品* @param categoryId* @return*/@GetMapping("/list")@ApiOperation("根据分类id查询菜品")public Result<List<DishVO>> list(Long categoryId) {log.info("根据分类id查询菜品,categoryId={}", categoryId);// 查询Redis缓存//构造缓存keyString 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 {@Beanpublic 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 {@Autowiredprivate DishService dishService;@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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,即算完成!
至此,缓存菜品和缓存套餐功能已完成!