缓存使用的最佳实践,自定义缓存工具类

news2024/10/28 15:06:49

缓存穿透问题解决-缓存空值

访问数据库不存在的数据,会一直请求到数据库,被别有用心的人使用,可能会一直请求数据库,导致数据库宕机。解决方法有两

一:缓存空数据,二,使用布隆过滤器进行校验。

缓存空数据

在数据库查询到不存在的数据时,对该数据进行缓存为空(可以设置稍短的3~5分钟的TTL),之后相同的请求,就会在缓存中查到,而不去请求数据库。

代码案列

  /**
     * 查询商户信息
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //查询缓存
        String string = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
        //hutool 工具类 符合条件“adc" 不符合条件“”,null, "/t/n"
        if (StrUtil.isNotBlank(string)){
            Shop shop = JSONUtil.toBean(string, Shop.class);
            return Result.ok(shop);
        }
        //若是 " " 上面已经判断了不是“” 不是null ,
        if(string != null){
            return Result.fail("商户不存在");
        }

        // 缓存不存在 查数据库
        Shop shop = getById(id);
        if (shop ==null) {
            //将空值写入缓存
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("商户不存在");
        }

        //写入缓存
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return Result.ok(shop);
    }

缓存击穿问题- 互斥锁

热点key失效,构造缓存复杂,在构造缓存的期间大量请求,只允许一个请求到数据库构造缓存。

具体流程

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就重试获取缓存资源和锁(递归),直到线程1把锁释放后,线程2获得到锁或者缓存资源,可能线程二执行到获取缓存就获得到缓存就之间返回了,也可能没查到缓存,执行到获得了锁,这时候要再次校验一下是否获得了缓存。没有获得缓存在取构建缓存。

 /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 20, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);

    }

    /**
     * 释放锁
     * @param key
     */
    private  void unlock(String key){
        stringRedisTemplate.delete(key);
    }
/**
     * 查询商户信息 缓存击穿互斥锁
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id){
        String shopKey = CACHE_SHOP_KEY+ id;

        // 1. 从redis中查询店铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);

        //2.判断是否命中缓存  isnotblank false: "" or "/t/n" or "null"
        if(StrUtil.isNotBlank(shopJson)){
            // 3.若命中则返回信息
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //数据穿透判空   不是null 就是空串 ""
        if (shopJson != null){
            //返回错误信息
//            return  Result.fail("没有该商户信息(缓存)");
            return null;
        }
        //4.没有命中缓存,查数据库
        //todo :解决缓存击穿  不能直接查数据库。 利用互斥锁解决

        /**
         * 实现缓存重建
         * 1. 获取互斥锁
         * 2. 判断是否成功
         * 3. 失败就休眠重试
         * 4.成功 查数据库
         * 5 数据库存在该数据写入缓存
         * 6 不存在返回错误信息并写入缓存“”
         * 7 释放锁
         *
         */

        //获取互斥锁 失败  休眠重试
        String lockKey = "lock:shop" + id;
        Shop shop=null;

        try {
            boolean isLock = tryLock(lockKey);
            //获取锁失败
            if (!isLock) {

                System.out.println("获取锁失败,重试");
                Thread.sleep(50);
                return queryWithMutex(id);//递归 重试
            }

            // 获取锁成功,再次检测缓存是否存在,存在就无需构建缓存,因为可能有的线程刚构建好缓存并释放锁,其他线程获取了锁
            //检测缓存是否存在  存在
            shopJson = stringRedisTemplate.opsForValue().get(shopKey);
            if (StrUtil.isNotBlank(shopJson)) {
                return JSONUtil.toBean(shopJson, Shop.class);
            }
            if (shopJson !=null){
                return null;
            }
            // 缓存不存在
            // 查数据库
             shop = super.getById(id);
            Thread.sleep(200);//模拟你测试环境 热点key失效模拟重建延迟
            if (shop == null){
                //没有该商户信息
                stringRedisTemplate.opsForValue().set(shopKey,"",CACHE_NULL_TTL,TimeUnit.SECONDS);
                return null;
            }
            //有该商户信息
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            unlock(lockKey);
        }
        return shop;

    }

缓存击穿问题- 逻辑过期时间

需要添加逻辑过期时间字段,直接在shop类中添加不太友好改了源代码

可以新建一个类

/**
 * 逻辑过期类
 */
@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

