缓存优化(缓存击穿和缓存雪崩)
缓存击穿和缓存雪崩
缓存击穿
- 缓存击穿是指用户查询的数据在缓存中不存在,但是后端数据库中却存在。
- 这种现象一般是由于缓存中的某个键过期导致的,比如一个热点数据键,它每时每刻都在接受大量的并发访问,如果某一刻这个键突然失效了,那么就会导致大量的并发请求进入数据库,导致其压力瞬间增大甚至崩溃。
- 常见的解决方案有:分布式锁,逻辑过期等。
缓存雪崩
- 缓存雪崩是指在同一时段大量的缓存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; //尝试加锁的等待时间
@Autowired
RedissonClient 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_开头的key
cleanCache("dish_*");
return Result.success();
}
@PutMapping
@ApiOperation("修改菜品")
@Lock()
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
//将所有的菜品缓存数据清理掉,即所有以dish_开头的key
cleanCache("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_开头的key
cleanCache("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里的分布式锁: