Redis实战(黑马点评)——关于缓存(缓存更新策略、缓存穿透、缓存雪崩、缓存击穿、Redis工具)

redis实现查询缓存的业务逻辑 

 service层实现

@Overridepublic Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;// 现查询redis内有没有数据String shopJson = (String) redisTemplate.opsForValue().get(key);if(StrUtil.isNotBlank(shopJson)){ // 如果redis的数据为存在,那么解析为对象// 将json转为对象Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}// 如果不存在,就先查数据库,再存入redisShop shop = getById(id);if(shop == null){return Result.fail("店铺不存在");}// 存在就写入redis,包括将对象转为jsonredisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));return Result.ok(shop);}

缓存更新策略 

缓存更新策略的最佳实践方案:

  1. 低一致性需求:使用Redis自带的内存淘汰机制
  2. 高一致性需求:主动更新,并以超时删除作为处理方案
读操作:(查询)
  • 缓存命中则直接返回
  • 缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:(增删改)
  • 先写数据库,然后再删除缓存
  • 要确保数据库与缓存操作的原子性

缓存穿透

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力。

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤器
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基本格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

 缓存空对象的方法解决缓存穿透

@Override  
public Result queryById(Long id) {  // 使用店铺ID构建缓存键  String key = CACHE_SHOP_KEY + id;  // 检查店铺信息是否已经缓存到Redis中  String shopJson = (String) redisTemplate.opsForValue().get(key);  // 如果缓存中存在数据,则将JSON字符串解析为Shop对象  if (StrUtil.isNotBlank(shopJson)) {   // 将JSON转换为Shop对象  Shop shop = JSONUtil.toBean(shopJson, Shop.class);  return Result.ok(shop); // 返回店铺对象作为成功结果  }  // 如果缓存中包含表示无数据的占位符,则返回错误信息  if ("#".equals(shopJson)) {  return Result.fail("没有店铺相关信息"); // 没有店铺信息可用  }  // 如果缓存中未找到店铺数据,则查询数据库  Shop shop = getById(id);  // 如果数据库中不存在该店铺  if (shop == null) {  // 在缓存中存储一个占位符,以表示该店铺不存在  // 这可以防止对同一ID的进一步查询再次访问数据库  redisTemplate.opsForValue().set(key, "#", CACHE_NULL_TTL, TimeUnit.MINUTES);  return Result.fail("店铺不存在"); // 返回错误,指示店铺不存在  }  // 如果找到店铺,则将店铺对象以JSON字符串的形式存储到缓存中  redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);  return Result.ok(shop); // 返回店铺对象作为成功结果  
}

缓存雪崩

缓存击穿 

缓存击穿是指在高并发环境下,某个热点数据的缓存同时失效,导致大量请求直接访问数据库,从而造成数据库压力骤增的现象。

缓存击穿解决方案

解决方案优点缺点
互斥锁

- 没有额外的内存消耗

- 保证一致性

- 实现简单

- 线程需要等待,性能受到影响

- 可能有死锁风险

逻辑锁- 线程无需等待,性能较好

- 不保证一致性

- 有额外的内存消耗

- 实现复杂

基于互斥锁解决缓存击穿 

 基于互斥锁解决缓存击穿 + 缓存空对象的方法解决缓存穿透

// 查询店铺信息,使用互斥锁解决缓存击穿问题
public Shop queryWithMutex(Long id) {// 构造缓存的keyString key = CACHE_SHOP_KEY + id;// 1. 先从Redis中查询店铺信息String shopJson = (String) redisTemplate.opsForValue().get(key);// 如果Redis中存在缓存数据,直接解析JSON并返回对象if (StrUtil.isNotBlank(shopJson)) {// 将JSON字符串转换为Shop对象Shop shop = JSONUtil.toBean(shopJson, Shop.class);return shop;}// 如果Redis中缓存的值为"#", 表示数据库中没有该店铺信息if ("#".equals(shopJson)) {return null;}// 2. 构造互斥锁的keyString lockKey = "lock:shop:" + id;// 定义店铺对象Shop shop = null;try {// 尝试获取互斥锁boolean isLock = tryLock(lockKey);// 如果获取锁失败,线程休眠50ms后重试if (!isLock) {Thread.sleep(50); // 等待50msreturn queryWithMutex(id); // 递归调用,再次尝试获取锁}// 3. 如果没有获取到缓存数据,查询数据库shop = getById(id);// 模拟数据库查询的延时,生产环境应该去掉这段代码// Thread.sleep(200);// 4. 如果数据库中没有该店铺信息if (shop == null) {// 在Redis中存储一个特殊的标记值"#", 表示该店铺不存在redisTemplate.opsForValue().set(key, "#", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 5. 如果数据库中有数据,将店铺信息存入Redis// 将Shop对象转换为JSON字符串redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {// 捕获线程中断异常throw new RuntimeException(e);} finally {// 释放互斥锁unlock(lockKey);}// 返回查询到的店铺信息return shop;
}// 封装获取锁,释放锁
// 尝试获取互斥锁
private boolean tryLock(String key) {// 使用Redis的setIfAbsent方法尝试设置锁// 如果key不存在,则设置成功并返回true;否则返回falseBoolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);// 并设置一个过期时间(这里是 10 秒)return BooleanUtil.isTrue(flag);
}// 释放互斥锁
private void unlock(String key) {// 删除锁对应的keyredisTemplate.delete(key);
}

