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

news/2024/11/14 20:39:05/文章来源:https://www.cnblogs.com/zgg1h/p/18330996

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

缓存击穿和缓存雪崩

缓存击穿

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

缓存雪崩

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/773781.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【专题】2024家生活智能家居趋势报告合集PDF分享(附原数据表)

原文链接: https://tecdat.cn/?p=37146 近二十载间,中国消费市场见证了从产品创新到渠道创新的双重飞跃,无论是耐用消费品还是快速消费品,均在线上线下平台绽放出前所未有的丰富选择,多数行业已转型为以消费者为核心导向的买方市场格局。阅读原文,获取专题报告合集全文,…

c语言字符数组

字符数组与字符串,字符数据输出用%s表示 上面两种方式的区别:

mysql授权

mysql连接的两种方式 mysql服务端 10.0.0.51:3306 mysql -uroot -p密码 -h该账户允许登录的网段 -P实例端口第一种 基于ip:port的 网络链接形式,入口一 ,链接参数 ,-hlocahost -P3306 端口,窗口提供服务的入口windows机器,去链接 mysql服务端本质上是tcp的建立n…

DelphiJNI实际调试

1:下载 DelphiJNI:下载地址https://github.com/aleroot/DelphiJNI,版本比较老,没有找到其他,就用这个吧,如朋友有较新的pas文件,请留言下 2:下载jdk,这里下载JDK,这里使用javase-jkd18,也不知道这个版本要不要收费,这里学习用暂时不关新这个。 3:编写调用class的代…

playbook+roles安装nginx实战

基本目录结构host文件夹 用于存放主机清单文件 hosts文件 hosts文件内容如下:(仅供参考) [proxy] node2 [web] 192.168.xx.xxplaybook-all-roles.yml文件 用于指定执行哪个role的文件(命名可以自定义) 文件内容如下:(仅供参考) 因为roles文件夹下只有nginx一个文件夹,所…

2024夏中山集训第1周

【NOIP模拟一】20240729 C 注意到答案是s除以区间gcd。 裴蜀定理推广 D像这样建图,跑全源最短路。 在这张图上有 \(1\to 2\to 3\to 4\to 5\) 和 \(7\to 8\to 9\to 3\ to 10\ to 11\) 两条路径。把路径上的点看作车上的点,每个点本身看作车站。 可以发现在车(一条路径)上的点…

关于多模块开发各级目录的用途

参考苍穹外卖 项目整体结构如下各层的用途序号 名称 说明1 sky-take-out maven父工程,统一管理依赖版本,聚合其他子模块2 sky-common 子模块,存放公共类,例如:工具类、常量类、异常类等3 sky-pojo 子模块,存放实体类、VO、DTO等4 sky-server 子模块,后端服务,存放配置文…

WPF实现一个错误信息栏

实现结果一,首先建立一个UserControl 前台代码如下:点击查看代码 <UserControl x:Class="实现一个错误信息栏.ErrorLog"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/x…

c语言中数据的格式化输出

001、输出整型数据,直接输出[root@PC1 test]# ls test.c [root@PC1 test]# cat test.c #include <stdio.h>int main(void) {printf("[%d]\n", 123);return 0; } [root@PC1 test]# gcc test.c -o kkk [root@PC1 test]# ls kkk test.c [root@PC1 test]# ./kkk …

电脑技巧 | 你想拥有这样的自定义工具栏命令按钮吗?QTTabBar帮助你实现!

【电脑技巧】第90期:你想拥有这样的自定义工具栏命令按钮吗?QTTabBar帮助你实现!

window系统使用Tomcat部署若依微服务

安装JAVA 下检查是否安装了JAVAjava -version提示"java: command not found"则表示没有安装,如果安装了会显示JAVA版本信息 CentOS安装JAVAsudo yum install java-11-openjdk-devel 安装完成再执行一下:java -version 说明安装成功,没问题 设置环境变量 设置JAVA_H…