Sentinel 滑动时间窗口源码分析

news2024/9/21 14:46:51

前言:

Sentinel 的一个重要功能就是限流,对于限流来说有多种的限流算法,比如滑动时间窗口算法、漏桶算法、令牌桶算法等,Sentinel 对这几种算法都有具体的实现,如果我们对某一个资源设置了一个流控规则,并且选择的流控模式是“快速失败”,那么 Sentinel 就会采用滑动时间窗口算法来作为该资源的限流算法,本篇我们来分析 Sentinel 中滑动时间窗口算法的实现。

Sentinel 系列文章传送门:

Sentinel 初步认识及使用

Sentinel 核心概念和工作流程详解

Spring Cloud 整合 Nacos、Sentinel、OpenFigen 实战【微服务熔断降级实战】

Sentinel 源码分析入门【Entry、Chain、Context】

Sentine 源码分析之–NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot

Sentine 源码分析之–AuthoritySlot、SystemSlot、GatewayFlowSlot

Sentine 源码分析之–ParamFlowSlot

Sentine 源码分析之–FlowSlot

滑动窗口的原理

滑动窗口是一种常用的算法,用于统计一段时间内的事件或数据点,在限流场景中,滑动窗口将时间窗口分割成多个小的时间片段(通常称为桶,也可以叫做样本窗口),每个时间片段独立统计,随着时间的推移,最旧的时间片段的数据会被新的时间片段替换,形成“滑动”的效果,在具体实现上,‌滑动时间窗算法可以通过多种数据结构来实现,‌例如使用环形数组、‌哈希表等,可以使用一个环形数组来存储时间窗口内的数据点,‌数组的大小等于时间窗口的大小,每当有新的数据点进入时,‌旧的对应时间点的数据将被覆盖,‌从而实现滑动时间窗的效果,‌此外,‌也可以使用哈希表结构来实现滑动时间窗口,其中键为时间点,‌值为该时间点的数据值或变化量。‌

滑动窗口的优点

  • 实现更细粒度的时间控制,与固定窗口(整个时间窗口只统计一次)相比,滑动窗口通过连续滑动减少了窗口切换时的流量突变,避免了请求在窗口刚开始时因为累积的计数而被误判为超限。
  • 减少突发流量对系统的影响,保证服务的稳定性和可靠性,在实际应用中,流量往往呈现出突发性特征,如果使用固定窗口算法,在窗口重置的瞬间可能会接受大量请求(时间窗口的起始点聚集大量流量),造成短时间内的服务压力,滑动窗口可以更均匀、更细粒度的控制每个时间片段内的流量,从而降低了因突发流量导致的导致的系统压力。
  • 提高系统响应的实时性,滑动窗口提供了更实时的流量数据,系统能够基于最实时的流量情况做出响应,这对于需要快速适应流量变化的在线服务尤其重要,可以即时调整资源分配和访问策略。

Sentinel 滑动时间窗口的实现

Sentinel 官网的图就很清楚的告诉了我们 Sentinel 使用环形数组实现滑动窗口,下图中的右上角就是滑动窗口的示意图,是 StatisticSlot 的具体实现,底层采用的是 LeapArray 来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。

在这里插入图片描述

滑动窗口的核心数据结构

  • ArrayMetric:滑动窗口核心实现类。
  • LeapArray:滑动窗口顶层数据结构,主要存储窗口数据。
  • WindowWrap:每一个滑动窗口的包装类,其内部的数据结构用 MetricBucket 表示。
  • MetricBucket:指标桶,例如通过数量、阻塞数量、异常数量、成功数量、响应时间,已通过未来配额(抢占下一个滑动窗口的数量)。
  • MetricEvent:指标类型,例如通过数量、阻塞数量、异常数量、成功数量、响应时间等。

ArrayMetric 构造方法源码解析

ArrayMetric 是滑动窗口的入口类,实现了 Metric 接口,该接口主要定义一个滑动窗口中成功的数量、异常数量、阻塞数量,TPS、响应时间等,ArrayMetric 提供了两个构造方法,两个构造方法的区别是在于当前时间窗口达到限制之后,是否可以抢占下一个时间窗口,具体逻辑如下:

  • intervalInMs:滑动窗口的总时间,例如 1 分钟、1 秒中。
  • sampleCount:在一个滑动窗口的总时间中的抽样的个数,默认为 2,即一个滑动窗口的总时间包含两个相等的区间,一个区间就是一个窗口。
  • enableOccupy:是否允许抢占,即当前滑动窗口已经达到限制后,是否可以占用下一个时间窗口的容量。
