缓存优化(缓存击穿和缓存雪崩)
缓存击穿和缓存雪崩
缓存击穿
- 缓存击穿是指用户查询的数据在缓存中不存在,但是后端数据库中却存在。
- 这种现象一般是由于缓存中的某个键过期导致的,比如一个热点数据键,它每时每刻都在接受大量的并发访问,如果某一刻这个键突然失效了,那么就会导致大量的并发请求进入数据库,导致其压力瞬间增大甚至崩溃。
- 常见的解决方案有:分布式锁,逻辑过期等。
缓存雪崩
- 缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,给数据库带来了巨大的压力。
- 常见的解决方案有:给不同key的过期时间添加一个随机值,利用Redis集群提高服务的可用性,给缓存业务添加降级限流策略,给业务添加多级缓存等。
当前项目中存在的问题
- 当数据库中菜品或套餐的数据发生变化时(即管理端新增、修改、删除或设置启售或停售时),redis缓存中的数据也需要同步地更新。
- 当前项目中的更新方式是:当菜品或套餐的数据发生变化时,直接清空redis中的菜品或套餐数据,然后等用户端查询的时候再把新的数据缓存进redis。
- 这种做法可能会导致缓存击穿和缓存雪崩。当redis中的菜品或套餐数据被清空时,如果用户端短时间内传来了大量的查询请求,此时redis中的缓存还来不及加载,于是大量得请求就直接到达了数据库,导致数据库压力过大。
解决方案
- 本项目有以下特点:数据库中的菜品数据和套餐数据发生变化的频率很低,而前端的查询请求频率又很高。
- 所以,我们可以使用redisson提供的分布式锁来以下方法进行加锁,从而保证数据库压力不会过大:
- 管理端对菜品表和套餐表的新增、修改、删除和设置启售或停售四个接口。从而保证数据的强一致性。
- 业务层中与用户端根据分类id查询有关的方法。在这种情况下,如果redis中有相应的数据缓存,就会在控制层直接从redis中取出该数据并响应,不会到达业务层;如果redis中没有相应的数据缓存,请求就会到达业务层,此时对业务层中的方法进行加锁,于是,同时就只能有一个线程进入到数据库查询数据,并将查询到的数据存入redis缓存,之后的请求就不会到达业务层了。
代码开发
- 在com.sky.annotation包下自定义注解Lock,用于标识某个方法需要加锁执行:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {}
- 在com.sky.service.aspect包下创建切面类LockAspect,用于自动加锁和解锁:
@Aspect
@Component
@Slf4j
@Order(0) //提升该切面类的执行优先级
public class LockAspect {private static final String FAIR_LOCK = "lock"; //锁使用的对象public static final long WATING_TIME = 60; //尝试加锁的等待时间@AutowiredRedissonClient redissonClient;/*** 切入点*/@Pointcut("@annotation(com.sky.annotation.Lock)")public void readWriteLockPointcut() {}/*** 环绕通知,在通知中进行分布式锁的加锁和解锁** @param proceedingJoinPoint*/@Around("readWriteLockPointcut()")public Object readWriteLock(ProceedingJoinPoint proceedingJoinPoint) {//获得锁对象RLock lock = redissonClient.getLock(FAIR_LOCK);try {boolean success = lock.tryLock(WATING_TIME, TimeUnit.SECONDS); //尝试加锁,等待WATING_TIME秒if (success) {log.info("线程{}加锁成功", Thread.currentThread().getName());} else {log.info("线程{}加锁失败", Thread.currentThread().getName());}return proceedingJoinPoint.proceed(); //执行原始方法} catch (Throwable e) {throw new RuntimeException(e);} finally {lock.unlock(); //最后释放锁log.info("线程{}释放锁", Thread.currentThread().getName());}}
}
- 在admin包下的DishController类中的save、delete、update和startOrStop方法上加上@Lock注解:
...
public class DishController {...@PostMapping@ApiOperation("新增菜品")@Lock()public Result save(@RequestBody DishDTO dishDTO) {log.info("新增菜品:{}", dishDTO);dishService.saveWithFlavor(dishDTO);//清理缓存数据String key = "dish_" + dishDTO.getCategoryId();cleanCache(key);return Result.success();}@DeleteMapping@ApiOperation("批量删除菜品")@Lock()public Result delete(@RequestParam List<Long> ids) {log.info("批量删除菜品:{}", ids);dishService.deleteBatch(ids);//将所有的菜品缓存数据清理掉,即所有以dish_开头的keycleanCache("dish_*");return Result.success();}@PutMapping@ApiOperation("修改菜品")@Lock()public Result update(@RequestBody DishDTO dishDTO) {log.info("修改菜品:{}", dishDTO);dishService.updateWithFlavor(dishDTO);//将所有的菜品缓存数据清理掉,即所有以dish_开头的keycleanCache("dish_*");return Result.success();}@PostMapping("/status/{status}")@ApiOperation("菜品启售停售")@Lock()public Result startOrStop(@PathVariable Integer status, Long id) {log.info("菜品启售停售:{},{}", status, id);dishService.startOrStop(status, id);//将所有的菜品缓存数据清理掉,即所有以dish_开头的keycleanCache("dish_*");return Result.success();}...
}
- 在admin包下的SetmealController类中的save、delete、update和startOrStop方法上加上@Lock注解:
...
public class SetmealController {...@PostMapping@ApiOperation("新增套餐")@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")@Lock()public Result save(@RequestBody SetmealDTO setmealDTO) {log.info("新增套餐:{}", setmealDTO);setmealService.saveWithDish(setmealDTO);return Result.success();}@DeleteMapping@ApiOperation("批量删除套餐")@CacheEvict(cacheNames = "setmealCache", allEntries = true)@Lock()public Result delete(@RequestParam List<Long> ids) {log.info("批量删除套餐:{}", ids);setmealService.deleteBatch(ids);return Result.success();}@PutMapping@ApiOperation("修改套餐")@CacheEvict(cacheNames = "setmealCache", allEntries = true)@Lock()public Result update(@RequestBody SetmealDTO setmealDTO) {log.info("修改套餐:{}", setmealDTO);setmealService.update(setmealDTO);return Result.success();}@PostMapping("/status/{status}")@ApiOperation("启售停售套餐")@CacheEvict(cacheNames = "setmealCache", allEntries = true)@Lock()public Result startOrStop(@PathVariable Integer status, Long id) {log.info("启售停售套餐:{},{}", status, id);setmealService.startOrStop(status, id);return Result.success();}
}
- 在DishServiceImpl类中的listWithFlavor方法上加上@Lock注解:
...
public class DishServiceImpl implements DishService {...@Lock()public List<DishVO> listWithFlavor(Dish dish) {List<Dish> dishList = dishMapper.list(dish);List<DishVO> dishVOList = new ArrayList<>();for (Dish d : dishList) {DishVO dishVO = new DishVO();BeanUtils.copyProperties(d,dishVO);//根据菜品id查询对应的口味List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId());dishVO.setFlavors(flavors);dishVOList.add(dishVO);}return dishVOList;}
}
- 在SetmealServiceImpl类中的list方法上加上@Lock注解:
...
public class SetmealServiceImpl implements SetmealService {...@Lock()public List<Setmeal> list(Setmeal setmeal) {List<Setmeal> list = setmealMapper.list(setmeal);return list;}...
}
功能测试
通过接口文档测试或前后端联调测试,并观察日志和redis缓存进行验证:
- 正常执行,无阻塞:
- 当加锁时被阻塞:
- redis里的分布式锁: