Sentinel限流算法:滑动时间窗算法、漏桶算法、令牌桶算法。拦截器定义资源实现原理

文章目录

    • 滑动时间窗算法
      • 基本知识
      • 源码算法分析
    • 漏桶算法
    • 令牌桶算法
    • 拦截器处理web请求



滑动时间窗算法

基本知识

限流算法最简单的实现就是使用一个计数器法。比如对于A接口来说,我要求一分钟之内访问量不能超过100,那么我们就可以这样来实现:

  • 最开始的时候就设置一个count值,每当一个请求过来我就count++
  • 如果count的值大于了100,并且与第一个请求的时间间隔小于1分钟,那么就表示请求数过多
  • 如果该请求与第一个请求的间隔时间大于1分钟,且count的值还在限流范围内,那么就重置count。

在这里插入图片描述

/**
 * @Description: 限流算法之计数器法,伪代码的实现
 * @Author 胡尚
 * @Date: 2024/7/12 8:50
 */
public class Count {
    // 当前时间
    private Long timeStamp = System.currentTimeMillis();
    // 请求数量
    private int count = 0;
    // 时间窗口的最大请求数
    private final int limit = 100;
    // 时间窗口ms
    private final long interval = 1000 * 60;

    /**
     * 限流校验,校验是否成功
     * @return true表示允许请求,false表示校验未通过
     */
    public boolean check(){
        long now = System.currentTimeMillis();
        // 还在一分钟 时间窗口之内
        if (now <= timeStamp + interval){
            count++;
            return count <= limit;
        } else {
            timeStamp = System.currentTimeMillis();
            // 超时后重置请求数
            count = 1;
        }

        return true;
    }
}



计数法的实现有一个缺点,那就是限流的时间精度不准确。比如第一个时间窗口的前半分钟只有十个请求,后半分钟有90个请求;第二个时间窗口的前半分钟有90个请求,后半分钟只有十个请求。这种情况下,限流器是不会出现限流的,但是我在在一个时间段中的请求却有180个了。

在这里插入图片描述



为了解决计算器法统计时间精度不够的问题,进而引入了滑动时间窗

滑动时间窗算法的基本原理是维护一个固定大小的时间窗口,‌窗口内的数据被认为是当前分析的有效数据。‌随着时间的推移,‌新的数据点进入窗口,‌而旧的数据点则被移出窗口,‌从而形成了一个滑动的时间窗口。‌

在这里插入图片描述


在上图中,一个红色窗格就是一个时间窗口,我们把一个时间窗口分为了6个小格子。就拿时间窗口为一分钟举例,上面每一个小格子就是10秒;每过10秒,我们的时间窗口就会往右移动一格。每一个格子都有自己的计数器count,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。



在具体实现上,‌滑动时间窗算法可以通过多种数据结构来实现,‌例如使用环形数组、‌哈希表等。‌例如,‌可以使用一个环形数组来存储时间窗口内的数据点,‌数组的大小等于时间窗口的大小(‌以时间单位为单位)‌。‌每当有新的数据点进入时,‌旧的对应时间点的数据将被覆盖,‌从而实现滑动时间窗的效果。‌此外,‌还可以使用哈希表来记录每个时间点上的数据变化情况,‌其中键为时间点,‌值为该时间点的数据值或变化量。‌



/**
 * @Description: 限流算法之滑动时间窗算法的伪代码实现
 * 假设每秒的请求不能超过100,我们设置一个1s的时间窗口,时间窗口中共有10个小格子,
 * 每个格子记录100ms的请求数,每100毫秒移动一次,每次移动都需要记录当前服务请求数
 * 我这里就简单实现,只有一个计数器,最新的小窗口永远存储最新访问请求总数。当然也可以每个小窗口都有自己的计数器。
 * @Author 胡尚
 * @Date: 2024/7/12 9:23
 */
public class SlidingTimeWindow {

    /**
     * 服务器访问次数,可以放在redis中实现分布式系统访问
     */
    private Long count = 0L;
    /**
     * 滑动时间窗,使用linkedList来记录滑动窗口的10个格子
     */
    private LinkedList<Long> slots = new LinkedList<>();