public class ArrayMetric implements Metric {

    private final LeapArray<MetricBucket> data;

    //com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#ArrayMetric(int, int)
    public ArrayMetric(int sampleCount, int intervalInMs) {
        //默认是可抢占的
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }

    //com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#ArrayMetric(int, int, boolean)
    public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
        //当前时间窗口容量满了 是否可抢占时间窗口
        if (enableOccupy) {
            //可抢占
            this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
        } else {
            //不可抢占
            this.data = new BucketLeapArray(sampleCount, intervalInMs);
        }
    }
}

LeapArray 源码分析

LeapArray 用来存储滑动窗口数据的,也就是所谓的环形数组,具体成员变量如下:

  • windowLengthInMs:样本窗口的时间间隔,单位秒。
  • sampleCount:样本窗口数量。
  • intervalInMs:一个滑动窗口跨越的时间长度,也就是总时间窗口。
  • array:样本窗口的集合,使用 AtomicReferenceArray 保证原子性。
public abstract class LeapArray<T> {

    //样本窗口的时间间隔 单位秒
    protected int windowLengthInMs;
    //样本窗口数量
    protected int sampleCount;
    //毫秒为单位 一个滑动窗口跨越的时间长度 也就是总时间窗口
    protected int intervalInMs;
    //样本窗口的集合 使用 AtomicReferenceArray 保证原子性
    protected final AtomicReferenceArray<WindowWrap<T>> array;

    /**
     * The conditional (predicate) update lock is used only when current bucket is deprecated.
     */
    private final ReentrantLock updateLock = new ReentrantLock();

    /**
     * The total bucket count is: {@code sampleCount = intervalInMs / windowLengthInMs}.
     *
     * @param sampleCount  bucket count of the sliding window
     * @param intervalInMs the total time interval of this {@link LeapArray} in milliseconds
     */
    public LeapArray(int sampleCount, int intervalInMs) {
        AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
        AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
        AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");

        this.windowLengthInMs = intervalInMs / sampleCount;
        this.intervalInMs = intervalInMs;
        this.sampleCount = sampleCount;

        this.array = new AtomicReferenceArray<>(sampleCount);
    }

}


MetricBucket 源码分析

MetricBucket 统计一个时间窗口内的各项指标数据,例如异常总数、成功总数等,Bucket 使用 LongAdder 数组记录一段时间内的各项指标,MetricBucket 包含一个 LongAdder 数组,数组的每个元素代表一类 MetricEvent。LongAdder 保证了数据修改的原子性。

public class MetricBucket {

    //记录各事件的计数 异常总数 成功总数等
    private final LongAdder[] counters;

    //最小耗时 默认值 5 秒
    private volatile long minRt;

    //构造方法
    public MetricBucket() {
        //遍历各种事件
        MetricEvent[] events = MetricEvent.values();
        //创建 数组
        this.counters = new LongAdder[events.length];
        for (MetricEvent event : events) {
            //事件加入数组
            counters[event.ordinal()] = new LongAdder();
        }
        //初始化最小事件
        initMinRt();
    }
}

public enum MetricEvent {

    /**
     * Normal pass.
     */
    PASS,
    /**
     * Normal block.
     */
    BLOCK,
    EXCEPTION,
    SUCCESS,
    RT,

    /**
     * Passed in future quota (pre-occupied, since 1.5.0).
     */
    OCCUPIED_PASS
}

WindowWrap 源码解析

MetricBucket 自身不保存时间窗口信息,因此 Sentinel 给 Bucket 加了一个包装类 WindowWrap,MetricBucket 用于统计各项指标数据,WindowWrap 用于记录 MetricBucket 时间窗口信息,具体属性如下:

  • windowLengthInMs:单个时间窗口的时间长度,也就是样本窗口的时间长度。
  • windowStart:样本窗口的起始时间。
  • value:样本窗口统计数据。
public class WindowWrap<T> {

    /**
     * Time length of a single window bucket in milliseconds.
     */
    //单个时间窗口的时间长度 也就是样本窗口的时间长度
    private final long windowLengthInMs;

    /**
     * Start timestamp of the window in milliseconds.
     */
    //样本窗口的起始时间
    private long windowStart;

    /**
     * Statistic data.
     */
    //样本窗口统计数据
    private T value;

    /**
     * @param windowLengthInMs a single window bucket's time length in milliseconds.
     * @param windowStart      the start timestamp of the window
     * @param value            statistic data
     */
    public WindowWrap(long windowLengthInMs, long windowStart, T value) {
        this.windowLengthInMs = windowLengthInMs;
        this.windowStart = windowStart;
        this.value = value;
    }

}

至此,Sentinel 滑动时间窗口的基本实现我们已经了解了,下面我们来分析一下 Sentinel 具体是如果使用这个滑动时间窗口的。

StatisticSlot#entry 方法源码解析

我们知道 StatisticSlot 是 Sentinel 的核心功能插槽之一,用于统计实时的调用数据,前面系列文章已经分析过,这里重点关注这行代码即可 node.addPassRequest(count)。


//com.alibaba.csp.sentinel.slots.statistic.StatisticSlot#entry
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
				  boolean prioritized, Object... args) throws Throwable {
	try {
		// Do some checking.
		//放行到下一个 slot 做限流 降级 等规则判断
		fireEntry(context, resourceWrapper, node, count, prioritized, args);

		// Request passed, add thread count and pass count.
		//请求已通过 线程数+1 用做线程隔离
		node.increaseThreadNum();
		//请求通过 计数器+1 用做限流
		node.addPassRequest(count);
		//请求来源节点判断
		if (context.getCurEntry().getOriginNode() != null) {
			// Add count for origin node.
			//来源节点不为空 来源节点的 线程数 和  计数器 也+1
			context.getCurEntry().getOriginNode().increaseThreadNum();
			context.getCurEntry().getOriginNode().addPassRequest(count);
		}
		//是否是入口资源类型
		if (resourceWrapper.getEntryType() == EntryType.IN) {
			// Add count for global inbound entry node for global statistics.
			//如果是入口资源类型 全局线程数 和 计数器 也要+1
			Constants.ENTRY_NODE.increaseThreadNum();
			Constants.ENTRY_NODE.addPassRequest(count);
		}

		// Handle pass event with registered entry callback handlers.
		//请求通过后回调
		for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
			handler.onPass(context, resourceWrapper, node, count, args);
		}
	} catch (PriorityWaitException ex) {
		//优先级等待异常这里没有增加请求失败的数量
		node.increaseThreadNum();
		//请求来源节点判断
		if (context.getCurEntry().getOriginNode() != null) {
			// Add count for origin node.
			//来源节点不为空 来源节点的 线程数 +1
			context.getCurEntry().getOriginNode().increaseThreadNum();
		}
		//是否是入口资源类型
		if (resourceWrapper.getEntryType() == EntryType.IN) {
			// Add count for global inbound entry node for global statistics.
			//如果是入口资源类型 全局线程数 +1
			Constants.ENTRY_NODE.increaseThreadNum();
		}
		// Handle pass event with registered entry callback handlers.
		//请求通过后回调
		for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
			handler.onPass(context, resourceWrapper, node, count, args);
		}
	} catch (BlockException e) {
		// Blocked, set block exception to current entry.
		//阻塞 没有通过异常  将异常信息保存在 当前的 entry 中
		context.getCurEntry().setBlockError(e);

		// Add block count.
		//增加阻塞数量
		node.increaseBlockQps(count);
		//请求来源节点判断
		if (context.getCurEntry().getOriginNode() != null) {
			//请求来源节点 阻塞数量 +1
			context.getCurEntry().getOriginNode().increaseBlockQps(count);
		}
		//是否是入口资源类型
		if (resourceWrapper.getEntryType() == EntryType.IN) {
			// Add count for global inbound entry node for global statistics.
			//如果是入口资源类型 全局阻塞数 +1
			Constants.ENTRY_NODE.increaseBlockQps(count);
		}

		// Handle block event with registered entry callback handlers.
		//回调
		for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
			handler.onBlocked(e, context, resourceWrapper, node, count, args);
		}

		throw e;
	} catch (Throwable e) {
		// Unexpected internal error, set error to current entry.
		//错误设置到 当前 Entry
		context.getCurEntry().setError(e);

		throw e;
	}
}


