news 2026/10/22 17:08:00

深入解析Sentinel滑动窗口LeapArray源码与参数调优实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析Sentinel滑动窗口LeapArray源码与参数调优实践

如果不是因为线上一次离奇的限流误伤,我可能到现在都不会去翻 Sentinel 底层的 LeapArray 源码。那次事故的表现很典型:规则明明设置了每秒 100 QPS,流量平稳时一点问题没有,但只要一出现突发小高峰,就会有大量本不该被拦截的请求被拦截,而且现象只在某些时间点出现,用固定窗口的思路怎么解释都解释不通。后来把源码一行行捋完才发现,问题的答案根本不在于限流算法本身,而在于最底下那个负责统计的滑动窗口数组——LeapArray。

这篇文章我会从使用者的角度,把 LeapArray 的类结构、核心方法、数据存取逻辑完整拆一遍,同时结合我自己改参数跑压力测试的实测结果,聊聊 sampleCount 和 intervalInMs 到底应该怎么配。如果你是做 Sentinel 二次开发,或者想在自己的框架里写一个类似的滑动窗口计数器,这篇文章应该能帮你省不少时间。

1. 拆源码之前先定位:LeapArray 在限流链路里是什么角色

1.1 一次请求进来后,Sentinel 做了什么

先不急着看代码,我们得搞清楚 LeapArray 在 Sentinel 整个限流链路里到底站在什么位置。Sentinel 的核心是一个责任链,每个请求进来之后会依次经过一堆 Slot,比如 FlowSlot 负责流量控制,DegradeSlot 负责熔断降级,ParamFlowSlot 负责热点参数限流。这些 Slot 看起来各管一摊,但它们判断“放行还是拒绝”的时候,靠的全是同一个东西——当前时间窗口内的统计指标。

这些指标从哪里来?从每个资源对应的节点来,节点的实现是 StatisticNode。StatisticNode 内部维护了两个最重要的滚动计数器,一个是秒级统计,一个是分钟级统计,代码大概是这样的:

private transient volatile Metric rollingCounterInSecond = new ArrayMetric(2, 1000); private transient volatile Metric rollingCounterInMinute = new ArrayMetric(60, 60000);

ArrayMetric的内部又包了一个LeapArray。看到这里你就明白了,所谓的 QPS、RT、异常比例、慢调用比例,这些数字最终都落在 LeapArray 身上。所以当你发现限流不准、统计值跳变诡异的时候,与其盯着规则配置怀疑人生,不如先怀疑统计源头。

1.2 LeapArray 在类层次里的位置

LeapArray 本身是一个抽象基类,名字拆开看很有意思:Leap是“跳跃、滑动”,Array是数组,合在一起就是一个“会向前滑动跳跃的环形数组”。它下面有两个比较核心的子类:

  • BucketLeapArray:用于常规的秒级/分钟级指标统计,统计桶是 MetricBucket。
  • HotLeapArray:用于热点参数统计,底层思路一致,但行为上略有差异。

数组里每个元素不是裸的统计数据,而是一个包装类WindowWrap<T>。T 就是真正的统计桶,比如 MetricBucket。WindowWrap 里装了三样东西:窗口长度、窗口起始时间、统计数据本身。

我用一个生活化的类比帮你建立直觉:LeapArray 就像一条环形传送带,传送带被等分成若干个仓位,每个仓位放一个 WindowWrap 包装盒,包装盒里才是真正的统计报表。传送带不停转,某个时间点你会站在某个仓位面前,如果你面前这个盒子已经过了时效,你不会把盒子扔掉换新的,而是直接把这个盒子里的旧报表清空,重新写上当前时间对应的新批次数据。

1.3 高并发下的读写特点

限流统计是一个典型的“读多写多”场景。每个请求进来,要先写一笔计数;每个请求判断要不要放行,又要读一次统计结果。在这种场景下,给整个数组加一把锁肯定不行,锁竞争会让限流组件本身变成系统的瓶颈。

所以 Sentinel 选择了AtomicReferenceArray<WindowWrap<T>>这个数据结构。它的好处是:底层是一个普通对象数组,但支持对数组里某个元素做原子性的 compare-and-set 操作。也就是说,并发更新某个槽位的时候,不需要锁整个数组,只需要 CAS 这一个槽位即可。后面你会看到,LeapArray 的核心方法就是靠这个 CAS 特性来保证线程安全的。

