限流算法又双叒叕被击穿了?这6种算法让你的系统固若金汤!

限流算法又双叒叕被击穿了?这6种算法让你的系统固若金汤!

大家好,我是服务端技术精选的小编。今天来聊聊一个让无数后端程序员夜不能寐的话题——限流算法

你是不是也遇到过这种情况:明明服务器配置不错,但一到高峰期就各种超时、宕机?流量一大系统就歇菜,像纸糊的一样脆弱?

别慌!老司机今天就给你盘点6种限流算法,从最简单的计数器到最优雅的令牌桶,让你的系统从此固若金汤!

一、不限流的系统,就是定时炸弹

先说说为啥需要限流。想象一下这个场景:你开了个小面馆,平时每天能接待100个客人。突然有一天,来了1000个客人,你会怎么办?

如果硬接,厨师累死,客人等到天荒地老,最后谁都不满意;如果合理控制,虽然损失一些生意,但至少能保证现有客人的体验。

限流就是在保护系统的同时,保证服务质量!

不限流的后果

我曾经见过一个真实案例:某电商平台搞秒杀活动,平时QPS只有几百,结果活动开始瞬间涌入10万QPS。因为没有做限流保护,整个系统直接崩溃,不仅秒杀商品没卖出去,连正常的商品详情页都打不开了。

常见的系统被"击穿"场景

  1. 热点事件:突发新闻导致流量暴涨
  2. 恶意攻击:DDoS攻击、爬虫恶意抓取
  3. 促销活动:双11、618等大促期间流量激增
  4. 系统故障:某个节点挂了,流量全部涌向其他节点

二、6种限流算法大揭秘

算法1:固定窗口计数器 - 最简单的"门卫"

原理:在固定时间窗口内,统计请求数量,超过阈值就拒绝。

public class FixedWindowCounter {
    private final int limit;
    private final long windowSize;
    private volatile long windowStart;
    private volatile int counter;

    public FixedWindowCounter(int limit, long windowSize) {
        this.limit = limit;
        this.windowSize = windowSize;
        this.windowStart = System.currentTimeMillis();
        this.counter = 0;
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        if (now - windowStart >= windowSize) {
            windowStart = now;
            counter = 0;
        }
        
        if (counter < limit) {
            counter++;
            return true;
        }
        
        return false;
    }
}

优点:实现简单,内存占用小,性能高
缺点:存在"突刺现象",不够平滑
适用场景:流量平稳的系统,对精度要求不高

算法2:滑动窗口计数器 - 更精准的"计时员"

原理:将时间窗口分成多个小格子,滑动统计,避免固定窗口的突刺问题。

public class SlidingWindowCounter {
    private final int limit;
    private final long windowSize;
    private final int bucketCount;
    private final AtomicInteger[] buckets;

    public SlidingWindowCounter(int limit, long windowSize, int bucketCount) {
        this.limit = limit;
        this.windowSize = windowSize;
        this.bucketCount = bucketCount;
        this.buckets = new AtomicInteger[bucketCount];
        
        for (int i = 0; i < bucketCount; i++) {
            buckets[i] = new AtomicInteger(0);
        }
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        cleanExpiredBuckets(now);
        
        int totalCount = Arrays.stream(buckets)
            .mapToInt(AtomicInteger::get)
            .sum();
        
        if (totalCount < limit) {
            int bucketIndex = (int) ((now / (windowSize / bucketCount)) % bucketCount);
            buckets[bucketIndex].incrementAndGet();
            return true;
        }
        
        return false;
    }
}

优点:更平滑,避免突刺现象
缺点:内存占用较大,实现复杂
适用场景:对平滑性要求较高的场景

算法3:令牌桶算法 - 最优雅的"发令员"

原理:系统以恒定速率产生令牌放入桶中,请求需要获取令牌才能通过。

public class TokenBucket {
    private final long capacity;
    private final double refillRate;
    private double tokens;
    private long lastRefillTime;

    public TokenBucket(long capacity, double refillRate) {
        this.capacity = capacity;
        this.refillRate = refillRate;
        this.tokens = capacity;
        this.lastRefillTime = System.currentTimeMillis();
    }