DefaultNode#addPassRequest 方法源码解析

DefaultNode#addPassRequest 方法没有复杂的逻辑,只是调用了 StatisticNode#addPassRequest 方法,我们接着分析。

//com.alibaba.csp.sentinel.node.DefaultNode#addPassRequest
@Override
public void addPassRequest(int count) {
	super.addPassRequest(count);
	this.clusterNode.addPassRequest(count);
}

StatisticNode#addPassRequest 方法源码解析

StatisticNode#addPassRequest 方法分别对分钟级时间窗口和秒级时间窗口进行了处理,我们选择一个分析即可,底层逻辑是一样的。

//com.alibaba.csp.sentinel.node.StatisticNode#addPassRequest
@Override
public void addPassRequest(int count) {
	//秒级时间窗口 500ms 一个样本
	rollingCounterInSecond.addPass(count);
	//分钟级时间窗口 每秒一个样本
	rollingCounterInMinute.addPass(count);
}

ArrayMetric#addPass 方法源码解析

ArrayMetric#addPass 方法主要是获取事件窗口,并对时间窗口中的对应值进行增加操作。

//com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#addPass
@Override
public void addPass(int count) {
	//获取当前的滑动时间窗口
	WindowWrap<MetricBucket> wrap = data.currentWindow();
	//时间窗口中的对应位置的值+count
	wrap.value().addPass(count);
}

LeapArray#currentWindow 方法源码解析

LeapArray#currentWindow 方法作用是获取当前滑动时间窗口。

//com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#currentWindow()
public WindowWrap<T> currentWindow() {
	//传入当前时间
	return currentWindow(TimeUtil.currentTimeMillis());
}

LeapArray#currentWindow 方法源码解析

LeapArray#currentWindow 方法主要作用是创建或者更新时间窗口,具体逻辑如下:

  • 根据当前时间戳获取样本窗口索引值。
  • 获取当前窗口的起始位置。
  • 根据当前样本时间窗口索引值,获取旧的时间窗口。
  • 如果当前样本时间窗口为 null,就创建一个样本时间窗口。
  • 不为空,首先判断计算出来的样本时间窗口其实质是否等于获取到的样本时间窗口起始值,如果等于则直接返回,如果大于则更新样本时间窗口数据,如果小于也会创建一个样本时间窗口返回(时钟回拨的情况),但是这个样本时间窗口不会参与计算。
//com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#currentWindow(long)
public WindowWrap<T> currentWindow(long timeMillis) {
	//时间小于 0 直接 return
	if (timeMillis < 0) {
		return null;
	}
	//根据当前时间戳获取样本窗口索引值
	int idx = calculateTimeIdx(timeMillis);
	// Calculate current bucket start time.
	//获取当前窗口的起始位置
	long windowStart = calculateWindowStart(timeMillis);

	/*
	 * Get bucket item at given time from the array.
	 *
	 * (1) Bucket is absent, then just create a new bucket and CAS update to circular array.
	 * (2) Bucket is up-to-date, then just return the bucket.
	 * (3) Bucket is deprecated, then reset current bucket and clean all deprecated buckets.
	 */
	while (true) {
		//根据当前样本时间窗口索引值 获取旧的时间窗口
		WindowWrap<T> old = array.get(idx);
		if (old == null) {
			/*
			 *     B0       B1      B2    NULL      B4
			 * ||_______|_______|_______|_______|_______||___
			 * 200     400     600     800     1000    1200  timestamp
			 *                             ^
			 *                          time=888
			 *            bucket is empty, so create new and update
			 *
			 * If the old bucket is absent, then we create a new bucket at {@code windowStart},
			 * then try to update circular array via a CAS operation. Only one thread can
			 * succeed to update, while other threads yield its time slice.
			 */
			//旧的时间窗口为空 创建 WindowWrap 对象 也就是创建样本时间窗口 MetricBucket
			WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
			//使用 CAS 更新索引位置的时间窗口 更新到 LeapArray 中
			if (array.compareAndSet(idx, null, window)) {
				// Successfully updated, return the created bucket.
				return window;
			} else {
				// Contention failed, the thread will yield its time slice to wait for bucket available.
				//让出 CPU 使用权
				Thread.yield();
			}
		} else if (windowStart == old.windowStart()) {
			/*
			 *     B0       B1      B2     B3      B4
			 * ||_______|_______|_______|_______|_______||___
			 * 200     400     600     800     1000    1200  timestamp
			 *                             ^
			 *                          time=888
			 *            startTime of Bucket 3: 800, so it's up-to-date
			 *
			 * If current {@code windowStart} is equal to the start timestamp of old bucket,
			 * that means the time is within the bucket, so directly return the bucket.
			 */
			//新旧时间窗口的起始时间一样 直接返回旧的时间窗口
			return old;
		} else if (windowStart > old.windowStart()) {
			/*
			 *   (old)
			 *             B0       B1      B2    NULL      B4
			 * |_______||_______|_______|_______|_______|_______||___
			 * ...    1200     1400    1600    1800    2000    2200  timestamp
			 *                              ^
			 *                           time=1676
			 *          startTime of Bucket 2: 400, deprecated, should be reset
			 *
			 * If the start timestamp of old bucket is behind provided time, that means
			 * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
			 * Note that the reset and clean-up operations are hard to be atomic,
			 * so we need a update lock to guarantee the correctness of bucket update.
			 *
			 * The update lock is conditional (tiny scope) and will take effect only when
			 * bucket is deprecated, so in most cases it won't lead to performance loss.
			 */
			//滚动窗口 因为之前就已经初始化好了对应时间的窗口规格(大小和数量),所以这里只会覆盖上一个时间周期的老数据,相当于环形数组
			//加锁
			if (updateLock.tryLock()) {
				try {
					// Successfully get the update lock, now we reset the bucket.
					//更新时间窗口
					return resetWindowTo(old, windowStart);
				} finally {
					//释放锁
					updateLock.unlock();
				}
			} else {
				// Contention failed, the thread will yield its time slice to wait for bucket available.
				Thread.yield();
			}
		} else if (windowStart < old.windowStart()) {
			// Should not go through here, as the provided time is already behind.
			//理论上来到这里是不正常的 但是这里还是重新创建了时间窗口
			//这里其实是时钟回拨问题,例如服务器时间被前调,导致了计算出来的窗口开始时间小于了现在目标的窗口时间
			//那么就新建一个窗口,仅用作统计,不会在流控 slot 中进行计算,出现这个问题肯定就会计算不准
			return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
		}
	}
}


LeapArray#calculateTimeIdx 方法源码解析

LeapArray#calculateTimeIdx 方法的作用是获取样本时间窗口的索引值,具体算法是:(当前的时间戳/样本窗口时间) % 样本窗口长度,以秒级时间窗口为例:(当前的时间戳/500) % 2。


//com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#calculateTimeIdx
private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
	//索引除以样本窗口时间 得到
	long timeId = timeMillis / windowLengthInMs;
	// Calculate current index so we can map the timestamp to the leap array.
	//得到的值 和窗口的样本数量取模得到索引值
	return (int)(timeId % array.length());
}

LeapArray#calculateWindowStart 方法源码解析

LeapArray#calculateWindowStart 方法的主要作用是计算当前样本时间窗口的起始时间。

//com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#calculateWindowStart
protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
	//当前窗口的起始时间 当前时间-当前时间和样本窗口时间取模
	return timeMillis - timeMillis % windowLengthInMs;
}

BucketLeapArray#newEmptyBucket 方法源码解析

BucketLeapArray#newEmptyBucket 方法的作用是创建指标桶 MetricBucket。

//com.alibaba.csp.sentinel.slots.statistic.metric.BucketLeapArray#newEmptyBucket
@Override
public MetricBucket newEmptyBucket(long time) {
	return new MetricBucket();
}

BucketLeapArray#resetWindowTo 方法源码解析

BucketLeapArray#resetWindowTo 方法的主要作用是更新窗口的开始时间和重置值。

//com.alibaba.csp.sentinel.slots.statistic.metric.BucketLeapArray#resetWindowTo
@Override
protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long startTime) {
	// Update the start time and reset value.
	w.resetTo(startTime);
	w.value().reset();
	return w;
}

使用循环数组的好处

而循环数组可以循环重复使用,可以避免频繁的创建 Bucket,减少内存资源的占用。

