一些并发常见的问题

一.现在有A,B,C三个线程如何同时进行,在并发情况下如何依次进行,如何保证有序交替执行

三种同步工具countdownlatch,cylicBarrier,Semaphore

  • countdownlatch:类似于一个起跑线,所有来的线程到这先等待,到齐后倒计时一起跑

  • cylicBarrier:类似与一个大巴,里面有许多的座位,等到所有的人都上车以后才开始跑

  • Semaphore:信号量,类似于给线程加权,第一个需要5个信号量,第二个需要1个,第三需要3个,现在我有7个信号量的话,第一个拿走5个第二个拿走1个剩下一个第三就走不了类似于这样

1.同时进行使用countdownlatch

    import java.util.concurrent.CountDownLatch;

    public class ConcurrentStart {
        public static void main(String[] args) throws InterruptedException {
            CountDownLatch startLatch = new CountDownLatch(1);

            new Thread(() -> {
                try {
                    startLatch.await();
                    System.out.println("A开始执行");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "A").start();

            new Thread(() -> {
                try {
                    startLatch.await();
                    System.out.println("B开始执行");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "B").start();

            new Thread(() -> {
                try {
                    startLatch.await();
                    System.out.println("C开始执行");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }, "C").start();

            // 同时释放所有线程
            Thread.sleep(100);

        startLatch.countDown();
        }
    }
  1. 顺序执行(使用 volatile)
public class SequentialExecution {
    // 使用volatile保证可见性
    private volatile static int current = 1;

    public static void main(String[] args) {
        new Thread(() -> {
            while (current != 1) {} // 忙等待
            System.out.println("A");
            current = 2;
        }, "A").start();

        new Thread(() -> {
            while (current != 2) {} // 忙等待
            System.out.println("B");
            current = 3;
        }, "B").start();

        new Thread(() -> {
            while (current != 3) {} // 忙等待
            System.out.println("C");
        }, "C").start();
    }
}

3.交替执行(使用 Semaphore)

import java.util.concurrent.Semaphore;

public class AlternateExecution {
    // 初始化信号量:A有1个许可,B/C无许可
    private static Semaphore semA = new Semaphore(1);
    private static Semaphore semB = new Semaphore(0);
    private static Semaphore semC = new Semaphore(0);

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                for (int i = 0; i < 3; i++) {
                    semA.acquire();  // 获取A许可
                    System.out.print("A ");
                    semB.release();  // 释放B许可
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "A").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 3; i++) {
                    semB.acquire();  // 获取B许可
                    System.out.print("B ");
                    semC.release();  // 释放C许可
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "B").start();

        new Thread(() -> {
            try {
                for (int i = 0; i < 3; i++) {
                    semC.acquire();  // 获取C许可
                    System.out.print("C ");
                    semA.release();  // 释放A许可
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, "C").start();
    }
}

二.分布式事务的CAP原理和BASE理论

  1. 一致性:不管用户访问哪一个节点,得到的数据必须是一致的,意思就是所有的节点必须同步

  2. 可用性:用户访问分布式系统的时候总是能够成功的

  3. 分区容错性:Partition,就是分区,就是当分布式系统节点之间出现网络故障导致节点之间无法通信的情况,Tolerance,就是容错,即便是系统出现网络分区,整个系统也要持续对外提供服务。

  4. 矛盾:在分布式架构中网络不可能一直保持稳定,网络分区是必定的此时就导致一致性和可用性不可能同时满足,要做取舍

    • 允许用户任意读写,保证可用性。但由于node03无法完成同步,就会出现数据不一致的情况。满足AP

    • 不允许用户写,可以读,直到网络恢复,分区消失。这样就确保了一致性,但牺牲了可用性。满足CP

既然分布式系统要遵循CAP定理,那么问题来了,我到底是该牺牲一致性还是可用性呢?如果牺牲了一致性,出现数据不一致该怎么处理?

人们在总结系统设计经验时,最终得到了一些心得:

  • Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。

  • Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。

  • Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

以上就是BASE理论。

简单来说,BASE理论就是一种取舍的方案,不再追求完美,而是最终达成目标。因此解决分布式事务的思想也是这样,有两个方向:

  • AP思想:各个子事务分别执行和提交,无需锁定数据。允许出现结果不一致,然后采用弥补措施恢复,实现最终一致即可。例如AT模式就是如此

  • CP思想:各个子事务执行后不要提交,而是等待彼此结果,然后同时提交或回滚。在这个过程中锁定资源,不允许其它人访问,数据处于不可用状态,但能保证一致性。例如XA模式

三.事务的悬挂和空回滚

好的,我们来用通俗的比喻和场景解释一下分布式事务中的“悬挂”和“空回滚”这两个概念。

想象一下,你在网上同时买了两件东西,一个来自仓库A(比如书本),一个来自仓库B(比如玩具)。为了保证交易公平(要么都发货,要么都不发),有一个总指挥(事务协调者) 在协调这两个仓库的操作。

核心概念:TCC 模式
我们以最常用的事务模式之一 TCC (Try-Confirm-Cancel) 来举例说明,它分三个阶段:

  1. Try (尝试):总指挥分别联系仓库A和仓库B,说:“我可能要买书/玩具,你们先帮我预留一本/一个(锁定资源),看看有没有货,但先别正式发货!” 每个仓库回复:“好的,预留好了!” 或者 “不行,没货了”。
  2. Confirm (确认):如果所有仓库在Try阶段都说“预留好了”,总指挥就发正式命令:“确认购买!把预留的书/玩具正式发出去吧!” (提交事务)。
  3. Cancel (取消):如果任何一个仓库在Try阶段说“不行”,或者总指挥在Try成功后因为某些原因(比如你反悔了)决定不买了,总指挥就发命令:“取消购买!把预留的书/玩具释放掉(库存加回去)吧!” (回滚事务)。

问题来了:网络是不可靠的!消息可能会延迟、重复或丢失。 这就导致了悬挂和空回滚。


1. 事务悬挂 (Hanging Transaction / Orphaned Try)

  • 通俗比喻:

    • 总指挥给仓库A发了一条Try指令:“预留一本书!”。
    • 仓库A成功预留了书,并回复:“预留成功!”。
    • 但是!这条回复消息在网络中迷路了(延迟或丢失),总指挥没收到
    • 总指挥等啊等,超时了,心想:“仓库A没回复,肯定失败了(或者超时了),那这次交易就算了吧。” 于是,总指挥发起了 Cancel 指令给仓库A和仓库B(即使仓库B可能还没收到Try)。
    • 关键点: 仓库A 后来终于收到了那个迷路的 Cancel 指令,并成功执行了取消(释放了预留的书)。
    • 现在问题来了: 那个最初迷路的 “预留成功” 的回复,在总指挥发出 Cancel 之后终于送到了总指挥那里!
    • 悬挂产生了: 总指挥看到仓库A的“预留成功”回复,会误以为这个预留是新开始的一个事务的Try请求。但实际上,这个预留是之前那个已经被Cancel掉的事务遗留下来的!这个预留的资源(那本书)就像幽灵一样被“悬挂”在仓库A里,没人知道它属于哪个有效的事务(因为最初的事务已经被取消了),它被孤立了,无法被后续正常的事务流程处理(Confirm 或 新的 Cancel),造成资源长期锁定
  • 核心问题: Try 操作执行成功了,但其结果(成功回复)在对应的 Cancel 操作执行完成之后才到达协调者。 导致这个 Try 预留的资源失去了归属,成了“孤儿”。

  • 危害: 被悬挂的资源(如库存、优惠券、余额)被锁定无法释放,影响后续业务。

总结就是新老try命令没有区分,按照我的记忆这个和分布式集群的脑裂问题很像,所有可以有相同的解法,记录版本,是新版本发的try我就接受,老的我就丢弃


2. 空回滚 (Empty Rollback / Phantom Rollback)

  • 通俗比喻:

    • 总指挥决定发起一次购买事务。
    • 给仓库B发了一条 Try 指令:“预留一个玩具!”。
    • 但是!这条 Try 指令在网络中迷路了(延迟或丢失),仓库B根本没收到,也根本没执行预留
    • 总指挥等仓库B的回复超时了,或者它收到了仓库A Try 失败的回复(假设仓库A的Try也发出了)。
    • 于是,总指挥决定回滚整个事务,发起了 Cancel 指令给仓库A和仓库B。
    • 仓库A收到 Cancel:它之前可能执行了Try(预留)也可能没执行。如果执行了,就释放预留;如果没执行,就啥也不做(但需要记录一下)。
    • 关键点: 仓库B终于收到了 Cancel 指令。但仓库B之前根本没收到过 Try 指令!它没有为这个事务预留任何玩具!
    • 空回滚发生了: 仓库B收到了一个要求它“取消预留”的指令(Cancel),但它压根没做过预留(Try)。这就相当于让仓库B去取消一个不存在的操作。
  • 核心问题: 在一个分支事务(仓库B)根本没有执行过 Try 操作的情况下,该分支事务收到了协调者发来的 Cancel 指令并要求执行回滚。

  • 为什么允许空回滚? 分布式系统要求最终一致性。即使某个参与者没收到Try,协调者发出Cancel了,所有参与者都必须能响应Cancel,确保最终状态一致(都回滚了)。系统设计上要求,即使没执行Try,收到Cancel也必须能处理并返回成功。

  • 潜在冲突: 如果那个迷路的 Try 指令在 Cancel 之后才到达仓库B,仓库B就会执行Try(预留一个玩具)。这时就出现了冲突:仓库B刚刚为一个事务预留了资源(Try),但这个事务在协调者那边已经被标记为回滚(Cancel)了!这就是为什么需要防悬挂措施(见下文)。


如何解决悬挂和空回滚?

系统设计者想出了办法,主要靠记录状态

  1. 防悬挂 (防止悬挂):

    • 每个分支事务(仓库)在执行真正的 Try 操作之前,先查一下本地记录。
    • 查什么?查有没有记录表明同一个全局事务ID (XID) 已经执行过 Cancel 操作了。
    • 如果查到有 Cancel 记录: 说明这个 Try 来晚了(悬挂的Try),拒绝执行这个 Try 操作!或者执行一个空Try(只记录状态,不真正预留资源)。
    • 如果没查到 Cancel 记录: 正常执行 Try(预留资源),并记录下 Try 状态。
    • 核心: Try 操作需要检查该事务是否已被回滚(Cancel 记录)
  2. 允许空回滚:

    • 当一个分支事务(仓库)收到 Cancel 指令,但发现自己没执行过这个事务的 Try 操作(本地没有该 XID 的 Try 记录)时:
    • 不能直接报错说“我没Try过,不能Cancel”。
    • 它必须先记录一条状态:“这个事务 XID 执行过空回滚(Cancel 了,但没 Try)”。
    • 然后返回 Cancel 成功
    • 核心: Cancel 操作需要支持幂等性(多次调用效果一样)和即使没有 Try 也要能执行(记录空回滚状态)。

总结一下这个防御机制:

  • Cancel 记录是老大: 一旦一个分支记录了 Cancel(无论是否空回滚),后续迟到的 Try 看到这个记录就必须“装死”(不执行或空执行)。
  • Try 要“看脸色”: Try 操作执行前必须看看有没有对应的 Cancel 记录,有就不干(或干个假的)。
  • Cancel 要“大度”: Cancel 操作即使发现自己没活干(没Try过),也要应一声“好的,取消了”(记录一下),保证流程能走下去。

快递流程对比总结

阶段 正常流程 事务悬挂 (幽灵包裹) 空回滚 (取消不存在的订单) 防御措施
Try (预留) 总指挥: “仓库A/B,预留货!”
仓库: “好的,预留了!” (记录预留)
总指挥: “仓库A,预留货!”
仓库A: 预留成功,但回复丢失
总指挥没收到回复,以为失败。
总指挥根本没发出 Try 给仓库B (或消息丢失) 仓库执行 Try 前:
查本地记录:
已有该订单的 Cancel 记录?拒绝执行真预留 (防悬挂)
无记录 → 正常预留并记录 Try。
Cancel (取消) 总指挥: “仓库A/B,取消预留!”
仓库: 释放预留。 (正常取消)
总指挥: (因超时) “仓库A,取消预留!”
仓库A: 收到 Cancel,释放预留,记录 Cancel
然后,迟到的 Try 成功回复才到总指挥 → 悬挂产生!
总指挥: (因超时或其他失败) “仓库B,取消预留!”
仓库B: 从未收到过该订单的 Try!
执行空回滚:记录 “XID 已空取消”,并回复成功。
仓库收到 Cancel:
查本地记录:
有该订单的 Try 记录? → 正常释放预留,记录 Cancel。
无 Try 记录?记录 “XID 已空取消” (允许空回滚),回复成功。
后续问题 仓库A 的预留已被取消,但总指挥后来收到它的 Try 成功回复,误以为是新事务 → 该预留成幽灵状态,资源被锁死。 迟到的 Try 指令之后到达仓库B
仓库B 检查记录 → 发现已有该 XID 的 Cancel 记录 (空回滚记录)拒绝执行真预留 (防悬挂生效!)
核心思想:
1. Cancel 记录优先: 见 Cancel 记录,Try 必须怂。
2. Cancel 要大度: 没活干也要应一声并记下来。
3. Try 要谨慎: 干活前先看有没有 Cancel 记录。

通过这种在参与者(仓库)本地记录事务状态(Try执行过吗?Cancel执行过吗?是空回滚吗?)的机制,就能有效地防止悬挂事务的产生,并正确处理空回滚请求,保证分布式事务最终的一致性。

四.限流的算法

好的,我们来通俗易懂地讲解一下这四种关键的限流算法:固定窗口计数滑动窗口计数令牌桶算法漏桶算法。它们都用于控制系统的流量(如 API 请求、网络数据包),防止系统被突发流量冲垮。

想象一个水龙头水桶的场景,请求就是水流。


1. 固定窗口计数法 (Fixed Window Counter)

  • 原理:

    • 把时间切成一个个固定长度的窗口(比如每 1 分钟一个窗口)。
    • 在每个窗口内,系统维护一个计数器
    • 当一个请求到来时:
      • 如果当前时间还在当前窗口内,且计数器小于预设的阈值(比如 100 次),则计数器加 1,请求被允许。
      • 如果计数器达到或超过阈值,则请求被拒绝。
      • 如果当前时间超过了当前窗口的结束时间,则立即开启一个新的窗口,并将计数器重置为 0,然后处理这个请求(通常是允许,因为新窗口刚开始)。
  • 通俗比喻:

    • 你开了一个小卖部,规定每分钟最多只能卖 10 瓶水(窗口长度=1分钟,阈值=10)。
    • 在 0:00 - 0:59 这个窗口内,卖出了 9 瓶。
    • 在 0:59.999 秒,来了一个人想买水(第 10 瓶),允许,卖出,计数器=10。
    • 在 0:59.999 秒之后的下一个瞬间(比如 1:00.000),时间进入了新的窗口(1:00 - 1:59),计数器清零
    • 这时突然涌进来 20 个人(比如 1:00.001 秒),因为新窗口刚开始,计数器是 0,所以这 20 个人的请求全部被允许!虽然他们几乎是在同一秒内来的。小卖部瞬间被抢购一空。
  • 优点: 实现简单,内存消耗少(只需存储当前窗口计数和窗口结束时间)。

  • 缺点: 临界点问题(Boundary Problem) 非常严重!如上例所示,在两个窗口的交界处(如 0:59 - 1:00),系统可能会在极短时间内承受 2倍阈值 的请求(前一个窗口末尾的请求 + 后一个窗口开头的请求),这对系统是很大的冲击。不够平滑。


2. 滑动窗口计数法 (Sliding Window Counter)

  • 原理:

    • 为了解决固定窗口的临界点问题。
    • 它不再使用固定边界,而是统计最近一段时间内(比如最近 1 分钟)的请求总数。
    • 实现方式通常有两种:
      1. 精确计数(需要存储时间戳): 维护一个时间戳列表或队列,记录每个请求到达的时间。当新请求到来时,移除所有超出“最近 N 秒”范围的老请求时间戳,然后计算队列中剩余请求的数量(即最近 N 秒内的请求数)。如果数量小于阈值,则允许并记录新请求时间戳;否则拒绝。
      2. 近似计数(更常用,子窗口划分): 将大的滑动窗口(如 1 分钟)划分为多个更小的、固定长度的子窗口(Sub-window,如 6 个 10 秒的子窗口)。系统维护这些子窗口的计数器和一个“窗口开始时间”指针。新请求到来时:
        • 移除所有过期的子窗口(时间早于 “当前时间 - 大窗口长度”)。
        • 计算当前所有未过期子窗口的计数器总和
        • 如果总和 < 阈值,则允许请求,并将当前请求计数加到最新的子窗口计数器上(如果当前时间已进入下一个子窗口,则创建新的子窗口)。
        • 否则拒绝请求。
  • 通俗比喻:

    • 还是那个小卖部,规定最近 1 分钟内最多只能卖 10 瓶水。
    • 系统把 1 分钟拆成 6 个 10 秒的小格子(子窗口)。
    • 现在时间是 1:00。在 0:55 - 0:59.999 期间(属于第 1 分钟的后半段)卖出了 5 瓶(记录在最后两个小格子里)。
    • 在 1:00.001 秒,涌进来 20 个人想买水。
    • 系统计算:当前时间 1:00.001,往前推 1 分钟是 0:00.001。0:00.001 到 0:55 之间的格子(前 5.5 个小格子)都过期了,被扔掉。只剩下 0:55 - 1:00 这 1 个小格子(实际上是半个,但按子窗口算可能算一个)里记录的 5 瓶水。
    • 最近 1 分钟(实际是最近 5 秒)的总请求数是 5 < 10。
    • 所以,系统允许这 20 个人中的前 5 个人(5 + 5 = 10)买水(把计数加到 1:00 - 1:10 这个新子窗口)。第 6 到第 20 个人会被拒绝!因为加上他们,最近 1 分钟(主要是刚过去的 5 秒和当前瞬间)的请求数就超过 10 了。
  • 优点: 大大缓解了固定窗口的临界点问题,流量控制更加平滑和精确。能更好地反映最近的流量状况。

  • 缺点: 实现比固定窗口复杂。精确计数需要存储更多数据(每个请求的时间戳)或消耗更多 CPU(频繁清理过期数据)。子窗口法是一种内存和精度之间的权衡,子窗口越多越精确但内存开销越大。


3. 令牌桶算法 (Token Bucket)

  • 原理:

    • 想象一个,这个桶有一个固定容量(Capacity,比如 100 个令牌)。
    • 系统以恒定速率(Rate,比如每秒 10 个)往这个桶里放入令牌。如果桶满了,新令牌被丢弃。
    • 当一个请求(数据包、API 调用)到来时:
      • 它需要从桶里取出一个令牌
      • 如果桶里有令牌(>0),取出一个令牌,请求被允许执行
      • 如果桶里没有令牌(=0),请求被拒绝排队等待(取决于实现)。
    • 关键点:如果一段时间没有请求,桶里的令牌会累积起来(最多到桶容量)。当有突发流量到来时,只要桶里有足够的令牌,这些突发请求可以立即被处理(直到令牌用完)。
  • 通俗比喻:

    • 你有一个装游戏币的桶,桶最多能装 100 枚币(容量)。
    • 管理员每秒往桶里放 10 枚币(恒定速率),桶满了就不放了。
    • 玩家想玩游戏,每次需要投 1 枚币(取一个令牌)。
    • 场景 1:桶里有 50 枚币。突然来了 30 个玩家。每个玩家都能立即拿到币玩游戏(处理请求),桶里剩下 20 枚币。
    • 场景 2:桶里有 100 枚币(满的)。突然来了 120 个玩家。前 100 个玩家能立即拿到币玩游戏。后 20 个玩家因为桶空了,只能等待(排队)或被拒绝(离开)。
    • 场景 3:连续 5 秒没人来玩。管理员每秒放 10 枚,5 秒后桶里就有 50 枚币了(原来可能是空的或不满)。这时如果来 50 个玩家,他们都能立刻玩。桶积累的令牌允许处理突发流量。
  • 优点:

    • 允许突发流量: 桶里积累的令牌可以应对短时间的流量高峰。
    • 长期平均速率可控: 放入令牌的速率决定了长期的平均请求处理速率(如 10 req/s)。
    • 相对平滑: 没有固定窗口那种明显的边界临界问题。
  • 缺点: 实现相对复杂(需要定时放令牌或惰性计算),需要维护桶的当前令牌数量和时间戳。突发流量的大小受限于桶容量。

  • 典型应用: 网络流量整形(如 QoS),API 网关限流(允许合理突发),云计算服务限流。


4. 漏桶算法 (Leaky Bucket)

  • 原理:

    • 想象一个底部有孔的桶。
    • 请求(水流)以任意速率流入桶中。
    • 桶有一个固定容量(Capacity)。如果流入的请求速率过快导致桶满了,新来的请求就会溢出(被拒绝)。
    • 无论流入的速率多快多慢,桶底部的孔以恒定速率(Rate)漏出水(处理请求)。
    • 关键点:请求的处理(出水)速率是绝对恒定的。突发流量会被桶“缓存”一下,但处理速度不会变快,只能按照恒定速率流出。如果桶空了,则出水停止(无请求可处理)。
  • 通俗比喻:

    • 你有一个底部有小洞的漏斗(漏桶),漏斗最多能装 100 毫升水(容量)。
    • 水(请求)可以任意速度倒进漏斗(用户请求速率)。
    • 如果倒太快,漏斗满了(100ml),多余的水就会溢出来(拒绝请求)。
    • 无论你倒得多快多慢,水从底部小洞流出的速度恒定为每秒 10 毫升(恒定处理速率)。
    • 场景 1:你慢慢倒水(5ml/s),漏斗不会满,水以 10ml/s 流出(处理速度比输入快,桶通常是空的或不满)。
    • 场景 2:你突然猛倒 150ml 水(突发请求)。前 100ml 进入漏斗,后 50ml 溢出(被拒绝)。然后漏斗里的 100ml 水,会按照 10ml/s 的恒定速度慢慢流完,总共需要 10 秒。突发流量被“整形”成了平滑输出。
    • 场景 3:你持续以 15ml/s 倒水。漏斗会以 10ml/s 的速度漏水。多余的 5ml/s 会累积在漏斗里。大约 20 秒后漏斗就满了(100ml / (15-10)ml/s = 20s),之后所有倒进来的水都会溢出(请求被拒绝),直到你倒水的速度降到 <=10ml/s。
  • 优点:

    • 输出流量绝对平滑: 无论输入多么起伏,输出(请求被处理的速率)是恒定的(Rate)。这对于下游系统非常友好。
    • 强制限制了最大速率: 处理速率不会超过 Rate。
    • 简单易实现(队列版): 常用一个 FIFO(先进先出)队列来实现:请求到来时,如果队列未满则入队(水流入桶),队列已满则拒绝(溢出);一个单独的处理器以恒定速率(Rate)从队列头部取出请求处理(水从孔漏出)。
  • 缺点:

    • 无法应对突发流量: 即使桶是空的,处理速率也不会加快,还是恒定的 Rate。桶的作用只是缓存/整形,不能加速处理。突发流量要么被拒绝(桶满时),要么只能排队等待以恒定速度处理。
    • 可能引入较大延迟: 当桶里有积压时,新请求需要排队等待较长时间才能被处理。
  • 典型应用: 需要严格平滑输出速率的场景,如老式电信网络、某些需要保护下游脆弱系统的场景。


总结对比表

特性 固定窗口计数 滑动窗口计数 令牌桶算法 漏桶算法
核心思想 固定时间段内计数 最近时间段内计数 按固定速率产生令牌,请求消耗令牌 请求流入队列,按固定速率流出处理
关键数据结构 计数器 + 窗口结束时间 时间戳队列 / 子窗口计数器数组 当前令牌数 + 最后填充时间 FIFO 队列
流量控制依据 当前窗口计数 < 阈值? 最近 N 秒计数 < 阈值? 桶中有令牌? 队列未满?
处理速率 窗口内平均 <= 阈值 任意时刻最近 N 秒平均 <= 阈值 长期平均 <= 令牌产生速率 绝对恒定 = 流出速率
突发流量处理 ❌ 差 (临界点 2 倍冲击) ⚠️ 较好 (取决于窗口精度) ✅ 好 (可用令牌数内允许突发) ⚠️ 有限 (靠队列缓存,但处理速度不变)
输出流量平滑度 ❌ 不平滑 (窗口边界突变) ✅ 较平滑 ✅ 较平滑 (允许突发) ✅✅ 绝对平滑
主要缺点 临界点问题 实现较复杂,资源消耗稍高 实现较复杂 无法利用空闲资源加速处理突发
典型用途 简单限流场景 需要相对精确平滑限流的场景 允许合理突发、限制长期平均速率的场景 需要强制平滑输出速率的场景

简单选择指南:

  • 简单粗暴:固定窗口(但注意临界问题)。
  • 精确平滑:滑动窗口。
  • 允许合理突发:令牌桶。
  • 绝对平滑输出:漏桶。

理解这四种算法是设计和实现高效、稳定限流系统的基础。它们各有千秋,需要根据实际应用场景的需求(是否允许突发?是否需要绝对平滑?实现复杂度要求?)来选择合适的算法或组合。

你可能感兴趣的:(学习记录,java,算法,开发语言)