    public static void main(String[] args) throws InterruptedException {
        SlidingTimeWindow timeWindow = new SlidingTimeWindow();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    timeWindow.onCheck();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        // 模拟一直都有请求数,随机休眠几毫秒
        while(true){
            // TODO 限流校验
            timeWindow.count++;
            Thread.sleep(new Random().nextInt(15));
        }

    }

    private void onCheck() throws InterruptedException {
        while (true){
            // 把当前访问总数存入时间小窗口中
            slots.add(count);

            // 时间窗口的剔除操作
            if (slots.size() > 10){
                slots.removeFirst();
            }

            // 最新的时间小窗口的数和最老的时间小窗口数进行比较,是否要限流
            if (slots.peekLast() - slots.peekFirst() > 100 ){
                // TODO 限流标识
            } else{
                // TODO 解除限流标识
            }

            Thread.sleep(100);
        }
    }


}



源码算法分析

我们接下来看看Sentinel它的滑动时间窗是怎么实现的。我们从StatisticSlot类的entry()方法开始看,因为它每次都会记录请求通过/请求拒绝相关的计数

public void entry (...) throws Throwable {
    ...
        // 请求通过,我们详细看看这个方法的处理逻辑
        node.addPassRequest(count);
}

// 调用到StatisticNode类的addPassRequest()方法中
public void addPassRequest(int count) {
    // 一个记录秒级
    // 它的创建语句 :Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,IntervalProperty.INTERVAL);
    // 小窗口总数SampleCountProperty.SAMPLE_COUNT = 2   滑动时间窗总时间数IntervalProperty.INTERVAL=1000 这两个数在下面的方法中会用到
    rollingCounterInSecond.addPass(count);
    // 一个记录分钟级
    rollingCounterInMinute.addPass(count);
}

// 进入到addPass()方法中
public void addPass(int count) {
    // 获取到当前时间小窗口
    WindowWrap<MetricBucket> wrap = data.currentWindow();
    // 往时间小窗口中进行相加操作
    // wrep.value()获取的是MetricBucket对象,它存储了一个LongAdder[] counters数组
    // 所以这里的添加操作应该是往LongAdder[]数组中对应的某一个小标进行相加操作
    wrap.value().addPass(count);
}



我们首先详细分析data.currentWindow();这里是如何根据当前时间获取到对应的时间小窗口的

  • 计算出当前请求时间对应的小窗口下标idx
  • 计算出当前请求的小窗口起始时间
  • 取出idx对应的老的小窗口对象
    • 如果老的小窗口对象为null,那么就直接创建一个小窗口对象,并存入滑动窗口对应的小窗口位置
    • 如果老的小窗口对象所代表的小窗口起始时间 = 当前请求的小窗口起始时间 ,那么就返回老的小窗口对象
    • 如果老的小窗口对象所代表的小窗口起始时间 < 当前请求的小窗口起始时间,那么就会替换覆盖掉老的小窗口对象
    • 时钟回拨的处理,创建一个新的无关紧要的小窗口对象返回