数据预热

 /**
     * 添加逻辑过期时间
     * @param id
     * @param expireSeconds
     */
    public void savaShop2Redis(Long id ,Long expireSeconds){

        // 查询店铺数据
        Shop shop = getById(id);

        //封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        redisData.setData(shop);
        //写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

执行测试方法即可加入到redis。

正式代码

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {
    String key = CACHE_SHOP_KEY + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isBlank(json)) {
        // 3.存在,直接返回
        return null;
    }
    // 4.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(json, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    // 5.判断是否过期
    if(expireTime.isAfter(LocalDateTime.now())) {
        // 5.1.未过期,直接返回店铺信息
        return shop;
    }
    // 5.2.已过期,需要缓存重建
    // 6.缓存重建
    // 6.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    // 6.2.判断是否获取锁成功
    if (isLock){
        CACHE_REBUILD_EXECUTOR.submit( ()->{

            try{
                //重建缓存
                this.saveShop2Redis(id,20L);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                unlock(lockKey);
            }
        });
    }
    // 6.4.返回过期的商铺信息
    return shop;
}

缓存工具类

package com.hmdp.utils;

import cn.hutool.core.lang.func.Func;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import javafx.beans.binding.ObjectExpression;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

/**
 * Redis 工具类
 * * 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
 * * 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
 *
 * * 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
 * * 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
 */
@Component
public class CacheClient {

    private  final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //range

    /**
     * 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
     * @param key
     * @param value
     * @param time
     * @param unit
     */
    public void set(String key , Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time,unit);
    }


    /**
     * 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
     * @param key  redis的key
     * @param value
     * @param time
     * @param unit
     */
    public void setWithLogicalExpire(String key , Object value, Long time, TimeUnit unit){

        //RedisData 是自定义类
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }


    /**
     *  方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
     * @param
     * @param id
     * @return
     */
    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 = stringRedisTemplate.opsForValue().get(key);

        //2.判断是否命中缓存  isnotblank false: "" or "/t/n" or "null"
        if(StrUtil.isNotBlank(json)){
            // 3.若命中则返回信息
            R r = JSONUtil.toBean(json, type);
            //            return Result.fail("没有该商户信息");
            return r;
        }
        //数据穿透判空   不是null 就是空串 ""
        if (json != null){
            return null;
        }
        //4.没有命中缓存,查数据库,因为不知道操作那个库,函数式编程,逻辑交给调用者完成
