当前位置: 首页 > news >正文

Sentinel 滑动时间窗口源码分析

前言:

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.//来源节点不为空 来源节点的 线程数 和  计数器 也+1context.getCurEntry().getOriginNode().increaseThreadNum();context.getCurEntry().getOriginNode().addPassRequest(count);}//是否是入口资源类型if (resourceWrapper.getEntryType() == EntryType.IN) {// Add count for global inbound entry node for global statistics.//如果是入口资源类型 全局线程数 和 计数器 也要+1Constants.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.//来源节点不为空 来源节点的 线程数 +1context.getCurEntry().getOriginNode().increaseThreadNum();}//是否是入口资源类型if (resourceWrapper.getEntryType() == EntryType.IN) {// Add count for global inbound entry node for global statistics.//如果是入口资源类型 全局线程数 +1Constants.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) {//请求来源节点 阻塞数量 +1context.getCurEntry().getOriginNode().increaseBlockQps(count);}//是否是入口资源类型if (resourceWrapper.getEntryType() == EntryType.IN) {// Add count for global inbound entry node for global statistics.//如果是入口资源类型 全局阻塞数 +1Constants.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.//错误设置到 当前 Entrycontext.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();//时间窗口中的对应位置的值+countwrap.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 直接 returnif (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 对象 也就是创建样本时间窗口 MetricBucketWindowWrap<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 。

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

相关文章:

Sentinel 滑动时间窗口源码分析

前言&#xff1a; Sentinel 的一个重要功能就是限流&#xff0c;对于限流来说有多种的限流算法&#xff0c;比如滑动时间窗口算法、漏桶算法、令牌桶算法等&#xff0c;Sentinel 对这几种算法都有具体的实现&#xff0c;如果我们对某一个资源设置了一个流控规则&#xff0c;并…...

猎码安卓APP开发IDE,amix STUDIO中文java,HTML5开发工具

【无爱也能发电】Xili 2024/8/2 10:41:20 猎码安卓APP开发IDE,amix java开发工具 我研发这些只有一小部分理由是为了赚钱&#xff0c;更多是想成就牛逼的技术产品。 目前的产品就够我赚钱的&#xff0c;我持续更新就好了&#xff0c;没必要继续研究。 IDE不赚钱&#xff0c;谁…...

【Deep-ML系列】Linear Regression Using Gradient Descent(手写梯度下降)

题目链接&#xff1a;Deep-ML 这道题主要是要考虑矩阵乘法的维度&#xff0c;保证维度正确&#xff0c;就可以获得最终的theata import numpy as np def linear_regression_gradient_descent(X: np.ndarray, y: np.ndarray, alpha: float, iterations: int) -> np.ndarray:…...

NVIDIA A100 和 H100 硬件架构学习

目前位置NV各种架构代号&#xff1a; NVIDIA GPU 有多个代号和架构&#xff0c;这些架构对应不同的世代和硬件特性。以下是 NVIDIA 主要 GPU 架构及其计算能力&#xff08;Compute Capability&#xff09;代号的简要概述&#xff1a; Tesla 架构 G80、GT200 Compute Capabi…...

企业研发设计协同解决方案

新迪三维设计&#xff0c;20年深耕三维CAD 全球工业软件研发不可小觑的中国力量 2003-2014 年 新迪数字先后成为达 索SolidWorks、 ANSYS Spaceclaim、MSC等三维CAD/CAE 软件厂商的中国研发中心&#xff0c;深度参与国际 一流工业软件的研发过程&#xff0c;积累了丰富的 技术经…...

iOS 18(macOS 15)Vision 中新增的任意图片智能评分功能试玩

概述 在 WWDC 24 中库克“大厨”除了为 iOS 18 等平台重磅新增了 Apple Intelligence 以外&#xff0c;苹果也利用愈发成熟的机器学习引擎扩展了诸多内置框架&#xff0c;其中就包括 Vision。 想用本机人工智能自动为我们心仪的图片打一个“观赏分”吗&#xff1f;“如意如意&…...

如何实现若干子任务一损俱损--浅谈errgroup

errgroup 是 Go 语言官方扩展库 x/sync 中的一个包&#xff0c;它提供了一种方式来并行运行多个 goroutine&#xff0c;并在所有 goroutine 都完成时返回第一个发生的错误&#xff08;如果有的话&#xff09;。这对于需要并行处理多个任务并等待它们全部完成&#xff0c;同时需…...

并查集的基础题

## 洛谷p1196 绿 35m 点到祖先的距离 代码&#xff1a; #include<bits/stdc.h> using namespace std; const int N3e510; int f[N],dist[N],num[N];//num计算祖先有多少儿子 &#xff0c;dist计算距离祖先有几个 int zx(int x){ if(f[x]x)return x;//x没爸爸 e…...

[论文翻译] LTAChecker:利用注意力时态网络基于 Dalvik 操作码序列的轻量级安卓恶意软件检测

LTAChecker: Lightweight Android Malware Detection Based on Dalvik Opcode Sequences using Attention Temporal Networks 摘要&#xff1a; Android 应用程序已成为黑客攻击的主要目标。安卓恶意软件检测是一项关键技术&#xff0c;对保障网络安全和阻止异常情况至关重要。…...

HTTPS链接建立的过程

HTTPS&#xff08;HyperText Transfer Protocol Secure&#xff09;建立链接的过程主要是通过TLS&#xff08;Transport Layer Security&#xff09;协议来实现的。HTTPS的链接建立过程可以分为以下几个步骤&#xff1a; 1. **客户端发起请求** - 客户端向服务器发送一个请求&…...

文档控件DevExpress Office File API v24.1 - 支持基于Unix系统的打印

DevExpress Office File API是一个专为C#, VB.NET 和 ASP.NET等开发人员提供的非可视化.NET库。有了这个库&#xff0c;不用安装Microsoft Office&#xff0c;就可以完全自动处理Excel、Word等文档。开发人员使用一个非常易于操作的API就可以生成XLS, XLSx, DOC, DOCx, RTF, CS…...

IP地址封装类(InetAddress类)

文章目录 前言一、IP地址是什么&#xff1f;二、IP地址封装类 1.常用方法2.实操展示总结 前言 当我们想要获取到通信对方的IP地址、主机地址等信息时&#xff0c;我们可以使用InetAddress类。InetAddress类在java的net包中。 一、IP地址是什么&#xff1f; IP地址 (Internet Pr…...

数据库设计规范化

在数据库设计中&#xff0c;尤其是在关系型数据库管理系统中&#xff0c;规范化&#xff08;Normalization&#xff09;是一种通过减少数据冗余和依赖关系来优化数据库表结构的过程。规范化可以确保数据的完整性和减少数据更新时的问题。规范化的过程通常遵循一系列标准或范式&…...

预约咨询小程序搭建教程,源码获取,从0到1完成开发并部署上线

目录 一、明确需求与规划功能 二、选择开发工具与模板 三、编辑小程序内容 四、发布与运营 五、部分代码展示 制作一个预约咨询小程序&#xff0c;主要可以分为以下几个步骤&#xff1a; 一、明确需求与规划功能 明确需求&#xff1a; 1.确定小程序的服务对象&#xf…...

leetcode217. 存在重复元素,哈希表秒解

leetcode217. 存在重复元素 给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 &#xff0c;返回 true &#xff1b;如果数组中每个元素互不相同&#xff0c;返回 false 。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3,1] 输出&#xff1a;true 示例 2&#x…...

QT:QString 支持 UTF-8 编码吗?

在 Qt 中&#xff0c;字符串的处理主要依赖于 QString 类。QString 内部并不是直接使用 UTF-8 编码来存储数据的。相反&#xff0c;QString 使用 Unicode&#xff08;特别是 UTF-16&#xff09;来存储文本&#xff0c;以支持多语言环境的国际化应用。这种设计使得 QString 能够…...

我主编的电子技术实验手册(13)——电磁元件之继电器