setIfAbsent 方法是 Redis 中的一种操作,用于设置一个键的值,仅在该键不存在的情况下进行设置。具体来说,它的功能如下:

  • 键不存在时:如果指定的键(key)在 Redis 中不存在,则将其设置为指定的值(在这个例子中是 "1"),并可以指定该键的过期时间(这里是 10 秒)。此时,方法返回 true

  • 键已存在时:如果指定的键已经存在于 Redis 中,则不会进行任何操作,保持原有的值不变,方法返回 false

基于逻辑锁解决缓存击穿 

 // 创建一个固定大小的线程池,用于缓存重建任务,避免频繁创建线程带来的开销private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 查询商铺信息,考虑逻辑过期* @param id 商铺ID* @return 商铺信息,如果不存在或已过期则返回null*/public Shop queryWithLogicalExpire(Long id){// 构建缓存的key,用于从Redis中查询对应的商铺信息String key = CACHE_SHOP_KEY + id;// 1. 从Redis查询商铺缓存,获取商铺信息的JSON字符串String shopJson = (String) redisTemplate.opsForValue().get(key);// 2. 判断缓存是否存在if (StrUtil.isBlank(shopJson)) {// 3. 缓存不存在,直接返回nullreturn null;}// 4. 缓存命中,需要先将JSON字符串反序列化为RedisData对象RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);// 从RedisData对象中提取商铺信息,并将其反序列化为Shop对象Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);// 获取缓存的过期时间LocalDateTime expireTime = redisData.getExpireTime();// 5. 判断缓存是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 5.1. 缓存未过期,直接返回商铺信息return shop;}// 构建锁的key,用于控制缓存重建的并发访问String lockKey = LOCK_SHOP_KEY + id;// 尝试获取锁,确保缓存重建操作的线程安全boolean isLock = tryLock(lockKey);// 6.2. 判断是否成功获取锁if (isLock) {// 6.3. 成功获取锁,开启独立线程进行缓存重建,避免阻塞主线程CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 重建缓存,将最新的商铺信息保存到Redis中,并设置过期时间this.saveShop2Redis(id, 20L); // 假设20L是过期时间,单位为秒} catch (Exception e) {// 如果在缓存重建过程中发生异常,抛出运行时异常,并记录日志throw new RuntimeException(e);} finally {// 无论缓存重建成功与否,都需要释放锁,避免死锁unlock(lockKey);}});}// 返回当前查询到的商铺信息(可能已过期)return shop;}public void saveShop2Redis(Long id, Long expireSeconds) {// 1. 查询店铺数据Shop shop = getById(id);// 2. 封装逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));// 3. 写入RedisredisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));}

RedisData类

