场景:给用户端展示的数据都是通过查询数据库所得,因此数据库访问压力会随着用户访问量增大而增加,从而导致系统响应慢、用户体验差。
方法:通过Redis缓存数据,减少查询数据库操作。(Redis的数据是存储在内存的,数据库的数据存储在磁盘,访问内存会更快。Redis相关知识可参见Redis基础)
举例:
以外卖平台为例,页面根据分类来展示菜品。当选择一个分类时,页面展示该分类的所有菜品,因此,缓存逻辑为:
- 每个分类下的菜品对应一份缓存数据;
- 数据库中菜品数据有变更时,清理缓存数据。(避免数据不一致)
综上,Redis的数据结构为:
(ps:Redis中的数据类型和Java中的数据类型并不是完全对应的,Java中的任何一个对象都可以转成Redis中的string字符串进行存储。若是从Java层面考虑,这里的value实则是List集合,然后把这个集合序列化,最终把它转成Redis字符串存储。dish_id表示分类id。)
代码举例:
使用Redis缓存访问数据:——用户端实现
@RestController("userDishController")
@RequestMapping("/user/dish")
@Api(tags = "C端-菜品相关接口")
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 条件查询菜品和口味
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("条件查询菜品和口味")
public Result<List<DishVO>> listWithCategoryId(Long categoryId) {
// 1. 构造redis中的key, 规则:dish_分类id
String key = "dish_" + categoryId;
// 2. 查询Redis是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
// 3. 如果存在,直接返回,无须查询数据库
if (list != null && list.size() > 0) {
return Result.success(list);
}
// 4. 如果不存在,查询数据库,将查询到的数据放入Redis中
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE); // 查询起售中的菜品
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);// 存储查询结果到list
return Result.success(list);
}
}
清除缓存数据——管理端实现
需要清除缓存数据的情况:
- 修改了数据库中的数据;
- 删除了数据库中的数据;
- 新增了数据库中的数据;
总结:只要涉及到了对应数据库的增删改,都需要清除缓存。
代码中涉及的不同情况:
- case1: 新增一个菜品——删除该菜品所属分类对应的缓存;
- case2: 批量删除菜品——删除所有分类对应的缓存(删除时是根据菜品的id删除的,所以想要知道菜品所对应的分类id,还遍历查询一遍);
- case3: 修改菜品(涉及两种情况:a. 只修改菜品的基本信息,菜品的分类不变;b. 修改了菜品的分类,这就涉及到了两个分类对应的缓存值)——删除所有分类对应的缓存(与上面的情况一样,如果要确定哪两个分类受影响,还得查一次(查修改前所属分类id),索性直接删除所有的分类的缓存)
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 清理缓存数据
* @param pattern
*/
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
/**
* 新增菜品(case1)
*
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);
// 清理缓存数据
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);
return Result.success();
}
/**
* 菜品批量删除(case2)
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品删除")
// 通过@RequestParam解析前端请求地址中字符串参数ids=1,2,3...为一个列表
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);
// 清理缓存数据(将所有的dish_*的缓存数据都清除掉)
cleanCache("dish_*");
return Result.success();
}
/**
* 修改菜品(case3)
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
// 清理缓存数据(将所有的dish_*的缓存数据都清除掉)
cleanCache("dish_*");
return Result.success();
}
Spring Cache
Spring Cache是Spring框架提供的缓存工具,使用它可以进一步简化代码。它实现了基于注解的缓存功能,我们只需要添加一个注解,就能实现缓存功能。
Spring Cache只是提供了一层抽象,底层可以切换不同的缓存实现,包括EHCache,Caffeine,Redis等。我们只需要导入对应的缓存产品的客户端,就可以告诉Spring Cache我们想用哪个缓存实现。
maven坐标:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Spring Cache提供的常用注解:
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
使用Spring Cache缓存数据,@CachePut中key的生成:cacheNames::key,这里使用SqEL语法,SqEL细则:
Spring Expression Language (SpEL) expression for computing the key dynamically.
Default is “”, meaning all method parameters are considered as a key, unless a custom keyGenerator has been set.
The SpEL expression evaluates against a dedicated context that provides the following meta-data:
- #result for a reference to the result of the method invocation. For supported wrappers such as Optional, #result refers to the actual object, not the wrapper
- #root.method, #root.target, and #root.caches for references to the method, target object, and affected cache(s) respectively.
- Shortcuts for the method name (#root.methodName) and target class (#root.targetClass) are also available.
- Method arguments can be accessed by index. For instance the second argument can be accessed via #root.args[1], #p1 or #a1. Arguments can also be accessed by name if that information is available.
举例:
@CachePut(cacheNames = "setmeal",key = "#setmealDTO.categoryId")
注意: 不同的注解对应支持生成key的方式可能不一样,比如@Chacheable中就不支持#result的形式,所以使用的时候可以通过 Ctrl +鼠标点击key
跳转到源文件中去查看。
举例:
实现思路如下:
- 导入Spring Cache和Redis相关maven坐标
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
- 在用户端接口查询 方法上加入@Cacheable注解
- 在管理端接口数据库增、删改等方法上加入CacheEvict注解
@CacheAble举例:
/**
* 条件查询
* @param categoryId
* @return
*/
@GetMapping("list")
@ApiOperation("根据分类id查询套餐")
// key为: setmealCache::categoryId值, Redis存储的value为list方法的返回结果
@Cacheable(cacheNames = "setmealCache", key = "#categoryId")
public Result<List<Setmeal>> list(Long categoryId){
Setmeal setmeal = new Setmeal();
setmeal.setCategoryId(categoryId);
setmeal.setStatus(StatusConstant.ENABLE);
List<Setmeal> etmealVOList = setmealService.list(setmeal);
return Result.success(etmealVOList);
}
@CacheEvic举例:
精确清理:
/**
* 新增套餐
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
@CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")
public Result save(@RequestBody SetmealDTO setmealDTO){
log.info("新增套餐:{}",setmealDTO);
setmealService.save(setmealDTO);
return Result.success();
}
全部清理:
/**
* 批量删除套餐
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result deleteBatch(Long[] ids){
setmealService.deleteBatch(ids);
return Result.success();
}