计数器限流案例

wen java案例 1

从单机到分布式:计数器限流算法的经典案例与防击穿实战指南


目录导读

  1. 引言:为什么计数器限流是互联网系统的“隐形门卫”
  2. 计数器限流核心原理:从“水龙头”到“令牌桶”的思维跃迁
  3. 经典案例拆解:单机版计数器限流的Java实现与陷阱
    • 1 基础版:AtomicLong + 定时重置
    • 2 进阶版:滑动窗口计数器(解决临界突变)
  4. 防击穿实战:计数器限流在秒杀系统中的应用与血泪教训
  5. 高频问答:面试官最想听懂的4个计数器限流细节
  6. 总结与扩展:从单机到分布式(Redis+Lua的演进路径)

引言:为什么计数器限流是互联网系统的“隐形门卫”

在微服务架构盛行的今天,系统面临的瞬时流量峰值远超过硬件能承受的物理极限,比如微博热搜、电商大促或抢票软件,每秒数万次的请求风暴足以击穿任何未设防的数据库连接池,计数器限流(Fixed Window Counter)作为最原始、最直观的流量控制手段,以极低的内存开销(仅需一个整数) 实现了“保证系统不死”的最低目标,它不追求完美平滑,而是用简单粗暴的方式告诉上游:“我这会儿忙不过来,你先等5秒再试”。

计数器限流案例

计数器限流核心原理:从“水龙头”到“令牌桶”的思维跃迁

想象一个水龙头(你的接口),每秒只能流出100滴水(吞吐量)。计数器限流就是安装一个机械水表:从本秒0点开始计数,水表数值超过100时,直接关门(拒绝请求),等下一秒开始,水表清零重新计数。
这个模型的最大缺陷是“临界突变”:若第1秒最后100ms涌来100个请求,第2秒前100ms又涌来100个请求,那么这200ms内实际放行了200个请求,远超每秒100的阈值,这被称为“窗口切换临界问题”,而滑动窗口计数器(把1秒拆成4个250ms的格子,滚动计算总和)则能规避这个裂缝。

经典案例拆解:单机版计数器限流的Java实现与陷阱

1 基础版:AtomicLong + 定时重置
public class FixedWindowLimiter {
    private final int maxCount = 100; // 窗口内最大请求数
    private long windowStart = System.currentTimeMillis();
    private AtomicLong counter = new AtomicLong(0);
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        if (now - windowStart >= 1000) {
            counter.set(0);
            windowStart = now;
        }
        return counter.incrementAndGet() <= maxCount;
    }
}

致命陷阱synchronized 锁在多线程高并发下虽能保证原子性,但锁竞争严重降低吞吐量,实际生产建议使用 LongAdderAtomicLongupdateAndGet(CAS自旋),但无法避免“窗口重置瞬间的并发安全”问题(需配合双重检查锁)。

2 进阶版:滑动窗口计数器(解决临界突变)

将时间段划分为N个槽位(如4个槽位,每槽代表250ms),采用环形数组存储每个槽位的请求数,每次请求时累加当前槽位减去即将废弃的槽位值,示例核心代码:

private int[] slots = new int[4]; // 4个槽位
private int currentIndex = 0;
private long lastMoveTime = System.currentTimeMillis();
public boolean allow() {
    long now = System.currentTimeMillis();
    int moved = (int)((now - lastMoveTime) / 250); // 计算需要滑动几个槽
    for (int i = 0; i < moved; i++) {
        currentIndex = (currentIndex + 1) % 4;
        slots[currentIndex] = 0; // 清空旧槽
    }
    lastMoveTime = now;
    int sum = Arrays.stream(slots).sum();
    if (sum < 100) { slots[currentIndex]++; return true; }
    return false;
}

注意:该实现存在线程安全问题,实战中需加锁或使用 RingBuffer(如Disruptor)。

防击穿实战:计数器限流在秒杀系统中的应用与血泪教训

惨案原型:某中小型电商平台在0点秒杀时,未对“查询库存接口”做任何保护,瞬间1万并发袭来,数据库连接池被打爆,导致正常用户也无法访问
整改方案

  • 第一层:网关层(Nginx)配置 limit_req_zone 按IP做固定窗口限流(每秒5次)。
  • 第二层:应用层对“库存查询”接口使用 Redis + Lua 脚本实现分布式计数器(每秒1000次),防止跨实例累加误差。
  • 第三层:对白名单用户(VIP)单独设置计数器额度,防止“一人抢光所有额度”。
    血泪教训计数器限流必须搭配“快速失败”,直接返回 503 或“稍后重试”,绝不能排队等待,否则队列本身会成为新的攻击目标。

高频问答:面试官最想听懂的4个计数器限流细节

Q1:固定窗口计数器和滑动窗口计数器,谁更耗内存?
A:固定窗口仅需1个整数,内存占用O(1);滑动窗口需要N个槽位(通常4-8个),内存占用O(N),但滑动窗口通过对时间切片的细化,能将“临界突发流量”从2倍误差降低到 (1 + 1/N) 倍左右。

Q2:计数器限流和漏斗(Leaky Bucket)限流的本质区别?
A:计数器是 “丢弃多余” ,漏斗是 “匀速转发” ,计数器允许突发(只要总量不超),漏斗强制恒定速率,因此计数器更适合“可丢弃请求”的场景,比如秒杀,而漏斗适合“必须平滑处理”的数据库写入。

Q3:如何解决计数器限流的“冷启动”问题?
A:如果系统刚启动时计数器为空,短时间涌入大量请求会被全部放行,可引入预热因子,如:实际阈值 = maxThreshold * 已运行时间 / 预热时长,直到达到全量阈值。

Q4:单机计数器在集群中会失效吗?如何扩展?
A:完全失效,集群中每台机器有独立的计数,总放行量会 N倍放大,解决方案是使用 Redis INCR + EXPIRE 原子操作,或使用 Sentinel/Resilience4j 提供的集群限流器(需引入一致性哈希)。

总结与扩展:从单机到分布式(Redis+Lua的演进路径)

计数器限流是算法基石,但它本质上是“非平滑”的,生产环境的最佳实践是 “累进防御”

  1. 本地计数器(毫秒级响应,用于挡住恶意攻击);
  2. 分布式Redis计数器(精确跨节点统计);
  3. 令牌桶算法(平滑突发,如Guava RateLimiter);
  4. 自适应限流(根据CPU负载、RT耗时动态调整阈值,如Alibaba Sentinel)。

最后一道思考题:如果你的系统只有5个实例,每个实例限流100QPS,但整体流量必须控制在400QPS,你会怎么设计?答案:采用全局计数(Redis)并设置每个实例的本地最大上限为全局阈值的80%作为安全裕度。


(本文基于主流电商与开放平台限流方案归纳总结,核心代码片段可复用于中小型项目,但生产环境务必结合压测结果调整参数。)

抱歉,评论功能暂时关闭!