2. 先理解它为什么存在:固定窗口的临界突刺

2.1 固定窗口实现初看起来没问题

要真正看懂 LeapArray 的巧妙之处,得先看它的对立面:固定窗口统计。

假设我们要做一分钟维度限流,阈值为 6000 次。最简单粗暴的做法是维护一个计数器 count,再记录一个窗口起始时间 startTime。请求进来时:

long now = System.currentTimeMillis(); if (now - startTime >= 60 * 1000) { startTime = now; count = 0; } count++; if (count > 6000) { // 拒绝 }

逻辑很清晰,很多初级限流方案就是这么写的。它的问题在哪里?在窗口边界。

2.2 边界上的“两倍流量黑洞”

我们用一个具体的例子来说明。窗口 A 是第 0 秒到第 60 秒,窗口 B 是第 60 秒到第 120 秒。阈值是每分钟 6000 次,换算一下大概是每秒 100 次。

如果前 59 秒里几乎没什么流量,只来了 1000 个请求,那么窗口 A 还剩下 5000 次的配额。在最后 1 秒,突然涌入 5000 个请求,窗口 A 会全部放行。紧接着第 60 秒,新的窗口 B 开始了,计数清零,又可以放行 6000 个。那么在第 59 秒到第 61 秒这 2 秒内,系统实际可能承受了近 12000 次请求,远超阈值。

这种问题有个专门的名字叫“临界突刺”,它并不是偶发情况,只要流量存在毛刺,在固定窗口下就一定会出现。你可能觉得每秒 100 次限流不算什么,但如果是抢购、秒杀、接口抖动这种场景,这 2 秒的高峰就足以把下游打挂。

2.3 滑动窗口的朴素解决思路

滑动窗口的思路是:不要用一个 60 秒的大窗口,而是把 1 分钟切成 60 个小窗口,每个小窗口 1 秒。统计时不再看单一窗口的累计值,而是把最近 60 个小窗口的值全部加起来,作为当前这一分钟的流量。

这样在窗口边界发生问题时,旧的 1 秒小窗口会一个一个地滑出去,而不是整个窗口清零重建。流量最多只会在 1 秒粒度上抖动,不会出现 2 秒内翻倍的情况。

LeapArray 做的事,本质就是把“最近 intervalInMs 毫秒”这个滑动窗口拆成 sampleCount 个小桶,然后用一个环形数组高效地管理它们。

3. 类骨架逐字段拆解:LeapArray 的定义、构造与 WindowWrap

3.1 三个时间参数

先看 LeapArray 的核心字段和构造方法:

public abstract class LeapArray<T> { protected int windowLengthInMs; // 每个小窗口的时间跨度 protected int sampleCount; // 小窗口个数,也就是数组长度 protected int intervalInMs; // 整个滑动窗口的总跨度 protected final AtomicReferenceArray<WindowWrap<T>> array; public LeapArray(int sampleCount, int intervalInMs) { this.windowLengthInMs = intervalInMs / sampleCount; this.intervalInMs = intervalInMs; this.sampleCount = sampleCount; this.array = new AtomicReferenceArray<>(sampleCount); } }

注意三者的关系:windowLengthInMs = intervalInMs / sampleCount。也就是说,总窗口跨度被均分成了 sampleCount 份,每一份是一个独立的统计桶。这个除法要求能够整除,否则最后一个窗口会同其他窗口长度不一致,导致统计偏差,后面我会专门提到这个坑。

array.length()就是 sampleCount。数组下标是固定的,但数组元素(WindowWrap)里记录的 windowStart 是随着时间不断变化的,所以它才能“复用”。

3.2 WindowWrap:窗口的包装盒

WindowWrap 的代码非常简洁:

public class WindowWrap<T> { private final long windowLengthInMs; // 窗口长度 private long windowStart; // 窗口起始时间戳 private T value; // 统计桶 public WindowWrap(long windowLengthInMs, long windowStart, T value) { this.windowLengthInMs = windowLengthInMs; this.windowStart = windowStart; this.value = value; } public long windowStart() { return windowStart; } public T value() { return value; } public void setValue(T value) { this.value = value; } public void resetTo(long startTime) { this.windowStart = startTime; } }

这里有一个关键点:为什么 windowLengthInMs 用final,而 windowStart 是可变的?因为一个数组槽位对应的窗口长度是永远不变的,变的是“这个槽位当前代表哪个时间段”。随着时间推进,同一个数组槽位会被反复重置给新的时间段,这就是滑动窗口的核心机制。真正会变的是 windowStart,它在每次窗口过期时被重置到新的时间点。

3.3 两个抽象方法:新桶与重置

LeapArray 定义了两个抽象方法,留给子类去实现:

public abstract T newEmptyBucket(long timeMillis); protected abstract WindowWrap<T> resetWindowStart(WindowWrap<T> windowWrap, long startTime);
  • newEmptyBucket:创建一个全新的统计桶。对于 BucketLeapArray 来说,就是 new 一个 MetricBucket。
  • resetWindowStart:当数组槽位上的旧窗口已经过期时,把这个 WindowWrap 重置到新的时间点。

BucketLeapArray 的实现大概是这样的:

public class BucketLeapArray extends LeapArray<MetricBucket> { public BucketLeapArray(int sampleCount, int intervalInMs) { super(sampleCount, intervalInMs); } @Override public MetricBucket newEmptyBucket(long timeMillis) { return new MetricBucket(); } @Override protected WindowWrap<MetricBucket> resetWindowStart(WindowWrap<MetricBucket> w, long startTime) { w.resetTo(startTime); w.value().reset(); return w; } }

注意这里的设计取舍:窗口过期时,它没有扔掉旧的 WindowWrap,也没有创建新的 WindowWrap,而是直接复用对象,只改 windowStart,并且把旧统计桶里的数据清空。这样带来的最大好处是大幅减少对象创建频率。限流场景下每秒可能有成千上万个请求在读写统计桶,如果每次都 new 对象,GC 压力会非常难看。我见过一些自研限流组件在高并发下频繁 Full GC,查到最后就是这种小对象的分配问题。

4. currentWindow() 核心方法拆解:时间戳到桶的下标映射与桶复用

4.1 两个辅助计算:时间戳如何变成数组下标

要拿到“当前时间对应的窗口”,需要先做两个计算:

private int calculateTimeIdx(long timeMillis) { long timeId = timeMillis / windowLengthInMs; return (int) (timeId % array.length()); } protected long calculateWindowStart(long timeMillis) { return timeMillis - timeMillis % windowLengthInMs; }

这里面的逻辑非常精妙。timeMillis / windowLengthInMs可以得到“从某个基准时刻到现在,一共经历了多少个完整的小窗口”,这个数字记为 timeId。然后timeId % array.length()得到一个固定范围内的数组下标。因为 timeId 会随着时间递增,所以取模的结果会在 0 到 sampleCount-1 之间循环,这就是“环形数组”的由来。

calculateWindowStart的作用是把任意一个时间戳“对齐”到它所属窗口的起始时间。比如 windowLengthInMs 是 500ms,当前时间戳是 1630000000123ms,那么窗口起始时间就是 1630000000000ms,也就是把时间戳向下取整到 500 的整倍数。

4.2 currentWindow() 的完整逻辑

核心方法来了,这段代码是整个 LeapArray 的心脏:

public WindowWrap<T> currentWindow(long timeMillis) { if (timeMillis < 0) { return null; } int idx = calculateTimeIdx(timeMillis); long windowStart = calculateWindowStart(timeMillis); while (true) { WindowWrap<T> old = array.get(idx); if (old == null) { // 槽位还是空的,创建新窗口并 CAS 放入 WindowWrap<T> window = new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); if (array.compareAndSet(idx, null, window)) { return window; } else { Thread.yield(); } } else if (windowStart == old.windowStart()) { // 当前时间正好落在 old 这个窗口内,直接复用 return old; } else if (windowStart > old.windowStart()) { // 槽位上是一个已经过期的窗口,重置它 if (resetWindowStart(old, windowStart)) { return old; } else { Thread.yield(); } } else { // 理论上是时钟回拨或者异常情况,创建一个临时窗口返回 return new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); } } }

这段代码虽然短,但每一个分支都有它的存在理由。

4.3 四种分支逐一分析

第一种情况:old == null。这个槽位从来没有被写入过,说明当前时间点前面的窗口还没人碰过这个位置。此时创建一个新的 WindowWrap,然后用compareAndSet(idx, null, window)尝试放入。CAS 成功说明没有竞争,直接返回;CAS 失败说明有另一个线程抢先创建了,那么当前线程就让出 CPU,重新进入循环再读一次。

第二种情况:windowStart == old.windowStart()。这是最常见的情况,说明当前时间戳和这个槽位里记录的时间段正好匹配。比如 10:00:00.100 和 10:00:00.400 都属于同一个 500ms 窗口,所以直接返回 old 即可,所有并发请求都会拿到同一个统计桶,往里面累加数据。

第三种情况:windowStart > old.windowStart()。说明槽位里存的是一个“过去”的窗口,它已经不在当前滑动窗口范围内了。此时需要把它重置到新的时间点,然后返回 old。这里之所以能复用对象,就是因为它避开了频繁创建新对象的开销。但并发下可能有两个线程同时发现这个窗口过期,都去执行重置,于是 CAS 或某种同步机制会保证只有一个线程重置成功,失败者自旋重试。

第四种情况:windowStart < old.windowStart()。槽位里的窗口起始时间比当前计算的窗口起始时间还要晚?这只能说明发生了时钟回拨,或者某种极端异常。此时反正拿不到正确的窗口,索性直接 new 一个临时 WindowWrap 返回,保证当前请求能有一个桶可以用,同时不污染数组里的已有数据。

4.4 为什么非要 while(true) 自旋

一开始我觉得这个自旋有点“浪费 CPU”,后来在压测环境下才真正体会它的价值。在高并发下,多个线程可能同时发现同一个槽位过期,同时尝试重置。如果重置操作本身是 CAS 实现的,那么只有一个线程能成功,其他线程必须重新读取数组。一旦成功线程返回后,其他线程再读数组,就会发现 windowStart 已经更新,进入第二种情况直接复用窗口,整个过程就收敛了。

如果没有这个自旋,失败线程直接返回 null 或者创建一个新窗口,就会出现某个请求拿到的统计桶不是当前的正式桶,数据写到临时对象里丢了,限流统计就会出现莫名其妙的误差。所以这个 while(true) 不是性能浪费,而是正确性的兜底。

5. 统计侧源码:getPreviousWindow、values() 和上层聚合

5.1 getPreviousWindow:上一个窗口怎么找

currentWindow 是拿“当前窗口”,但有些场景需要参考“上一个窗口”的数据,比如慢调用比例、异常比例等,需要对比当前窗口和上一个窗口的差值来判断趋势。

public WindowWrap<T> getPreviousWindow(long timeMillis) { if (timeMillis < 0) { return null; } int idx = calculateTimeIdx(timeMillis - windowLengthInMs); long windowStart = calculateWindowStart(timeMillis - windowLengthInMs); while (true) { WindowWrap<T> previousWindow = array.get(idx); if (previousWindow == null) { return null; } if (windowStart == previousWindow.windowStart()) { return previousWindow; } if (windowStart > previousWindow.windowStart()) { return null; } return null; } }

它的逻辑很直观:先把时间往前拨一个窗口长度,再计算对应的数组下标和窗口起始时间,然后去数组里找。如果找到的窗口起始时间和预期的 windowStart 一致,说明确实存在一个完整的上一窗口;如果那个槽位还是空的,或者已经是更老的窗口,就返回 null。

有一个容易忽略的细节:这个方法访问数组时没有写操作,所以在并发下读到不一致的数据是可能的。但因为统计本身是近似的,上一窗口少统计一点或多统计一点,在工程上是可以接受的,Sentinel 就是这么权衡的。

5.2 values():只把“活着”的窗口捞出来

上层聚合统计时,最常用的方法是 values(),它用来获取当前时刻所有“还没有结束”的窗口:

public List<T> values(long timeMillis) { if (timeMillis < 0) { return new ArrayList<>(); } int size = array.length(); List<T> result = new ArrayList<>(size); for (int i = 0; i < size; i++) { WindowWrap<T> windowWrap = array.get(i); if (windowWrap == null) { continue; } if (timeMillis < windowWrap.windowStart() + windowLengthInMs) { result.add(windowWrap.value()); } } return result; }

关键在于这个判断:timeMillis < windowWrap.windowStart() + windowLengthInMs。它检查的是这个窗口的结束时间是否在当前时间之后。如果窗口的结束时间已经小于等于当前时间,说明这个窗口已经彻底滑出统计区间了,虽然它还物理存在于数组里,但不能再参与当前统计。

这就是滑动窗口“滑动”的另一面:不是靠后台线程去清理旧窗口,而是在读取时用时间戳自然过滤。懒加载、懒清理,这个思路在 Sentinel 里用得很彻底。

5.3 上层怎么聚合出 QPS

以秒级统计为例,StatisticNode 里的 QPS 计算过程大致是把 values() 返回的所有 MetricBucket 里的 pass 计数加起来。我用简化代码表示:

public long passQps() { long pass = 0; List<MetricBucket> buckets = rollingCounterInSecond.values(); for (MetricBucket bucket : buckets) { pass += bucket.pass(); } return pass; }

这里的聚合必须用“遍历所有小窗口累加”,而不是只取当前窗口的值再乘一个倍数。因为最近 1 秒这个滑动窗口里,可能包含部分上个周期的窗口和部分当前窗口,只有全部取出来相加,才能得到真正的“最近 1 秒”统计值。

这里也顺带解释了为什么限流的 QPS 数值总是有轻微波动:因为它是多个小窗口的瞬时累加,窗口之间天然存在切换的瞬间。理解了这个,你就不会因为看到 QPS 显示 98、99、101 而觉得系统出 bug 了。

6. sampleCount 和 intervalInMs 的选型,以及我踩过的坑

6.1 默认参数对照

Sentinel 里默认的统计配置是这样的:

使用场景sampleCountintervalInMs单个窗口长度
秒级统计21000500ms
分钟级统计60600001000ms

秒级统计用 2 个 500ms 的小窗口,分钟级统计用 60 个 1s 的小窗口。这个设计是在“精度”和“内存/CPU开销”之间取的平衡。你如果自己配置,需要清楚调整任何一个参数带来的连锁反应。

6.2 sampleCount 大小如何取舍

我实测过一组数据:同一个限流规则,阈值 100 QPS,突发流量用脚本每 200ms 打进一批请求,对比 sampleCount 为 2、10、60 时的通过情况。

sampleCount单窗口长度实际表现
2500ms窗口边界处有明显抖动,突发时偶尔多放行十几二十个请求
10100ms平滑很多,限流曲线更贴合阈值
60约16.7ms统计很精细,但每个桶的计数周期太短,内存和遍历开销上升明显

简单来说,sampleCount 太小,滑动窗口的“滑动粒度”太粗,临界突刺虽然比固定窗口好很多,但还是会有毛刺;sampleCount 太大,每个小窗口的时间太短,很容易出现某个窗口只有一两个请求甚至没有请求,统计的离散性反而变大,同时遍历和内存成本也上去了。

如果问我怎么选,我的建议是:默认配置能满足绝大多数场景,不用动。如果你确实对精度有要求,比如希望限流反应更快,可以调大 sampleCount,但不要超过 60。超过之后收益基本下降,成本反而明显上升。

6.3 踩坑:被重置的旧窗口返回瞬间统计为 0

这是我实际遇到的一个问题。某个接口用 Sentinel 做慢调用比例熔断,阈值设的是“RT 超过 200ms 的比例达到 30% 就熔断”。上线之后,熔断偶尔会误触发,查监控发现触发瞬间的慢调用比例高达 40% 以上,但事后看请求的 p99 明明在 150ms 左右。

后来定位到原因:慢调用比例的统计依赖秒级窗口,而秒级窗口只有 2 个 500ms 的桶。当某个桶刚好被重置到新的时间窗口时,这个桶里的统计数据是空的(0),而另一个桶里保留的是上一个窗口的累积数据。此时如果恰好有慢请求打进来,新的桶里瞬间累积了较高的慢调用比例,两桶相加之后,比例数值就被短暂放大,触发了熔断。

这种问题不算 LeapArray 的 bug,而是“统计窗口很粗 + 桶复用机制”带来的瞬时失真。解决思路是:在判断比例类指标时,对刚重置的窗口做一定的忽略或预热,或者把 sampleCount 调大一些,让单个桶的影响变小。

6.4 排查统计不准的三个方向

如果你也遇到限流统计和真实流量对不上,我建议按这个顺序排查:

第一,检查 intervalInMs 是否能被 sampleCount 整除。如果不能整除,最后一个窗口的长度会和前面的不一致,导致 values() 聚合出来的时间跨度不是预期值。这个属于配置错误,最隐蔽。

第二,检查系统时钟是否发生过跃变。LeapArray 的所有计算都依赖时间戳,时钟往前跳一次,可能导致 windowStart 计算出来的值反而小于 old.windowStart(),触发第四种分支,产生临时桶;时钟往后退一次,则可能出现窗口长期不切换,统计值叠加。

第三,检查你是不是在多个线程里手动修改了 WindowWrap 的 windowStart 或 value。LeapArray 的内部机制保证了一个槽位在并发下只有一个所有者,但你如果绕过 currentWindow() 直接操作数组,就会破坏这个约束。我自己调试时干过这种事,最后统计乱得没法看,折腾了一下午才找到原因。

6.5 给自己项目写类似组件的一点启发

读完 LeapArray,我最想强调的是“复用”这两个字。现在很多做技术的人一遇到性能问题就上缓存、加锁、换数据库,但 Sentinel 给我们展示了一个非常经典的思路:在高频读写场景里,用环形数组 + 时间戳取模 + CAS 复用对象,可以做到无锁、无频繁对象创建,同时保持统计精度。

如果你的项目也需要一个轻量滑动窗口计数器,比如接口 QPS 统计、登录限流、验证码发送频率控制,完全可以参考这个设计。用 Java 的话可以直接抄 AtomicReferenceArray 的写法;用 Go 的话,可以用 atomic 包里的 CompareAndSwapPointer 做类似的事情。数据结构本身是跨语言通用的。

最后再分享一个我自己养成的习惯:拿到一个开源限流框架,先别忙着配规则,先花半小时把它最底层的统计结构读一遍。因为所有规则、算法、配置,最终都是建立在统计数据之上的。数据结构的边界在哪里,整个系统的行为边界就在哪里。LeapArray 虽然代码不长,但它把“环形数组、时间窗口、CAS、对象复用”这四件事揉在一起,读透之后,以后再看任何限流、熔断、监控组件,都会有一种豁然开朗的感觉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 9:06:44

T恤DIY定制系统开发指南:从设计画布到印刷文件的完整链路

简介&#xff1a;面向手机端T恤在线定制场景&#xff0c;这套基于HTML5 Canvas的DIY设计系统源码&#xff0c;适合前端开发者、交互设计师及电商平台从业者学习参考。压缩包大小14.19MB&#xff0c;内部主要包含入口HTML页面、图片素材、JavaScript交互脚本、字体库及CSS样式表…

作者头像 李华
网站建设 2026/10/21 11:02:36

校企合作实训管理系统开发实战:从选题设计到SpringBoot落地

做计算机毕业设计选系统类题目的同学&#xff0c;十有八九都经历过这样的焦虑&#xff1a;题目拿到手框架没头绪&#xff0c;代码写了一半发现需求理解偏了&#xff0c;最后勉强写完又担心答辩过不了。校企合作的大学生实训管理系统这个题&#xff0c;这几年在高校里出现频率相…

作者头像 李华
网站建设 2026/10/22 13:13:07

HIS系统源码包落地全攻略:从解压、配置到二次开发

简介&#xff1a;这是一份面向医疗信息化前端开发学习者的HIS系统源码包&#xff0c;适合想了解医院信息系统登录注册、挂号、病历、药方等核心页面如何实现的开发者。压缩包共42个文件&#xff0c;约736KB&#xff0c;以9个html页面、8个js脚本、6个css样式为主&#xff0c;并…

作者头像 李华
网站建设 2026/10/21 11:09:43

利尔达RF Lora模块SX1276驱动与通信实战指南

简介&#xff1a;这份利尔达RF Lora资料面向物联网开发者与嵌入式工程师&#xff0c;聚焦基于SX1276芯片的Lora通信模块&#xff0c;帮助读者理解扩频调制原理、模块配置与收发程序实现&#xff0c;适用于智慧城市、智能农业、物流追踪等远距离低功耗场景&#xff0c;初学者与有…

作者头像 李华
网站建设 2026/10/22 15:23:24

ElGamal加密算法详解:从离散对数到安全性证明与工程实践

1. 为什么还要聊ElGamal&#xff1a;公钥密码里的一块活化石如果你接触过密码学&#xff0c;十有八九见过"ElGamal"这个名字。它不新&#xff0c;上世纪八十年代就提出了&#xff0c;今天很多公钥加密方案、签名方案、零知识证明协议里&#xff0c;都能看到它的影子。…

作者头像 李华