本专栏是笔者主编教材&#xff08;图0所示&#xff09;的电子版&#xff0c;依托简易的元器件和仪表安排了30多个实验&#xff0c;主要面向经费不太充足的中高职院校。每个实验都安排了必不可少的【预习知识】&#xff0c;精心设计的【实验步骤】&#xff0c;全面丰富的【思考习…...

odoo from样式更新

.xodoo_form {.o_form_sheet {padding-bottom: 0 !important;border-style: solid !important;border-color: white;}.o_inner_group {/* 线框的样式 *//*--line-box-border: 1px solid #666;*//*box-shadow: 0 1px 0 #e6e6e6;*/margin: 0;}.grid {display: grid;gap: 0;}.row …...

Oracle(52)分区表有哪些类型?

分区表在Oracle数据库中主要分为以下几种类型&#xff1a; 范围分区&#xff08;Range Partitioning&#xff09;列表分区&#xff08;List Partitioning&#xff09;哈希分区&#xff08;Hash Partitioning&#xff09;组合分区&#xff08;Composite Partitioning&#xff0…...

大黄蜂能飞的起来吗?

Bumblebee argument 虽然早期的空气动力学证明大黄蜂不能飞行——因为体重太重&#xff0c;翅膀太薄&#xff0c;但大黄蜂并不知道&#xff0c;所以照飞不误。 背景 在20世纪初&#xff0c;‌科学家们通过研究发现&#xff0c;‌大黄蜂的身体与翼展的比例失调&#xff0c;‌按照…...

KubeSphere 容器平台高可用:环境搭建与可视化操作指南

Linux_k8s篇 欢迎来到Linux的世界&#xff0c;看笔记好好学多敲多打&#xff0c;每个人都是大神&#xff01; 题目&#xff1a;KubeSphere 容器平台高可用&#xff1a;环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

第19节 Node.js Express 框架

Express 是一个为Node.js设计的web开发框架&#xff0c;它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用&#xff0c;和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

业务系统对接大模型的基础方案:架构设计与关键步骤

业务系统对接大模型&#xff1a;架构设计与关键步骤 在当今数字化转型的浪潮中&#xff0c;大语言模型&#xff08;LLM&#xff09;已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中&#xff0c;不仅可以优化用户体验&#xff0c;还能为业务决策提供…...

CTF show Web 红包题第六弹

提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框&#xff0c;很难让人不联想到SQL注入&#xff0c;但提示都说了不是SQL注入&#xff0c;所以就不往这方面想了 ​ 先查看一下网页源码&#xff0c;发现一段JavaScript代码&#xff0c;有一个关键类ctfs…...

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...

【人工智能】神经网络的优化器optimizer(二):Adagrad自适应学习率优化器

一.自适应梯度算法Adagrad概述 Adagrad&#xff08;Adaptive Gradient Algorithm&#xff09;是一种自适应学习率的优化算法&#xff0c;由Duchi等人在2011年提出。其核心思想是针对不同参数自动调整学习率&#xff0c;适合处理稀疏数据和不同参数梯度差异较大的场景。Adagrad通…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

如何将联系人从 iPhone 转移到 Android

从 iPhone 换到 Android 手机时&#xff0c;你可能需要保留重要的数据&#xff0c;例如通讯录。好在&#xff0c;将通讯录从 iPhone 转移到 Android 手机非常简单&#xff0c;你可以从本文中学习 6 种可靠的方法&#xff0c;确保随时保持连接&#xff0c;不错过任何信息。 第 1…...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案

随着新能源汽车的快速普及&#xff0c;充电桩作为核心配套设施&#xff0c;其安全性与可靠性备受关注。然而&#xff0c;在高温、高负荷运行环境下&#xff0c;充电桩的散热问题与消防安全隐患日益凸显&#xff0c;成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...

AI,如何重构理解、匹配与决策?

AI 时代&#xff0c;我们如何理解消费&#xff1f; 作者&#xff5c;王彬 封面&#xff5c;Unplash 人们通过信息理解世界。 曾几何时&#xff0c;PC 与移动互联网重塑了人们的购物路径&#xff1a;信息变得唾手可得&#xff0c;商品决策变得高度依赖内容。 但 AI 时代的来…...