    public synchronized boolean tryAcquire() {
        refillTokens();
        
        if (tokens >= 1) {
            tokens -= 1;
            return true;
        }
        
        return false;
    }

    private void refillTokens() {
        long now = System.currentTimeMillis();
        if (now > lastRefillTime) {
            double tokensToAdd = (now - lastRefillTime) * refillRate / 1000.0;
            tokens = Math.min(capacity, tokens + tokensToAdd);
            lastRefillTime = now;
        }
    }
}

Guava RateLimiter使用

// 使用Google Guava的令牌桶实现
RateLimiter rateLimiter = RateLimiter.create(2.0); // 每秒2个令牌

public void handleRequest() {
    if (rateLimiter.tryAcquire()) {
        // 处理请求
        processRequest();
    } else {
        // 拒绝请求
        rejectRequest();
    }
}

优点:允许突发流量,流量整形效果好
缺点:实现相对复杂
适用场景:需要应对突发流量的系统

算法4:漏桶算法 - 最稳定的"水龙头"

原理:请求进入漏桶,以恒定速率流出,超过桶容量的请求被丢弃。

public class LeakyBucket {
    private final long capacity;
    private final double leakRate;
    private double currentVolume;
    private long lastLeakTime;

    public LeakyBucket(long capacity, double leakRate) {
        this.capacity = capacity;
        this.leakRate = leakRate;
        this.currentVolume = 0;
        this.lastLeakTime = System.currentTimeMillis();
    }

    public synchronized boolean tryAdd(double volume) {
        leak();
        
        if (currentVolume + volume <= capacity) {
            currentVolume += volume;
            return true;
        }
        
        return false;
    }

    private void leak() {
        long now = System.currentTimeMillis();
        if (now > lastLeakTime) {
            double leaked = (now - lastLeakTime) * leakRate / 1000.0;
            currentVolume = Math.max(0, currentVolume - leaked);
            lastLeakTime = now;
        }
    }
}

优点:出口流量恒定,削峰效果好
缺点:无法应对合理的突发流量
适用场景:需要严格控制出口流量

算法5:滑动窗口限流 - 最精确的"监控员"

原理:维护一个滑动的时间窗口,精确统计窗口内的请求数。

public class SlidingWindowRateLimiter {
    private final int limit;
    private final long windowSize;
    private final Queue<Long> requestTimes;

    public SlidingWindowRateLimiter(int limit, long windowSize) {
        this.limit = limit;
        this.windowSize = windowSize;
        this.requestTimes = new LinkedList<>();
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 移除窗口外的请求
        while (!requestTimes.isEmpty() && 
               now - requestTimes.peek() > windowSize) {
            requestTimes.poll();
        }
        
        if (requestTimes.size() < limit) {
            requestTimes.offer(now);
            return true;
        }
        
        return false;
    }
}

优点:精度最高,避免突刺现象
缺点:内存占用最大,性能开销较高
适用场景:对精度要求极高的场景

算法6:分布式限流 - 多节点的"协调员"

原理:在分布式环境下,多个节点协同限流。

@Component
public class DistributedRateLimiter {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public boolean fixedWindowLimit(String key, int limit, long windowSize) {
        long window = System.currentTimeMillis() / windowSize;
        String redisKey = key + ":" + window;
        
        String luaScript = 
            "local current = redis.call('incr', KEYS[1]) " +
            "if current == 1 then " +
            "  redis.call('expire', KEYS[1], ARGV[2]) " +
            "end " +
            "if current <= tonumber(ARGV[1]) then " +
            "  return 1 " +
            "else " +
            "  return 0 " +
            "end";
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        Long result = redisTemplate.execute(script,
            Collections.singletonList(redisKey),
            String.valueOf(limit),
            String.valueOf(windowSize / 1000));
        
        return result != null && result == 1;
    }
}

三、实战案例:电商平台限流方案

业务背景

某电商平台关键接口:

  • 商品详情:QPS 1万,可突发到3万
  • 用户登录:QPS 5千,需防刷
  • 下单接口:QPS 2千,绝对不能超

分层限流策略

// 网关层 - 全局限流
@Component
public class GatewayRateLimiter {
    private final TokenBucket globalLimiter = new TokenBucket(50000, 40000);
    