public WindowWrap<T> currentWindow(long timeMillis) {
    // 当前时间戳基本上不会小于0
    if (timeMillis < 0) {
        return null;
    }

    // 获取当前时间对应的小窗口的下标数: timeMillis / 小窗口总时间数 % 小窗口数
    // 比如当前限流时间是1秒,移动窗口的小窗口数是2,那么每个小窗口总时间数就是500ms。
    // 假如当前时间为700ms。那么就是 700 / 500 = 1 ,然后1%2 = 1  最终得到我当时间所在的小窗口是下标为1的小窗口
    int idx = calculateTimeIdx(timeMillis);
    
    // Calculate current bucket start time.
    // 计算当前小窗口的启动时间:timeMillis - timeMillis % 小窗口总时间数
    // 还是上面的案例: 700 - 700%500 = 500。 那么700ms这个时间对应的小窗口启动时间为500
    // 假如当前时间为1600ms:1600 - 1600%500 = 1500。  那么1600md对应小窗口的启动时间是1500
    long windowStart = calculateWindowStart(timeMillis);

    while (true) {
        // 取出老小窗口对象WindowWrap,根据上面得到的当前时间对应的小窗口的下标数
        WindowWrap<T> old = array.get(idx);
        
        // 如果滑动时间窗口中,该小窗口为null
        if (old == null) {
            // 那么就创建一个小窗口对象WindowWrap,参数有小窗口的总时长、小窗口的起始时间、当前请求时间戳
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            // 再用CAS算法存入时间窗口中
            if (array.compareAndSet(idx, null, window)) {
                return window;
            } else {
                Thread.yield();
            }


            // 如果当前时间对应的小窗口起始时间 = 老小窗口的起始时间。那么就直接返回老的小窗口对象
            // 可能有一个700ms的请求先过来,创建了一个WindowWrap小窗口对象。然后来了一个750ms的请求,这时它们的小窗口的起始时间就是一样的
        } else if (windowStart == old.windowStart()) {
            return old;


            // 如果当前时间对应的小窗口起始时间 > 老的小窗口的起始时间。那么就把老的小窗口对象给覆盖掉
            // 可能有一个700ms的请求先过来,创建了一个WindowWrap小窗口对象。
            // 然后来了一个1600ms的请求,这时它们的小窗口的起始时间就是500 和 1500
            // 这里就会调用resetWindowTo(old, windowStart);方法进行滑动时间窗,剔除掉老的小窗口
        } else if (windowStart > old.windowStart()) {

            if (updateLock.tryLock()) {
                try {
                    // 重置小窗口
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } else {
                Thread.yield();
            }

            
            // 时钟回拨的处理逻辑,直接返回一个新的小窗口WindowWrap对象,它和滑动窗口都没有关系
        } else if (windowStart < old.windowStart()) {
            // Should not go through here, as the provided time is already behind.
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}



我们再看看往时间小窗口中进行相加操作的具体实现wrap.value().addPass(count);

// 往时间小窗口中进行相加操作
// wrep.value()获取的是MetricBucket对象,它存储了一个LongAdder[] counters数组
// 所以这里的添加操作应该是往LongAdder[]数组中对应的某一个小标进行相加操作
wrap.value().addPass(count);

// 方法调用
public void addPass(int n) {
    // 这里就传了一个代码pass通过的枚举对象
    add(MetricEvent.PASS, n);
}

// 往代码Pass通过的数组下标对应位置进行相加操作
public MetricBucket add(MetricEvent event, long n) {
    // event.ordinal()获取的其实就是当前对象在枚举中的下标数
    // 官方解释:返回此枚举常量的序号(其在枚举声明中的位置,其中初始常量被赋值为0的序数)
    counters[event.ordinal()].add(n);
    return this;
}



通过上方的代码我们就已经知道了Sentinel在滑动窗口上的具体实现。我们现在再看看限流FlowSlot流程中,从时间窗口中取值的流程。

快速失败方式 --> DefaultController.canPass()

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    // 当前时间窗口中取统计指标数据
    int curCount = avgUsedTokens(node);
    // 当前qps > count阈值,那么就要返回false
    if (curCount + acquireCount > count) {
        // 因为prioritized默认情况下都是false,所以下面if不需要花太多精力去看
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
            ...
        }
        return false;
    }
    return true;
}


// 我们看看avgUsedTokens(node)方法是怎么从时间窗口中取值的。最终会调用至	ArrayMetric.pass()方法中
public long pass() {
    data.currentWindow();
    long pass = 0;
    // data.values()其实就是从滑动时间窗口中取所有的小窗口对象中存的MetricBucket对象
    List<MetricBucket> list = data.values();

    for (MetricBucket window : list) {
        // 求总和
        pass += window.pass();
    }
    return pass;
}



漏桶算法

漏桶算法,又称leaky bucket。

在这里插入图片描述


从图中我们可以看到,整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。


我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,我们可以保证接口会以一个常速速率来处理请求。所以漏桶算法天生不会出现临界问题。



/**
 * @Description: 限流算法之漏桶算法  伪代码实现
 * @Author 胡尚
 * @Date: 2024/7/12 11:57
 */
public class LeakyBucket {
    /**
     * 当前时间
     */
    private long timeStamp = System.currentTimeMillis();
    /**
     * 桶的容量
     */
    private long capacity;
    /**
     * 水漏出的速度(每秒系统能处理的请求数)
     */
    private long rate;
    /**
     * 当前水量(当前累积请求数)
     */
    private long water;

    public boolean check() {
        long now = System.currentTimeMillis();
        // 当前水量
        water = Math.max(0, capacity - ((now - timeStamp) / 1000) * rate); 
        // 更新timeStamp
        timeStamp = now;
        if (water + 1 > capacity){
            // 水满了,直接拒绝
            return false;
        }else{
            // 还能装水
            water++;
            return true;
        }
    }
}



我们接下来看看sentinel,它在流控规则中的排队等待是怎么实现漏桶算法的。

  • 计算出两次请求应该的间隔时间costTime
  • 计算出下一次请求期望的时间点expectedTime
  • 如果currentTime > currentTime 就返回true,放行
    • 否则计算当前请求需要等待时间waitTime
    • 判断waitTime > 最大超时时间 就返回false
    • 否则再又重新计算一次等待时间,重新判断一次是否超过最大等待时间,再去进入等待sleep()
// 排队等待 --> RateLimiterController.canPass() ---> 漏桶算法
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    if (acquireCount <= 0) {
        return true;
    }

    if (count <= 0) {
        return false;
    }

    // 获取当前请求的当前时间
    long currentTime = TimeUtil.currentTimeMillis();

    // acquireCount一般就是一次请求,也就是1   count就是我们在控制台界面中设置的阈值
    // costTime就是算出两次请求之间应该的间隔时间,比如我1s中只能允许10个请求。那么这里就是 1.0 / 10 * 1000 = 100ms。
    // 也就是说我两次请求的间隔时间应该是100毫秒
    long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
    
    // expectedTime就是一个期望时间。 期望时间 = 两次请求间隔时间 + 上一次请求时间
    // 比如我上一个请求的时间为300ms,那么我期望下一个请求应该是400ms
    long expectedTime = costTime + latestPassedTime.get();

    // 假如我当前的请求时间 比期望时间还要大,那么就直接更新最后一次请求时间,并返回true放行当前请求
    // 比如我期望下一个请求的时间是400ms。但我现在请求的时间已经是450ms了,我比你期望的时间还要长,那你是不是肯定要给我放行了
    if (expectedTime <= currentTime) {
        latestPassedTime.set(currentTime);
        return true;
    } else {
        // 接下来就是我当前的请求时间,比期望的时间要小的处理逻辑

        // 重新算出来的期望时间 - 当前请求的时间 = 我当前这个请求需要等待的时间
        long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
        
        // 如果我这个请求要等待的时间,超过了控制台界面设置的最大超时时间那么就直接返回false
        if (waitTime > maxQueueingTimeMs) {
            return false;
        } else {
            // 接下来是我当前请求需要进行等待的处理逻辑

            // 修改上一次请求时间,上一次请求时间 + 间隔时间 = oldTime(其实就是重新算出来的期望时间)
            long oldTime = latestPassedTime.addAndGet(costTime);
            try {
                // 重新计算一次,期望时间 - 当前请求的时间 = 当前请求要等待的时间
                waitTime = oldTime - TimeUtil.currentTimeMillis();
                // 当前请求要等待的时间 是否大于了最大超时时间,如果大于了就把上一次请求时间改回来,在直接返回false
                if (waitTime > maxQueueingTimeMs) {
                    latestPassedTime.addAndGet(-costTime);
                    return false;
                }
                // 当前线程进入等待
                if (waitTime > 0) {
                    Thread.sleep(waitTime);
                }
                return true;
            } catch (InterruptedException e) {
            }
        }
    }
    return false;
}



