缓存优化(缓存击穿和缓存雪崩)

缓存优化(缓存击穿和缓存雪崩)

缓存击穿和缓存雪崩

缓存击穿

  • 缓存击穿是指用户查询的数据在缓存中不存在,但是后端数据库中却存在。
  • 这种现象一般是由于缓存中的某个键过期导致的,比如一个热点数据键,它每时每刻都在接受大量的并发访问,如果某一刻这个键突然失效了,那么就会导致大量的并发请求进入数据库,导致其压力瞬间增大甚至崩溃。
  • 常见的解决方案有:分布式锁,逻辑过期等。

缓存雪崩

  • 缓存雪崩是指在同一时段大量的缓存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里的分布式锁:
posted @   zgg1h  阅读(104)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示