    public boolean checkGlobalLimit() {
        return globalLimiter.tryAcquire();
    }
}

// 应用层 - 接口限流
@RestController
public class ProductController {
    private final TokenBucket productLimiter = new TokenBucket(30000, 10000);
    
    @GetMapping("/product/{id}")
    public ResponseEntity<?> getProduct(@PathVariable Long id) {
        if (!productLimiter.tryAcquire()) {
            return ResponseEntity.status(429).body("商品查询繁忙,请稍后重试");
        }
        
        return ResponseEntity.ok(productService.getById(id));
    }
}

// 用户级限流
@Component
public class UserRateLimiter {
    @Autowired
    private DistributedRateLimiter distributedLimiter;
    
    public boolean checkUserLimit(Long userId, String operation) {
        String key = "user:" + userId + ":" + operation;
        
        switch (operation) {
            case "login":
                return distributedLimiter.fixedWindowLimit(key, 10, 3600000);
            case "order":
                return distributedLimiter.fixedWindowLimit(key, 3, 60000);
            default:
                return true;
        }
    }
}

四、限流算法选择指南

选择决策树

是否允许突发流量?
    ↓               ↓
   是              否
    ↓               ↓
令牌桶算法      漏桶/固定窗口
    ↓               ↓
需要高精度?     需要高精度?
    ↓               ↓
滑动窗口        滑动窗口计数器

选择建议

算法优点缺点适用场景
固定窗口简单、高性能突刺现象简单场景
滑动窗口平滑、精度好内存占用大中等精度要求
令牌桶允许突发、灵活实现复杂API限流
漏桶流量平滑不允许突发保护下游
精确滑动精度最高性能开销大金融系统
分布式集群协调依赖外部存储微服务

五、限流最佳实践

1. 分层限流

CDN层 → 网关层 → 服务层 → 接口层 → 用户层

2. 优雅降级

@Component
public class GracefulDegradation {
    
    public ResponseEntity<?> handleRateLimit(String operation) {
        switch (operation) {
            case "product":
                return getCachedProduct();
            case "search":
                return getHotRecommendations();
            default:
                return ResponseEntity.status(429).body("系统繁忙,请稍后重试");
        }
    }
}

3. 监控告警

# Prometheus告警规则
groups:
  - name: rate_limit
    rules:
      - alert: HighRateLimitRejection
        expr: rate(rate_limit_rejected_total[5m]) > 100
        for: 2m
        annotations:
          summary: "限流拒绝率过高"

六、总结:限流的核心要领

限流不是万能药,但是系统稳定性的重要保障。记住这几个核心要领:

核心原则

  1. 预防为主:在系统设计阶段就要考虑限流
  2. 分层防护:从网关到应用的多层限流
  3. 优雅降级:被限流时要有合理的降级策略
  4. 监控驱动:基于监控数据动态调整限流策略

选择建议

  • 简单场景:固定窗口计数器
  • 一般场景:令牌桶算法
  • 严格场景:漏桶算法
  • 高精度场景:滑动窗口限流
  • 分布式场景:Redis分布式限流

最佳实践清单

  •  根据业务特点选择合适的算法
  •  配置合理的限流阈值
  •  实现优雅的降级策略
  •  建立完善的监控体系
  •  支持动态调整限流参数

记住老司机的三句话:

  • "限流是手段,不是目的,最终要保证用户体验"
  • "没有完美的算法,只有合适的选择"
  • "限流要有温度,让用户感受到系统的关怀"

关注"服务端技术精选",不迷路!
持续分享Java后端实战干货!
点赞、转发、收藏就是对我最大的支持!

下期预告:《缓存击穿又双叒叕发生了?这5种解决方案让你的系统永不宕机!》

限流算法选择思维导图

业务需求 → 算法选择 → 参数调优 → 监控告警 → 持续优化
    ↓        ↓        ↓        ↓        ↓
突发流量   实现复杂度  阈值设置  关键指标  性能调优

标题:限流算法又双叒叕被击穿了?这6种算法让你的系统固若金汤!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/21/1766304294832.html

    0 评论
avatar