令牌桶算法

令牌桶算法,又称token bucket。

在这里插入图片描述

有一个线程,会根据一定增长的预热速率往桶中放令牌,如果桶中的令牌满了那么就就直接将令牌丢弃掉。

当有请求过来时就会从桶中获取令牌,如果获取到了就放行,如果没有获取到就拒绝。



/**
 * 令牌桶限流算法  伪代码
 * 这里就没有预热、增长的速率往桶中放入令牌
 */
public class TokenBucket {
    public long timeStamp = System.currentTimeMillis();  // 当前时间
    public long capacity; // 桶的容量
    public long rate; // 令牌放入速度
    public long tokens; // 当前令牌数量
 
    public boolean grant() {
        long now = System.currentTimeMillis();
        // 先添加令牌
        tokens = Math.min(capacity, tokens + (now - timeStamp) * rate);
        timeStamp = now;
        if (tokens < 1) {
            // 若不到1个令牌,则拒绝
            return false;
        } else {
            // 还有令牌,领取令牌
            tokens--;
            return true;
        }
    }
}

对应的sentinel中,Warm Up的实现就是基于令牌桶实现的,Warm Up --> WarmUpController.canPass() --> 令牌桶算法

这里就不展开介绍sentinel具体的实现算法了

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    long passQps = (long) node.passQps();

    long previousQps = (long) node.previousPassQps();
    syncToken(previousQps);

    // 开始计算它的斜率
    // 如果进入了警戒线,开始调整他的qps
    long restToken = storedTokens.get();
    if (restToken >= warningToken) {
        long aboveToken = restToken - warningToken;
        // 消耗的速度要比warning快,但是要比慢
        // current interval = restToken*slope+1/count
        double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
        if (passQps + acquireCount <= warningQps) {
            return true;
        }
    } else {
        if (passQps + acquireCount <= count) {
            return true;
        }
    }

    return false;
}