//       R r= getById(id); 交给调用者--》》函数式编程
        R r = dbFallback.apply(id);
        //5. 数据库为空,返回错误---》解决缓存穿透--》加入redis为空
        if (r == null){
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
//            return Result.fail("没有该商户信息");
            return null;
        }

        //6. 数据库不为空,返回查询的结果并加入缓存
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r),time, unit);
        return r;
    }

    /**
     *  方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
     * @param id
     * @return
     */
    public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Function<ID,R>dbFallback,String lockPrefix,Long time,TimeUnit unit){
        String key = keyPrefix+ id;

        // 1. 从redis中查询店铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);

        //2.判断数据是否存在(我们对于热点key设置永不过期)  isblank
        if(StrUtil.isBlank(json)){
            // 3.若未命中中则返回空
            return null;
        }

        //4.若命中缓存 判断是否过期
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();

        //未过期 直接返回查询信息
        if (expireTime.isAfter(LocalDateTime.now())){
            return r;
        }
        //过期
        // 重建缓存
        // 获取锁
        String lockKey = lockPrefix + id;
        if (tryLock(lockKey)) {
            //再次校验缓存是否未过期(线程1刚写入缓存然后释放锁,线程2在线程1释放锁的同时,执行到获得锁)
            //  从redis中查询店铺缓存
            json = stringRedisTemplate.opsForValue().get(key);

            //2.判断数据是否存在(我们对于热点key设置永不过期)  isblank
            if(StrUtil.isBlank(json)){
                // 3.若未命中中则返回空
                return null;
            }

            //4.若命中缓存 判断是否过期
            redisData = JSONUtil.toBean(json, RedisData.class);
            data = (JSONObject) redisData.getData();
            r = JSONUtil.toBean(data, type);
            expireTime = redisData.getExpireTime();

            //未过期 直接返回查询信息
            if (expireTime.isAfter(LocalDateTime.now())){
                return r;
            }

            //二次校验过后还时过期的就新开线程重构缓存


            // 获得锁,开启新线程,重构缓存 ,老线程直接返回过期信息
            CACHE_REBUILD_EXECUTOR.submit( ()->{

                try{
                    //重建缓存
                    //先查数据库 封装逻辑过期时间 再写redis
                    R r1 = dbFallback.apply(id);

                    this.setWithLogicalExpire(key, r1, time, unit);


                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });

        }
        //未获得锁 直接返回无效信息
        return r;
    }

    /**缓存穿透互斥锁解
     *
     * @param keyPrefix
     * @param id
     * @param type
     * @param dbFallback
     * @param time
     * @param unit
     * @return
     */
    public <R,ID>  R queryMutex(String keyPrefix, ID id, Class<R> type, Function<ID,R>dbFallback,String lockPrefix, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        //1.从redis中查询店铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.判断数据是否存在缓存
        if (StrUtil.isNotBlank(json)) {
            //2.1存在缓存
            R r = JSONUtil.toBean(json, type);
            return r;
        }
        //  2.2 是否缓存“”
        //判断命中是否为空值  ""
        if (json != null) {
            return null;
        }
        // 2.3不存在缓存
        // 3 缓存重建
        // 3.1 获取互斥锁
        String lockKey = lockPrefix + id;
        R r = null;
        try {
        boolean isLock = tryLock(lockKey);
        // 成功获取锁 - 》查数据库缓存重建
        if (isLock) {
            //二次校验 缓存是否有值
            json = stringRedisTemplate.opsForValue().get(key);
            //判断缓存是否存在
            if (StrUtil.isNotBlank(json)) {
                //存在缓存
                r = JSONUtil.toBean(json, type);
                return r;
            }
            if (json != null) {
                //缓存为 ""
                return null;
            }
            // 缓存不存在--》 查询数据库


            //  查询数据库
            r = dbFallback.apply(id);
            if (r == null) {
                //缓存空值
                stringRedisTemplate.opsForValue().set(key, "", time, unit);
            }
            //缓存重建
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r), time, unit);
            //返回数据
            return r;
        }
        // 3.2 获取锁失败 -》休眠重试
            //休眠
            Thread.sleep(50);
            // 递归重试
            return queryMutex(keyPrefix, id, type, dbFallback, lockPrefix, time, unit);
        }
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            unlock(lockKey);
        }
    }



    //endrange
    /**
     * 线程池
     */
    private  static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 获取所
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);

    }

    /**
     * 释放锁
     * @param key
     */
    private  void unlock(String key){
        stringRedisTemplate.delete(key);
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis 线程控制 总结

前言 相关系列 《Redis & 目录》&#xff08;持续更新&#xff09;《Redis & 线程控制 & 源码》&#xff08;学习过程/多有漏误/仅作参考/不再更新&#xff09;《Redis & 线程控制 & 总结》&#xff08;学习总结/最新最准/持续更新&#xff09;《Redis &a…

如何指定 Maven 的 JDK 版本?

maven 路径&#xff1a;/data/maven/ jdk 路径&#xff1a;/data/jdk_1.8 1、修改 mvn 可执行文件并指定 JDK 版本 vim /data/maven/bin/mvn # 在开头新增即可... # zhurs add JAVA_HOME PATH JAVA_HOME/data/jdk_1.8 ...保存退出即可&#xff01; 为什么在此处新增&#x…

AIGC时代的数据盛宴:R语言引领数据分析新风尚

文章目录 一、AIGC时代的挑战与R语言的机遇二、R语言在AIGC时代的数据预处理与清洗三、R语言在AIGC时代的统计分析四、R语言在AIGC时代的数据可视化五、R语言在AIGC时代的自动化报告生成六、R语言在AIGC时代的优势与未来发展《R语言统计分析与可视化从入门到精通》亮点内容简介…

软工毕设开题建议

文章目录 &#x1f6a9; 1 前言1.1 选题注意事项1.1.1 难度怎么把控&#xff1f;1.1.2 题目名称怎么取&#xff1f; 1.2 开题选题推荐1.2.1 起因1.2.2 核心- 如何避坑(重中之重)1.2.3 怎么办呢&#xff1f; &#x1f6a9;2 选题概览&#x1f6a9; 3 项目概览题目1 : 深度学习社…

spring-boot(4)

1.VueRouter安装与使用 2.状态管理VueX 3.Mock 如果后端没写好&#xff0c;可以通过这个来随机返回前端需要的后端数据&#xff0c;只不过是随机的。 vue: mounted:function (){console.log("-------------------------------------Hello");axios.get("http://…

华为原生鸿蒙操作系统的发布有何重大意义和影响:

#1024程序员节 | 征文# 一、华为原生鸿蒙操作系统的发布对中国的意义可以从多个层面进行分析&#xff1a; 1. 技术自主创新 鸿蒙操作系统的推出标志着中国在操作系统领域的自主创新能力的提升。过去&#xff0c;中国在高端操作系统方面依赖于外国技术&#xff0c;鸿蒙的发布…

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-24

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-24 目录 文章目录 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-24目录1. Optimizing Preference Alignment with Differentiable NDCG Ranking摘要研究背景问题与挑战如何解决创新点算法模型算…

Flutter登录界面使用主题

Now, let’s use the theme we initially created in our main function for a simple login screen: 现在&#xff0c;让我们使用最初在主函数中创建的主题来制作一个简单的登录屏幕&#xff1a; Create a Login Screen Widget: Inside the main.dartfile, create a new wid…

【Qt】常用控件:按钮类控件

思维导图&#xff1a; 一、Push Button 我们可以使用 QPushButton 表示一个按钮&#xff0c;这也是当前我们最熟悉的一个控件。QPushButton继承于QAbstractButton。这个类是一个抽象类&#xff0c;是按钮的父类。 1.1 常用的属性 属性说明text按钮中的文本icon按钮中的图标ic…

「C/C++」C++ STL容器库 之 std::list 双向链表容器

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…

【STM32】单片机ADC原理详解及应用编程

本篇文章主要详细讲述单片机的ADC原理和编程应用&#xff0c;希望我的分享对你有所帮助&#xff01; 目录 一、STM32ADC概述 1、ADC&#xff08;Analog-to-Digital Converter&#xff0c;模数转换器&#xff09; 2、STM32工作原理 二、STM32ADC编程实战 &#xff08;一&am…

小程序中设置可拖动区域

官方说明文档&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/component/movable-area.htmlhttps://developers.weixin.qq.com/miniprogram/dev/component/movable-view.html demo&#xff1a;浮动控件上下移动交互 .wxmx <movable-area><!-- y"…

python之多任务爬虫——线程、进程、协程的介绍与使用(16)

文章目录 1、什么是多任务?1.1 进程和线程的概念1.2 多线程与多进程的区别1.3 并发和并行2、python中的全局解释器锁3、多线程执行机制4、python中实现多线程(threading模块)4.1 模块介绍4.2 模块的使用5、python实现多进行程(Multiprocessing模块)5.1 导入模块5.2 模块的…

多层感知机的从零实现与softmax的从零实现(真·0000零基础)

今天再读zh.d2l书&#xff08;4.2. 多层感知机的从零开始实现 — 动手学深度学习 2.0.0 documentation&#xff09;&#xff0c; 看了关于多层感知机的从零实现与softmax的从零实现 目录 mlp从零实现&#xff0c; 点击“paddle”的代码 点击“torch”的代码 训练 参数解…

k8s部署使用有状态服务statefulset部署eureka集群,需登录认证

一、构建eureka集群镜像 1、编写dockerfile文件&#xff0c;此处基础镜像为arm版本&#xff0c;eureka目录中文件内容&#xff1a;application-dev.yml、Dockerfile、eureka-server-1.0-SNAPSHOT.jar(添加登录认证模块&#xff0c;文章最后附上下载连接) FROM mdsol/java8-j…

深入探索:深度学习在时间序列预测中的强大应用与实现

引言&#xff1a; 时间序列分析是数据科学和机器学习中一个重要的研究领域&#xff0c;广泛应用于金融市场、天气预报、能源管理、交通预测、健康监控等多个领域。时间序列数据具有顺序相关性&#xff0c;通常展示出时间上较强的依赖性&#xff0c;因此简单的传统回归模型往往…

基于SpringBoot的“社区维修平台”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“社区维修平台”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 管理员登录页面 住户管理页面 社区公关管理页面 维…

【JVM】——JVM运行机制、类加载机制、内存划分

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;JVM引入 1&#xff1a;编程语言 2&#xff1a;JAVA运行机制 二&#xff1a;JVM中内存…

EVADC模块多路触发导致AD值波动

前言&#xff1a;最近开发中遇到一个问题&#xff0c;某一路ADC通道采集的AD值波动比较厉害&#xff0c;达到9个hex值波动&#xff0c;对此进行了分析排查...... 1 排除硬件因素 用示波器对电路图上该ADC通道的测试点进行电压测量&#xff0c;发现电压比较稳定&#xff0c;换算…