总结:Sentinel 滑动时间窗口使用了环形数组 LeapArray 来实现,而 LeapArray 内部使用了一个 WindowWrap 类型的 array 来保存样本窗口,WindowWrap 的作用是用来包装 MetricBucket,WindowWrap 数组实现滑动窗口,MetricBucket 负责统计各项指标数据,WindowWrap 用于记录 MetricBucket 的时间窗口信息,寻找样本时间窗口实际上就是寻找 WindowWrap,找到了 WindowWrap 也就找到了 MetricBucket,Sentinel 滑动时间窗口的核心就是 LeapArray 、MetricBucket 和 WindowWrap 。

欢迎提出建议及对错误的地方指出纠正。

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

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

相关文章

【upload]-ini-[SUCTF 2019]CheckIn-笔记

上传图片木马文件后看到&#xff0c;检查的文件内容&#xff0c;包含<? 一句话木马提示 检查的文件格式 用如下图片木马&#xff0c;加上GIF89a绕过图片和<?检查 GIF89a <script languagephp>eval($_POST[cmd])</script> .user.ini实际上就是一个可以由用…

交换机VLAN配置中Tagged与Untagged端口的差异和应用区别

VLAN&#xff08;虚拟局域网&#xff09;是一种将局域网设备从逻辑上划分为不同虚拟工作组的技术。它打破了传统局域网在物理位置上的限制&#xff0c;允许网络管理员根据功能、部门或安全需求等因素&#xff0c;将同一物理网络中的设备划分到不同的逻辑网络中。每个VLAN都像一…

使用Copilot辅助编程:我如何减少加班并提高工作效率

当我聘用了一个高级工程师给我写代码&#xff0c;我再也不加班了&#xff01; 很多医生朋友说写代码很难&#xff0c;学不会python。在这个AI时代&#xff0c;作为智慧的顶尖人类&#xff0c;你还在百度搜代码真的是out了。 学会站在巨人的肩膀上&#xff0c;让AI替你搬砖&am…

工业互联网边缘计算实训室解决方案

一、引言 随着物联网&#xff08;IoT&#xff09;、5G通信技术的快速发展&#xff0c;工业互联网已成为推动制造业转型升级的重要力量。边缘计算作为云计算的延伸和补充&#xff0c;在实时数据分析、降低数据传输延迟、提升处理效率及增强数据安全性方面展现出巨大潜力。在此背…

C语言——查漏补缺

前言 本篇博客主要记录一些C语言的遗漏点&#xff0c;完成查漏补缺的工作&#xff0c;如果读者感兴趣&#xff0c;可以看看下面的内容。都是一些小点&#xff0c;下面进入正文部分。 1. 字符汇聚 编写代码&#xff0c;演示多个字符从两端移动&#xff0c;向中间汇聚 #inclu…

无人机可以用来追黄蜂吗?

哈哈&#xff0c;这个问题真是挺有趣的&#xff01;不过&#xff0c;从实际应用和安全性角度来考虑&#xff0c;使用无人机来追黄蜂可能并不是一个好主意。 首先&#xff0c;黄蜂通常对突然出现的移动物体非常敏感&#xff0c;尤其是像无人机这样的“不明飞行物”。如果无人机…

【网站项目】SpringBoot679牙科诊所管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Qt 窗口:对话框详解

目录 对话框 1. 对话框的基本概念 2. 对话框的内存释放问题 3. 自定义对话框界面 3.1 使用纯代码的方式定义 3.2 使用图形化的方式定义 4. 对话框的分类 4.1 模态对话框 4.2 非模态对话框 4.3 混合属性对话框 5. Qt 内置对话框 5.1 消息对话框 QMessageBox 示例1&…

生活生鲜超市小程序系统开发方案

生活生鲜超市小程序系统是集商品浏览、在线下单、支付结算、物流配送、会员管理等功能于一体。是为了满足现代消费者对新鲜食品购买的便利性与即时性需求。 适用于&#xff1a;生鲜超市、百货、连锁、水果、批发、便民、果蔬、食品、食材、鲜果、特产、海鲜等店铺。一、目标用户…

IP基础知识以及IP地址分类(A类 B类 C类 D类 E类)