@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

 封装redis工具类

    @Resourceprivate CacheClient cacheClient;public static final String CACHE_SHOP_KEY = "cache:shop:";public static final Long CACHE_SHOP_TTL = 30L;@Overridepublic Result queryById(Long id)  {// 解决缓存穿透
//        Shop shop = cacheClient // 传入一个从数据库内获取Shop对象的函数:this::getById
//                .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);// 互斥锁解决缓存击穿Shop shop = cacheClient.queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);// 逻辑过期解决缓存击穿
//        Shop shop = cacheClient
//                .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);if(shop == null){return Result.fail("店铺不存在");}return Result.ok(shop);}

1. queryWithPassThrough

这个方法用于处理缓存穿透问题。缓存穿透是指查询一个数据库中不存在的数据,由于缓存中也没有这个数据,所以每次查询都会直接打到数据库上,增加数据库的压力。

  • 参数

    • keyPrefix:缓存的前缀。

    • id:缓存的ID。

    • type:返回对象的类型。

    • dbFallback:数据库查询的回调函数。

    • time:缓存时间。

    • unit:时间单位。

2. queryWithLogicalExpire

这个方法用于处理缓存击穿问题。缓存击穿是指一个缓存中非常热门的数据突然过期,导致大量请求同时打到数据库上,增加数据库的压力。

  • 参数:与 queryWithPassThrough 相同。

最大的缺点是运行前要把所有缓存加到redis内,不然怎么查都是null

3. queryWithMutex(互斥锁)

这个方法结合了 queryWithPassThroughqueryWithLogicalExpire 的功能,用于处理缓存穿透和缓存击穿问题。

  • 参数:与 queryWithPassThrough 相同。

@Slf4j
@Component
public class CacheClient {public static final Long CACHE_NULL_TTL = 2L;public static final String LOCK_SHOP_KEY = "lock:shop:";private final RedisTemplate redisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);public CacheClient(RedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}public void set(String key, Object value, Long time, TimeUnit unit) {redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisredisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = (String) redisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判断命中的是否是空值if (json != null) {// 返回一个错误信息return null;}// 4.不存在,根据id查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisredisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);return r;}public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String json = (String) redisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化为对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未过期,直接返回店铺信息return r;}// 5.2.已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock){// 6.3.成功,开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R newR = dbFallback.apply(id);// 重建缓存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的商铺信息return r;}public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询商铺缓存String shopJson = (String) redisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判断命中的是否是空值if (shopJson != null) {// 返回一个错误信息return null;}// 4.实现缓存重建// 4.1.获取互斥锁String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.获取锁成功,根据id查询数据库r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisredisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}private boolean tryLock(String key) {Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(flag);}private void unlock(String key) {redisTemplate.delete(key);}
}

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

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

相关文章

ThinkPhp伪静态设置后,访问静态资源也提示找不到Controller

ThinkPhp没有配置伪静态时&#xff0c;除了默认的IndexController能访问&#xff0c;其他路由Controller都访问不到&#xff0c;提示404错误。配置了伪静态后就解决了这个问题。 但是当我的ThinkPhp后台项目中有静态资源放在public目录&#xff08;或子目录&#xff09;中需要…

2013年蓝桥杯第四届CC++大学B组真题及代码

目录 1A&#xff1a;高斯日记&#xff08;日期计算&#xff09; 2B&#xff1a;马虎的算式&#xff08;暴力模拟&#xff09; 3C&#xff1a;第39级台阶&#xff08;dfs或dp&#xff09; 4D&#xff1a;黄金连分数&#xff08;递推大数运算&#xff09; 5E&#xff1a;前缀…

【数据分享】1929-2024年全球站点的逐月平均能见度(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、湿度等指标&#xff01;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 有关气象指标的监测站点数据&#xff0c;之前我们分享过1929-2024年全球气象站点…

【动态规划】--- 斐波那契数模型

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 算法Journey &#x1f3e0; 第N个泰波那契数模型 &#x1f4cc; 题目解析 第N个泰波那契数 题目要求的是泰波那契数&#xff0c;并非斐波那契数。 &…

单片机-STM32 WIFI模块--ESP8266 (十二)

1.WIFI模块--ESP8266 名字由来&#xff1a; Wi-Fi这个术语被人们普遍误以为是指无线保真&#xff08;Wireless Fidelity&#xff09;&#xff0c;并且即便是Wi-Fi联盟本身也经常在新闻稿和文件中使用“Wireless Fidelity”这个词&#xff0c;Wi-Fi还出现在ITAA的一个论文中。…

计算机的错误计算(二百二十二)

摘要 利用大模型化简计算 实验表明&#xff0c;虽然结果正确&#xff0c;但是&#xff0c;大模型既绕了弯路&#xff0c;又有数值计算错误。 与前面相同&#xff0c;再利用同一个算式看看另外一个大模型的化简与计算能力。 例1. 化简计算摘要中算式。 下面是与一个大模型的…

ansible自动化运维实战--软件包管理模块、服务模块、文件模块和收集模块setup(4)

文章目录 一、软件包管理模块1.1、功能1.2、常用参数1.3、示例 二、服务模块2.1、功能2.2、服务模块常用参数2.3、示例 三、文件与目录模块3.1、file功能3.2、常用参数3.3、示例 四、收集模块-setup4.1、setup功能4.2、示例 一、软件包管理模块 1.1、功能 Ansible 提供了多种…

高速光模块中的并行光学和WDM波分光学技术

随着AI大模型训练和推理对计算能力的需求呈指数级增长&#xff0c;AI数据中心的网络带宽需求大幅提升&#xff0c;推动了高速光模块的发展。光模块作为数据中心和高性能计算系统中的关键器件&#xff0c;主要用于提供高速和大容量的数据传输服务。 光模块提升带宽的方法有两种…

Linux命令行配置网络代理

在Linux命令行中&#xff0c;你可以使用以下方法设置网络代理服务器。 本文演示代理地址为&#xff1a;http://192.168.1.30:7890 请根据实际代理地址进行替换 临时代理 使用环境变量的方法&#xff1a; 打开终端&#xff0c;并输入以下命令&#xff1a; export http_proxyhtt…

SpringBoot3+Vue3开发学生选课管理系统

功能介绍 分三个角色登录&#xff1a;学生登录&#xff0c;老师登录&#xff0c;教务管理员登录&#xff0c;不同用户功能不同&#xff01; 1.学生用户功能 选课记录&#xff0c;查看选课记录&#xff0c;退选。选课管理&#xff0c;进行选课。通知管理&#xff0c;查看通知消…

牛客周赛 Round 78 A-C

A.时间表查询&#xff01; 链接&#xff1a;https://ac.nowcoder.com/acm/contest/100671/A 来源&#xff1a;牛客网 题目描述 今天是2025年1月25日&#xff0c;今年的六场牛客寒假算法基础集训营中&#xff0c;前两场比赛已经依次于 20250121、20250123 举行&#xff1b;而…

Android - 通过Logcat Manager简单获取Android手机的Log

由于工作需要&#xff0c;经常需要获取Android手机的Log。 平常都是通过adb命令来获取&#xff0c;每次都要写命令。 偶然的一个机会&#xff0c;我从外网发现了一个工具 Logcat Manager&#xff0c;只需要通过简单的双击即可获取Android的Log&#xff0c;这里也分享一下。 目…

苍穹外卖-day10

苍穹外卖-day10 Spring Task Spring Task 是Spring框架提供的任务调度工具&#xff0c;可以按照约定的时间自动执行某个代码逻辑。 cron表达式 cron表达式其实就是一个字符串&#xff0c;通过cron表达式可以定义任务触发的时间 **构成规则&#xff1a;**分为6或7个域&…

一文详解Filter类源码和应用

背景 在日常开发中&#xff0c;经常会有需要统一对请求做一些处理&#xff0c;常见的比如记录日志、权限安全控制、响应处理等。此时&#xff0c;ServletApi中的Filter类&#xff0c;就可以很方便的实现上述效果。 Filter类 是一个接口&#xff0c;属于 Java Servlet API 的一部…

(2)STM32 USB设备开发-USB虚拟串口

例程&#xff1a;STM32USBdevice: 基于STM32的USB设备例子程序 - Gitee.com 本篇为USB虚拟串口教程&#xff0c;没有知识&#xff0c;全是实操&#xff0c;按照步骤就能获得一个STM32的USB虚拟串口。本例子是在野火F103MINI开发板上验证的&#xff0c;如果代码中出现一些外设的…

K8S中的数据存储之基本存储

基本存储类型 EmptyDir 描述&#xff1a;当 Pod 被调度到节点上时&#xff0c;Kubernetes 会为 Pod 创建一个空目录&#xff0c;所有在该 Pod 中的容器都可以访问这个目录。特点&#xff1a; 生命周期与 Pod 绑定&#xff0c;Pod 删除时&#xff0c;数据也会丢失。适用于临时…

谈谈RTMP|RTSP播放器视频view垂直|水平反转和旋转设计

技术背景 我们在做RTMP|RTSP播放器的时候&#xff0c;有这样的技术诉求&#xff0c;有的摄像头出来的数据是有角度偏差的&#xff0c;比如“装倒了”&#xff0c;或者&#xff0c;图像存在上下或者左右反转&#xff0c;这时候&#xff0c;就需要播放器能做响应的处理&#xff…

自然语言处理——从原理、经典模型到应用

1. 概述 自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;是一门借助计算机技术研究人类语言的科学&#xff0c;是人工智能领域的一个分支&#xff0c;旨在让计算机理解、生成和处理人类语言。其核心任务是将非结构化的自然语言转换为机器可以…

【MFC】C++所有控件随窗口大小全自动等比例缩放源码(控件内字体、列宽等未调整) 20250124

MFC界面全自动等比例缩放 1.在初始化里 枚举每个控件记录所有控件rect 2.在OnSize里&#xff0c;根据当前窗口和之前保存的窗口的宽高求比例x、y 3.枚举每个控件&#xff0c;根据比例x、y调整控件上下左右,并移动到新rect struct ControlInfo {CWnd* pControl;CRect original…

SkyWalking介绍

一款开源的系统性能监控工具(APM) 背景 在解决提报的IT性能问题时&#xff0c;由于缺乏系统性能监控运维的工具&#xff0c;导致问题排查非常困难&#xff0c;尤其是偶发的问题&#xff0c;无法进行问题复现还原&#xff0c;需要一套能实时监控线上系统性能的工具平台。 SkyWal…