首页 > 其他分享 >缓存优化(缓存击穿和缓存雪崩)

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

时间:2024-07-29 20:31:06浏览次数:15  
标签:缓存 Lock 击穿 加锁 雪崩 菜品 public Result

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

缓存击穿和缓存雪崩

缓存击穿

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

缓存雪崩

  • 缓存雪崩是指在同一时段大量的缓存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里的分布式锁:

标签:缓存,Lock,击穿,加锁,雪崩,菜品,public,Result
From: https://www.cnblogs.com/zgg1h/p/18330996

相关文章

  • React 的 KeepAlive 实战指南:深度解析组件缓存机制
    Vue的Keep-Alive组件是用于缓存组件的高阶组件,可以有效地提高应用性能。它能够使组件在切换时仍能保留原有的状态信息,并且有专门的生命周期方便去做额外的处理。该组件在很多场景非常有用,比如:·tabs缓存页面·分步表单·路由缓存在Vue中,通过KeepAlive包裹内的组件......
  • 缓存优化(缓存穿透)
    缓存优化(缓存穿透)缓存穿透缓存穿透是指查询一个一定不存在的数据时,数据库查询不到数据,也不会写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,可能导致数据库崩溃。这种情况大概率是遭到了攻击。常见的解决方案有:缓存空数据,使用布隆过滤器等。当前项目中存在的......
  • vue2 - 详细实现“视频切片/分段加载“播放大视频,解决视频过大加载播放缓慢问题,vue处
    效果图在vue2、nuxt2项目开发中,详解vue视频分片加载,所谓“边播放边加载”,利用axios分段请求后端服务器每次只拿一小段视频慢慢缓存播放,让非常大的视频(例如电影,很长的视频播放太慢)流畅播放,vue2实现将video视频进行切片网络请求加载提升视频加载速度,详细解决视频分段下载......
  • Memcached跨平台性能解码:操作系统对缓存速度的影响
    Memcached跨平台性能解码:操作系统对缓存速度的影响在分布式缓存系统的设计和部署中,Memcached因其轻量级和高性能而成为首选方案之一。然而,Memcached在不同操作系统上的性能表现可能会有显著差异。本文将深入探讨这些差异的原因,并提供实际的测试方法和代码示例,帮助系统架构......
  • 科普文:详解 JuiceFS 读性能:预读、预取、缓存、FUSE 和对象存储
    在高性能计算场景中,往往采用全闪存架构和内核态并行文件系统,以满足性能要求。随着数据规模的增加和分布式系统集群规模的增加,全闪存的高成本和内核客户端的运维复杂性成为主要挑战。JuiceFS,是一款全用户态的云原生分布式文件系统,通过分布式缓存大幅提升I/O吞吐量,并使用成本......
  • Linux——手动清理内存缓存
    前言:使用free-m命令可以查看内存缓存。一、方法1.1先进管理员账户,然后进root账户1.2运行下面的命令:syncecho1>/proc/sys/vm/drop_caches#清空目录项缓存echo0>/proc/sys/vm/drop_caches#还原默认配置,这一步如果出错,则不用管sync二、小贴士......
  • Nginx 如何实现请求的缓存过期策略?
    ......
  • 18、flask-进阶-插件-缓存flask-caching - 钩子函数(中间件)
    1.认识flask-caching插件使用插件1.安装$flaskinstallflask-caching2.初始化在exts.py中导入并初始化fromflask_cachingimportCache#初始化插件cache=Cache(config={'CACHE_TYPE':'simple'#缓存类型})#和app对象绑定definit_exts(app):......
  • 如何使用Redis实现一个缓存策略
    使用Redis实现一个缓存策略,主要涉及到数据的存储、读取、更新以及失效处理等方面。下面我将详细介绍如何使用Redis来设计和实现一个基本的缓存策略。1.确定缓存的数据结构和键命名规则首先,你需要决定使用Redis中的哪种数据结构来存储缓存数据,比如字符串(String)、哈希(Hash)、列......
  • Redis中缓存二进制数据
    使用FreeRedis访问:byte[]b=File.ReadAllBytes("e:\\3专职安全员C-模拟题库.pdf");Stopwatchp=newStopwatch();p.Start();cli.SetRange("key8",0,b);cli.Expire("key8",30);this.textBox1.Text=p.ElapsedMilliseconds.ToString();......