限流算法小结

计数器 VS 滑动窗口:

计数器算法是最简单的算法,可以看成是滑动窗口的低精度实现。
滑动窗口由于需要存储多份的计数器(每一个格子存一份),所以滑动窗口在实现上需要更多的存储空间。
也就是说,如果滑动窗口的精度越高,需要的存储空间就越大。

漏桶算法 VS 令牌桶算法:

漏桶算法和令牌桶算法最明显的区别是令牌桶算法允许流量一定程度的突发。
因为默认的令牌桶算法,取走token是不需要耗费时间的,也就是说,假设桶内有100个token时,那么可以瞬间允许100个请求通过。
当然我们需要具体情况具体分析,只有最合适的算法,没有最优的算法。



拦截器处理web请求

我们引入了下面的依赖,也就是我们实际使用sentinel的场景下,我们会发现我们controller层编写的http请求接口都不需要我们自己额外的定义资源开启保护。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

会自动的把我们的http请求接口定义成一个一个的资源。具体的实现如下:

入口是SentinelWebAutoConfiguration自动配置类

在这里插入图片描述



SentinelWebAutoConfiguration自动配置类它实现了WebMvcConfigurer接口,所以在SpringBoot启动过程中就会调用这个类重写的addInterceptors方法

public class SentinelWebAutoConfiguration implements WebMvcConfigurer {

	......

    // 此数组存的就是最下面创建的拦截器
	@Autowired
	private Optional<SentinelWebInterceptor> sentinelWebInterceptorOptional;

	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		if (!sentinelWebInterceptorOptional.isPresent()) {
			return;
		}
		SentinelProperties.Filter filterConfig = properties.getFilter();
        // 最终下面创建的拦截器会被添加在这里来
		registry.addInterceptor(sentinelWebInterceptorOptional.get())
				.order(filterConfig.getOrder())
				.addPathPatterns(filterConfig.getUrlPatterns()); // 这里其实就是拦截所有的请求  /**
		log.info(...);
	}

    // 往Spring容器中存一个SentinelWebInterceptor拦截器
	@Bean
	@ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled",matchIfMissing = true)
	public SentinelWebInterceptor sentinelWebInterceptor(SentinelWebMvcConfig sentinelWebMvcConfig) {
		return new SentinelWebInterceptor(sentinelWebMvcConfig);
	}
    
    ...
}

SentinelWebInterceptor extends AbstractSentinelInterceptor 这里是该类的继承关系,我们直接去看它父类中的preHandle()方法

