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 滑动时间窗口源码分析
前言: Sentinel 的一个重要功能就是限流,对于限流来说有多种的限流算法,比如滑动时间窗口算法、漏桶算法、令牌桶算法等,Sentinel 对这几种算法都有具体的实现,如果我们对某一个资源设置了一个流控规则,并…...
猎码安卓APP开发IDE,amix STUDIO中文java,HTML5开发工具
【无爱也能发电】Xili 2024/8/2 10:41:20 猎码安卓APP开发IDE,amix java开发工具 我研发这些只有一小部分理由是为了赚钱,更多是想成就牛逼的技术产品。 目前的产品就够我赚钱的,我持续更新就好了,没必要继续研究。 IDE不赚钱,谁…...
【Deep-ML系列】Linear Regression Using Gradient Descent(手写梯度下降)
题目链接:Deep-ML 这道题主要是要考虑矩阵乘法的维度,保证维度正确,就可以获得最终的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各种架构代号: NVIDIA GPU 有多个代号和架构,这些架构对应不同的世代和硬件特性。以下是 NVIDIA 主要 GPU 架构及其计算能力(Compute Capability)代号的简要概述: Tesla 架构 G80、GT200 Compute Capabi…...
企业研发设计协同解决方案
新迪三维设计,20年深耕三维CAD 全球工业软件研发不可小觑的中国力量 2003-2014 年 新迪数字先后成为达 索SolidWorks、 ANSYS Spaceclaim、MSC等三维CAD/CAE 软件厂商的中国研发中心,深度参与国际 一流工业软件的研发过程,积累了丰富的 技术经…...
iOS 18(macOS 15)Vision 中新增的任意图片智能评分功能试玩
概述 在 WWDC 24 中库克“大厨”除了为 iOS 18 等平台重磅新增了 Apple Intelligence 以外,苹果也利用愈发成熟的机器学习引擎扩展了诸多内置框架,其中就包括 Vision。 想用本机人工智能自动为我们心仪的图片打一个“观赏分”吗?“如意如意&…...
如何实现若干子任务一损俱损--浅谈errgroup
errgroup 是 Go 语言官方扩展库 x/sync 中的一个包,它提供了一种方式来并行运行多个 goroutine,并在所有 goroutine 都完成时返回第一个发生的错误(如果有的话)。这对于需要并行处理多个任务并等待它们全部完成,同时需…...
并查集的基础题
## 洛谷p1196 绿 35m 点到祖先的距离 代码: #include<bits/stdc.h> using namespace std; const int N3e510; int f[N],dist[N],num[N];//num计算祖先有多少儿子 ,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 摘要: Android 应用程序已成为黑客攻击的主要目标。安卓恶意软件检测是一项关键技术,对保障网络安全和阻止异常情况至关重要。…...
HTTPS链接建立的过程
HTTPS(HyperText Transfer Protocol Secure)建立链接的过程主要是通过TLS(Transport Layer Security)协议来实现的。HTTPS的链接建立过程可以分为以下几个步骤: 1. **客户端发起请求** - 客户端向服务器发送一个请求&…...
文档控件DevExpress Office File API v24.1 - 支持基于Unix系统的打印
DevExpress Office File API是一个专为C#, VB.NET 和 ASP.NET等开发人员提供的非可视化.NET库。有了这个库,不用安装Microsoft Office,就可以完全自动处理Excel、Word等文档。开发人员使用一个非常易于操作的API就可以生成XLS, XLSx, DOC, DOCx, RTF, CS…...
IP地址封装类(InetAddress类)
文章目录 前言一、IP地址是什么?二、IP地址封装类 1.常用方法2.实操展示总结 前言 当我们想要获取到通信对方的IP地址、主机地址等信息时,我们可以使用InetAddress类。InetAddress类在java的net包中。 一、IP地址是什么? IP地址 (Internet Pr…...
数据库设计规范化
在数据库设计中,尤其是在关系型数据库管理系统中,规范化(Normalization)是一种通过减少数据冗余和依赖关系来优化数据库表结构的过程。规范化可以确保数据的完整性和减少数据更新时的问题。规范化的过程通常遵循一系列标准或范式&…...
预约咨询小程序搭建教程,源码获取,从0到1完成开发并部署上线
目录 一、明确需求与规划功能 二、选择开发工具与模板 三、编辑小程序内容 四、发布与运营 五、部分代码展示 制作一个预约咨询小程序,主要可以分为以下几个步骤: 一、明确需求与规划功能 明确需求: 1.确定小程序的服务对象…...
leetcode217. 存在重复元素,哈希表秒解
leetcode217. 存在重复元素 给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。 示例 1: 输入:nums [1,2,3,1] 输出:true 示例 2&#x…...
QT:QString 支持 UTF-8 编码吗?
在 Qt 中,字符串的处理主要依赖于 QString 类。QString 内部并不是直接使用 UTF-8 编码来存储数据的。相反,QString 使用 Unicode(特别是 UTF-16)来存储文本,以支持多语言环境的国际化应用。这种设计使得 QString 能够…...
我主编的电子技术实验手册(13)——电磁元件之继电器
本专栏是笔者主编教材(图0所示)的电子版,依托简易的元器件和仪表安排了30多个实验,主要面向经费不太充足的中高职院校。每个实验都安排了必不可少的【预习知识】,精心设计的【实验步骤】,全面丰富的【思考习…...
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数据库中主要分为以下几种类型: 范围分区(Range Partitioning)列表分区(List Partitioning)哈希分区(Hash Partitioning)组合分区(Composite Partitioning࿰…...
大黄蜂能飞的起来吗?
Bumblebee argument 虽然早期的空气动力学证明大黄蜂不能飞行——因为体重太重,翅膀太薄,但大黄蜂并不知道,所以照飞不误。 背景 在20世纪初,科学家们通过研究发现,大黄蜂的身体与翼展的比例失调,按照…...
【JVM】- 内存结构
引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案
JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停 1. 安全点(Safepoint)阻塞 现象:JVM暂停但无GC日志,日志显示No GCs detected。原因:JVM等待所有线程进入安全点(如…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
宇树科技,改名了!
提到国内具身智能和机器人领域的代表企业,那宇树科技(Unitree)必须名列其榜。 最近,宇树科技的一项新变动消息在业界引发了不少关注和讨论,即: 宇树向其合作伙伴发布了一封公司名称变更函称,因…...
BLEU评分:机器翻译质量评估的黄金标准
BLEU评分:机器翻译质量评估的黄金标准 1. 引言 在自然语言处理(NLP)领域,衡量一个机器翻译模型的性能至关重要。BLEU (Bilingual Evaluation Understudy) 作为一种自动化评估指标,自2002年由IBM的Kishore Papineni等人提出以来,…...
【Kafka】Kafka从入门到实战:构建高吞吐量分布式消息系统
Kafka从入门到实战:构建高吞吐量分布式消息系统 一、Kafka概述 Apache Kafka是一个分布式流处理平台,最初由LinkedIn开发,后成为Apache顶级项目。它被设计用于高吞吐量、低延迟的消息处理,能够处理来自多个生产者的海量数据,并将这些数据实时传递给消费者。 Kafka核心特…...
CMS内容管理系统的设计与实现:多站点模式的实现
在一套内容管理系统中,其实有很多站点,比如企业门户网站,产品手册,知识帮助手册等,因此会需要多个站点,甚至PC、mobile、ipad各有一个站点。 每个站点关联的有站点所在目录及所属的域名。 一、站点表设计…...