IP地址是什么&#xff1f; IP 地址是互联网协议特有的一种地址&#xff0c;它是 IP 协议提供的一种统一的地址格式&#xff0c;为互联网上的每一个网络和每一台主机分配一个逻辑地址&#xff0c;以此来屏蔽物理地址的差异。 MAC和IP 在⽹络数据包传输中&#xff0c;源IP地址…

蜂鸣器(51单片机)

一、蜂鸣器介绍 1.蜂鸣器 2.蜂鸣器电路 3.芯片图示 二、蜂鸣器功能实现 1.蜂鸣器提示音代码 蜂鸣器函数 播放提示音功能实现 2.蜂鸣器播放音乐

Scrapy框架进行数据采集详细实现

摘要 本项目是python课程的课程项目&#xff0c;在简要学习完python和爬虫相关的Scrapy框架后&#xff0c;基于这两者的运用最终完成了对于北京链家网站新房页面的信息进行爬取&#xff0c;并将爬取的数据存放于excel之中&#xff0c;可使用excel或者wps进行查看。 1 引言 1…

论文分享 | Fuzz4All: 基于大语言模型的通用模糊测试

大语言模型是当前最受关注的研究热点&#xff0c;基于其生成和理解能力&#xff0c;对现有领域在提升性能和效果上做更多尝试。分享一篇发表于2024年ICSE会议的论文Fuzz4All&#xff0c;它组合多个大语言模型以非常轻量且黑盒的方式&#xff0c;实现了一种跨语言和软件的通用模…

【数学分析笔记】第1章第2节:映射与函数(2)

1. 集合与映射 1.12 函数&#xff08;实函数&#xff09; 函数是映射的一种特殊情况&#xff0c; f : X ⟶ Y f:\textbf{X}\longrightarrow \textbf{Y} f:X⟶Y x ⟼ y f ( x ) x\longmapsto yf(x) x⟼yf(x) 如果 X ⊂ R , Y R \textbf{X}\subset\mathbb{R},\textbf{Y}\ma…

OpenCV的Hello World

按照前文的步骤&#xff0c;我们已经在Windows机器上把OpenCV源代码编译成了DLL。接下来的问题自然是&#xff0c;我们怎么在自己的项目中使用OpenCV&#xff1f;我们将从零开始编写第一个OpenCV “Hello World”程序。通过本文的练习&#xff0c;大家将掌握&#xff1a; 在自…

你真的懂什么是串口吗?

你真的懂什么是串口吗&#xff1f; 文章目录 你真的懂什么是串口吗&#xff1f;问题什么是串口概念4种电平及其特性串口与电脑USB口通信 通讯分类常见的串行通信接口通信方式分类串行通信并行通信 传输方向分类同步方式分类PS&#xff1a;波特率和比特率 RS232 和 RS485RS232 引…

Django 中显示图片

在 Django 中显示图片的基本步骤包括&#xff1a;配置静态文件和媒体文件的处理、上传图片、以及在模板中显示图片。以下是详细步骤&#xff1a; 问题背景&#xff1a; 我在学习 Django 并在构建一个简单的网站&#xff0c;我尝试使用模板语言添加一些图片&#xff0c;但显示的…

升级阿里云linux服务器上的php版本

查看已安装的php软件包 [rootiZbp13pl2v34qj0thwq9aiZ ~]# rpm -qa|grep php php74-php-common-7.4.26-1.el7.remi.x86_64 oniguruma5php-6.9.7.1-1.el7.remi.x86_64 php74-php-gd-7.4.26-1.el7.remi.x86_64 php74-php-opcache-7.4.26-1.el7.remi.x86_64 php74-php-json-7.4.2…

阿里云Elasticsearch 企业级AI搜索方案发布

在AI技术日新月异的今天&#xff0c;尤其是大语言模型的兴起&#xff0c;企业智能化场景的解决方案正经历一场前所未有的革新。然而&#xff0c;大模型在实际应用中面临的挑战不容小觑&#xff0c;如何高效、精准地服务于企业的个性化需求成为关键。阿里云搜索产品团队通过阿里…

QT文件的操作

QT文件的操作 学习QT的文件操作类:Qfile、QTextStream。利用QtCreator工具实现文件的操作过程。通过文件读取样式表文件,动态的改变控件的外观。QFile提供了操作文件的常用功能。是一种IO设备,可以用来读写文本文件和二进制文件,也可以用来读写QT的资源文件。QFile类可以单…