// 前置拦截器方法
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    try {
        String resourceName = this.getResourceName(request);
        if (StringUtil.isEmpty(resourceName)) {
            return true;
        } else if (this.increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
            return true;
        } else {
            String origin = this.parseOrigin(request);
            String contextName = this.getContextName(request);
            ContextUtil.enter(contextName, origin);
            
            // 这里定义了资源,开启了保护
            Entry entry = SphU.entry(resourceName, 1, EntryType.IN);
            request.setAttribute(this.baseWebMvcConfig.getRequestAttributeName(), entry);
            return true;
        }
    } catch (BlockException var12) {
        // 如果出现了BlockException,则去相应的处理逻辑
        BlockException e = var12;

        try {
            this.handleBlockException(request, response, e);
        } finally {
            ContextUtil.exit();
        }

        return false;
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java面试八股之Redis Stream的实现原理及应用场景

Redis Stream的实现原理及应用场景 Redis Stream是一种在Redis 5.0版本中引入的数据结构&#xff0c;它主要用于实现高效的消息队列服务。下面我将详细解释其实现原理以及一些常见的应用场景。 实现原理 1. 结构组成&#xff1a; - Redis Stream由一个或多个消息组成&#xf…

链接追踪系列-00.es设置日志保存7天-番外篇

索引生命周期策略 ELK日志我们一般都是按天存储&#xff0c;例如索引名为"zipkin-span-2023-03-24"&#xff0c;因为日志量所占的存储是非常大的&#xff0c;我们不能一直保存&#xff0c;而是要定期清理旧的&#xff0c;这里就以保留7天日志为例。 自动清理7天以前…

.NET MAUI开源架构_2.什么是 .NET MAUI?

1.什么是.NET MAUI&#xff1f; .NET 多平台应用 UI (.NET MAUI) 是一个跨平台框架&#xff0c;用于使用 C# 和 XAML 创建本机移动和桌面应用。使用 .NET MAUI&#xff0c;可从单个共享代码库开发可在 Android、iOS、macOS 和 Windows 上运行的应用。 .NET MAUI 是一款…

主机安全-开源HIDS字节跳动Elkeid安装使用

目录 概述什么是HIDSHIDS与NIDS的区别EDR、XDR是啥&#xff1f; Elkeid架构Elkeid Agent && Agent centerElkeid DriverElkeid RASPElkeid HUBService DiscoveryManager安装数据采集规则&告警 参考 概述 什么是HIDS HIDS&#xff08; host-based intrusion detec…

Java-寻找二叉树两结点最近公共祖先

目录 题目描述&#xff1a; 注意事项&#xff1a; 示例&#xff1a; 示例 1&#xff1a; 示例 2&#xff1a; 示例 3&#xff1a; 解题思路&#xff1a; 解题代码&#xff1a; 题目描述&#xff1a; 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科…

怎么关闭Windows安全中心?

Windows安全中心是Windows操作系统中的一项重要功能&#xff0c;系统提供这个功能的目的是保护电脑免受各种安全威胁。尽管如此&#xff0c;有时候我们可能出于某些原因需要关闭它。本文将详细介绍如何关闭Windows安全中心&#xff0c;以及需要注意的事项。 重要提醒&#xff1…

kubernetes k8s 控制器 Replicaset 配置管理

目录 1、Replicaset控制器&#xff1a;概念、原理解读 1.1 Replicaset概述 1.2 Replicaset工作原理&#xff1a;如何管理Pod&#xff1f; 2、 Replicaset资源清单文件编写技巧 3、Replicaset使用案例&#xff1a;部署Guestbook留言板 4、Replicaset管理pod&#xff1a;扩…

CUDA编程00 - 配置CUDA开发环境

第一步&#xff1a;在一台装有Nvidia显卡和驱动的机器上&#xff0c;用nvidia-smi命令查看显卡所支持cuda版本 第二步&#xff1a; 到Nvidia官网下载CUDA Toolkit并安装&#xff0c;CUDA Toolkit Archive | NVIDIA Developer 安装时按提示下一步即可&#xff0c;安装完成用 nv…

【Harmony】SCU暑期实训鸿蒙开发学习日记Day1

关于ArkTS和ArkUI&#xff0c;基础语法请看&#x1f449;官方开发手册 系统学习后&#xff0c;聊聊几个点&#xff0c;面向刚学习这门语言的小白&#xff0c;用于巩固和回顾&#x1f60b; 目录 类型推断应用 函数相关 布局方式 线性布局 堆叠布局 网格布局 弹性布局 …

补充.IDEA的使用

首先我们要了解在idea中Java工程由项目&#xff08;project&#xff09;、模块&#xff08;module&#xff09;包&#xff08;package&#xff09;、类&#xff08;class&#xff09;组成。 他们之间的关系是project包含module包含package包含class。 所以我们要按照先建一个pr…

睡前故事—绿色科技的未来:可持续发展的梦幻故事

欢迎来到《Bedtime Stories Time》。这是一个我们倾听、放松、并逐渐入睡的播客。感谢你收听并支持我们&#xff0c;希望你能将这个播客作为你睡前例行活动的一部分。今晚我们将讲述绿色科技的未来&#xff1a;可持续发展的梦幻故事的故事。一个宁静的夜晚&#xff0c;希望你现…

1千多看图猜成语游戏ACCESS\EXCEL数据库

今天闲来无事想写个代码自己搞定&#xff0c;我不写代码已经很久了&#xff0c;主要是年纪不小了对新技术的学习比较吃力&#xff0c;兴趣也被生活打磨的体无完肤。今天又捡起VB&#xff08;暴露了年纪&#xff09;搞了一下。 当然&#xff0c;很多事情都是这样&#xff0c;自己…

PySide(PyQt)判断QLineEdit的输入是否合规

判断QLineEdit的输入是否符合要求&#xff0c;比如是否为整数或者浮点数。 1、使用正则表达式来判断 符合正则表达式则输入合规 import sys import re from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit, QLabelclass ExampleWidget(QWidget):…

一个用于管理多个 Node.js 版本的安装和切换开源工具

大家好&#xff0c;今天给大家分享一个用于管理多个Node.js版本的工具 NVM&#xff08;Node Version Manager&#xff09;&#xff0c;它允许开发者在同一台机器上安装和使用不同版本的Node.js&#xff0c;解决了版本兼容性问题&#xff0c;为开发者提供了极大的便利。 在开发环…

Kafka深入解析

一、kafka存储结构 1.kafka为什么使用磁盘作为存储介质 2.分析文件存储格式 3.快速检索消息 1.kafka存储结构 Kafka 的基本存储单元是分区&#xff08;partition&#xff09; &#xff08;1&#xff09;每个partition相当于一个大文件被平均分配到多个大小相等的segment段(数…

【Godot4.2】MLTag类:HTML、XML通用标签类

概述 HTML和XML采用类似的标签形式。 之前在Godot中以函数库形式实现了网页标签和内容生成。能用&#xff0c;但是缺点也很明显。函数之间没有从属关系&#xff0c;但是多有依赖&#xff0c;而且没有划分出各种对象和类型。 如果以完全的面向对象形式来设计标签类或者元素类…

stm32精密控制步进电机(升级篇)

这一篇文章里会深入的对步进电机控制方法进行论述 如何避免步进电机丢转的问题 1.机械结构&#xff1a;排查一下传动的问题&#xff0c;举个例子&#xff0c;我的毕设里大臂机械臂的步进电机有时会有丢转问题&#xff0c;造成无法运动到指定位置&#xff0c;后面发现是因为皮带…

爬虫代理访问超时怎么解决?

一、为什么会出现访问超时 爬虫使用代理可能会遇到访问超时的情况&#xff0c;主要和以下几个方面有关&#xff1a; 1.代理服务器性能&#xff1a; 代理服务器作为中间层&#xff0c;承担着转发请求和响应的任务。如果代理服务器性能不佳或超载&#xff0c;请求的响应时间可能…

ELK日志管理

文章目录 一、ELK概述什么是ELK?为什么使用ELK&#xff1f;ELK的工作原理 二、安装部署ELK前期准备安装部署Elasticsearch 软件修改系统配置安装插件在应用服务器上部署 Logstash安装 kibana 一、ELK概述 什么是ELK? 通俗来讲&#xff0c;ELK 是由 Elasticsearch、Logstash…

LeetCode热题100刷题16:74. 搜索二维矩阵、33. 搜索旋转排序数组、153. 寻找旋转排序数组中的最小值、98. 验证二叉搜索树

74. 搜索二维矩阵 class Solution { public:bool searchMatrix(vector<vector<int>>& matrix, int target) {int row matrix.size();int col matrix[0].size();for(int i0;i<row;i) {//先排除一下不存在的情况if(i>0&&matrix[i][0]>target…