🚦 限流算法大揭秘:让你的系统不再"堵车"!

72 阅读14分钟

副标题:令牌桶、漏桶、滑动窗口、计数器,四大天王谁更强?🎯


🎬 开场白:为什么需要限流?

想象一下这个场景:

你开了一家奶茶店🧋,正常情况下一个小时能做30杯奶茶。突然有一天,某个网红在抖音上推荐了你家奶茶,瞬间涌来500个客人...

没有限流的后果:

  • 店员累倒了 😵(服务器宕机)
  • 奶茶品质下降了 😭(响应变慢)
  • 后面的客人白等了 😤(请求超时)
  • 店铺被差评淹没了 💔(用户体验极差)

有限流的智慧:

  • 排队叫号,有序接待 😊(流量控制)
  • 品质稳定,顾客满意 ✨(服务质量保证)
  • 超出容量就拒绝或延后 🎫(拒绝策略)

这就是限流的意义:在系统容量有限的情况下,保证服务质量!


📊 四大限流算法全解析

1️⃣ 固定窗口计数器(Fixed Window Counter)

原理图解

时间轴:  0s    1s    2s    3s    4s    5s
         │────窗口1────│────窗口2────│────窗口3────│
请求数:  [10个]        [15个]        [8个]
限制:    每秒最多20个请求

窗口1: ✅ 通过(10 < 20)
窗口2: ✅ 通过(15 < 20)
窗口3: ✅ 通过(8 < 20

代码实现

public class FixedWindowRateLimiter {
    private final int maxRequests;  // 每个窗口最大请求数
    private final long windowSize;  // 窗口大小(毫秒)
    private AtomicInteger counter = new AtomicInteger(0);
    private long windowStart = System.currentTimeMillis();
    
    public FixedWindowRateLimiter(int maxRequests, long windowSizeSeconds) {
        this.maxRequests = maxRequests;
        this.windowSize = windowSizeSeconds * 1000;
    }
    
    public synchronized boolean allowRequest() {
        long now = System.currentTimeMillis();
        
        // 判断是否需要重置窗口
        if (now - windowStart >= windowSize) {
            counter.set(0);
            windowStart = now;
        }
        
        // 判断是否超过限制
        if (counter.get() < maxRequests) {
            counter.incrementAndGet();
            return true;
        }
        
        return false;  // 被限流
    }
}

使用示例

// 每秒最多100个请求
FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(100, 1);

if (limiter.allowRequest()) {
    // 处理请求
    processRequest();
} else {
    // 返回限流错误
    return "系统繁忙,请稍后再试";
}

致命缺陷:临界问题 ⚠️

          0:59秒        1:00秒
           │              │
  ────────┼──────────────┼────────
          │    100请求    │100请求
          └──窗口1────┘└──窗口2──
                ↑
         在1秒内通过了200个请求!
         超出限制但没被拦截!

生活例子 🏥:

  • 医院规定每小时接待100人
  • 0:50-0:59来了100人(窗口1通过)
  • 1:00-1:09又来了100人(窗口2通过)
  • 实际上10分钟来了200人,但没被限流!

优点 ✅:

  • 实现简单
  • 内存占用小
  • 性能高

缺点 ❌:

  • 临界问题严重
  • 无法应对突发流量
  • 流量不平滑

2️⃣ 滑动窗口计数器(Sliding Window Counter)

原理图解

固定窗口 vs 滑动窗口

固定窗口:
│──窗口1──│──窗口2──│──窗口3──│
  0-1s      1-2s      2-3s

滑动窗口:
│──────│
   └──────│
      └──────│
         └──────│
每次都统计当前时间往前推1秒的请求数

精细化实现:分片统计

将1秒划分为10个小格(每格100ms)

  0   100  200  300  400  500  600  700  800  900  1000ms
  │────│────│────│────│────│────│────│────│────│────│
  [5] [8] [12] [7] [6] [9] [11] [8] [10] [4]

当前时间:850ms
统计范围:850ms - 750ms = 前1秒
计算方法:从当前格往前推10格的总和

代码实现

public class SlidingWindowRateLimiter {
    private final int maxRequests;     // 最大请求数
    private final long windowSize;     // 窗口大小(毫秒)
    private final int sliceSize;       // 分片数量
    private final AtomicInteger[] counters;  // 每个分片的计数器
    private final long sliceDuration;  // 每个分片的时长
    
    public SlidingWindowRateLimiter(int maxRequests, long windowSizeSeconds, int sliceSize) {
        this.maxRequests = maxRequests;
        this.windowSize = windowSizeSeconds * 1000;
        this.sliceSize = sliceSize;
        this.sliceDuration = windowSize / sliceSize;
        this.counters = new AtomicInteger[sliceSize];
        for (int i = 0; i < sliceSize; i++) {
            counters[i] = new AtomicInteger(0);
        }
    }
    
    public boolean allowRequest() {
        long now = System.currentTimeMillis();
        
        // 计算当前所在的分片
        int currentSlice = (int) ((now / sliceDuration) % sliceSize);
        
        // 清理过期的分片
        cleanExpiredSlices(now);
        
        // 统计当前窗口内的总请求数
        int totalRequests = 0;
        for (AtomicInteger counter : counters) {
            totalRequests += counter.get();
        }
        
        // 判断是否超过限制
        if (totalRequests < maxRequests) {
            counters[currentSlice].incrementAndGet();
            return true;
        }
        
        return false;
    }
    
    private void cleanExpiredSlices(long now) {
        long expireTime = now - windowSize;
        for (int i = 0; i < sliceSize; i++) {
            long sliceStartTime = (now / sliceDuration - i) * sliceDuration;
            if (sliceStartTime < expireTime) {
                counters[i].set(0);
            }
        }
    }
}

Sentinel的实现:LeapArray

// Sentinel底层使用的滑动窗口实现
public class LeapArray<T> {
    // 窗口时长(毫秒)
    protected int windowLengthInMs;
    // 采样窗口数量
    protected int sampleCount;
    // 采样间隔
    protected int intervalInMs;
    
    // 核心数组:环形数组存储窗口
    protected final AtomicReferenceArray<WindowWrap<T>> array;
    
    public WindowWrap<T> currentWindow() {
        return currentWindow(TimeUtil.currentTimeMillis());
    }
    
    public WindowWrap<T> currentWindow(long timeMillis) {
        long timeId = timeMillis / windowLengthInMs;
        int idx = (int)(timeId % array.length());
        
        // 计算窗口开始时间
        long windowStart = timeId * windowLengthInMs;
        
        while (true) {
            WindowWrap<T> old = array.get(idx);
            
            // 1. 桶为空,创建新桶
            if (old == null) {
                WindowWrap<T> window = new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket());
                if (array.compareAndSet(idx, null, window)) {
                    return window;
                } else {
                    Thread.yield();
                }
            }
            // 2. 桶的时间正好,直接使用
            else if (windowStart == old.windowStart()) {
                return old;
            }
            // 3. 桶的时间过期了,重置
            else if (windowStart > old.windowStart()) {
                if (updateLock.tryLock()) {
                    try {
                        return resetWindowTo(old, windowStart);
                    } finally {
                        updateLock.unlock();
                    }
                } else {
                    Thread.yield();
                }
            }
            // 4. 桶的时间在未来(并发问题),等待
            else {
                return new WindowWrap<>(windowLengthInMs, windowStart, newEmptyBucket());
            }
        }
    }
}

优点 ✅:

  • 解决了临界问题
  • 流量统计更精确
  • 能应对突发流量

缺点 ❌:

  • 实现复杂
  • 内存占用较大
  • 性能略低于固定窗口

3️⃣ 漏桶算法(Leaky Bucket)

核心思想

就像一个漏水的桶

        请求流入
           ↓↓↓
      ┌─────────┐
      │  ╔═══╗  │  桶的容量固定
      │  ║   ║  │
      │  ║░░░║  │  ← 桶中的水(待处理请求)
      │  ║░░░║  │
      │  ╚═══╝  │
      └────┬────┘
           │←── 固定速率流出
           ↓
       处理请求

关键特点

  • 进水不定:请求随时可能来
  • 出水恒定:处理速率固定
  • 水满溢出:超过容量的请求被拒绝

代码实现

public class LeakyBucketRateLimiter {
    private final int capacity;           // 桶的容量
    private final int leakRate;           // 漏出速率(每秒)
    private AtomicInteger water = new AtomicInteger(0);  // 当前水量
    private long lastLeakTime = System.currentTimeMillis();
    
    public LeakyBucketRateLimiter(int capacity, int leakRatePerSecond) {
        this.capacity = capacity;
        this.leakRate = leakRatePerSecond;
    }
    
    public synchronized boolean allowRequest() {
        long now = System.currentTimeMillis();
        
        // 先漏水:计算从上次到现在漏掉了多少水
        long elapsedTime = now - lastLeakTime;
        int leaked = (int) (elapsedTime * leakRate / 1000);
        
        if (leaked > 0) {
            int newWater = Math.max(0, water.get() - leaked);
            water.set(newWater);
            lastLeakTime = now;
        }
        
        // 判断是否还有空间
        if (water.get() < capacity) {
            water.incrementAndGet();
            return true;
        }
        
        return false;  // 桶满了,拒绝请求
    }
}

使用示例

// 桶容量100,每秒漏出10个请求
LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(100, 10);

for (int i = 0; i < 200; i++) {
    if (limiter.allowRequest()) {
        System.out.println("请求" + i + "通过");
        processRequest();
    } else {
        System.out.println("请求" + i + "被拒绝(桶满了)");
    }
    Thread.sleep(50);  // 模拟请求间隔
}

生活例子 🚰

水龙头和水桶

  • 你拧开水龙头(请求涌入),水流忽大忽小
  • 水桶底部有个小孔(固定处理速率)
  • 水龙头流速 > 漏水速度 → 桶会满 → 溢出(拒绝请求)
  • 水龙头流速 < 漏水速度 → 桶不会满 → 所有水都能流出

优点 ✅:

  • 平滑输出:无论输入多大,输出速率恒定
  • 防止突发:能削峰填谷
  • 简单实现:逻辑清晰

缺点 ❌:

  • 无法应对突发:即使系统闲置,也不能快速处理
  • 可能造成等待:请求需要排队

4️⃣ 令牌桶算法(Token Bucket)⭐

核心思想

系统定期发放令牌,有令牌才能处理请求

        定期生成令牌
            ↓↓↓
      ┌─────────┐
      │ ◉ ◉ ◉  │  ← 令牌桶
      │ ◉ ◉ ◉  │     容量固定
      │ ◉ ◉    │
      └────┬────┘
           ↓
        拿令牌 → 处理请求
        没令牌 → 拒绝/等待

关键特点

  • 令牌生成速率固定:比如每秒生成100个
  • 桶有容量上限:比如最多存1000个
  • 可以积攒令牌:闲时积累,忙时使用
  • 支持突发流量:一次性拿多个令牌

代码实现(简化版)

public class TokenBucketRateLimiter {
    private final long capacity;        // 桶的容量
    private final long refillRate;      // 令牌生成速率(每秒)
    private AtomicLong tokens;          // 当前令牌数
    private long lastRefillTime;        // 上次填充时间
    
    public TokenBucketRateLimiter(long capacity, long refillRatePerSecond) {
        this.capacity = capacity;
        this.refillRate = refillRatePerSecond;
        this.tokens = new AtomicLong(capacity);  // 初始满令牌
        this.lastRefillTime = System.currentTimeMillis();
    }
    
    public synchronized boolean allowRequest() {
        return allowRequest(1);
    }
    
    public synchronized boolean allowRequest(int tokensNeeded) {
        // 先补充令牌
        refillTokens();
        
        // 判断令牌是否足够
        if (tokens.get() >= tokensNeeded) {
            tokens.addAndGet(-tokensNeeded);
            return true;
        }
        
        return false;
    }
    
    private void refillTokens() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastRefillTime;
        
        // 计算应该生成的令牌数
        long newTokens = elapsedTime * refillRate / 1000;
        
        if (newTokens > 0) {
            long currentTokens = tokens.get();
            long updatedTokens = Math.min(capacity, currentTokens + newTokens);
            tokens.set(updatedTokens);
            lastRefillTime = now;
        }
    }
    
    // 获取当前可用令牌数
    public long availableTokens() {
        refillTokens();
        return tokens.get();
    }
}

Google Guava实现

import com.google.common.util.concurrent.RateLimiter;

public class GuavaRateLimiterExample {
    public static void main(String[] args) {
        // 每秒生成5个令牌
        RateLimiter rateLimiter = RateLimiter.create(5.0);
        
        for (int i = 0; i < 10; i++) {
            // acquire()会阻塞等待,直到获取到令牌
            double waitTime = rateLimiter.acquire();
            System.out.println("等待了 " + waitTime + " 秒,处理请求" + i);
        }
    }
}

高级特性:预热限流(Warm Up)

// 从初始速率逐渐提升到目标速率
RateLimiter limiter = RateLimiter.create(
    100.0,                    // 目标速率:每秒100个
    3,                        // 预热时间:3秒
    TimeUnit.SECONDS         
);

// 前3秒速率从 100/3 ≈ 33 逐渐提升到 100

应用场景

  • 系统刚启动,缓存还是空的
  • 数据库连接池还没完全建立
  • 需要预热才能达到最佳性能

支持突发流量

// 桶容量1000,每秒生成100个令牌
TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(1000, 100);

// 系统闲置了10秒,积累了1000个令牌
// 突然来了500个请求,可以瞬间处理!
for (int i = 0; i < 500; i++) {
    if (limiter.allowRequest()) {
        processRequest();  // 全部通过!
    }
}

生活例子 🎫

游乐园门票

  • 游乐园每小时发放100张门票(生成令牌)
  • 门票可以积累,最多积累1000张(桶容量)
  • 游客拿着门票就能进入(消费令牌)
  • 早上游客少,门票积累了500张
  • 中午游客突然增多,500个游客可以同时进入!

优点 ✅:

  • 支持突发流量:积累的令牌可以瞬间使用
  • 流量平滑:长期平均速率受控
  • 灵活性高:可以支持预热、动态调整

缺点 ❌:

  • 实现稍复杂:需要定期生成令牌
  • 分布式难:多机器令牌同步困难

🆚 四大算法对比

算法能否应对突发流量平滑度实现复杂度适用场景
固定窗口⭐⭐⭐简单计数限流
滑动窗口⭐⭐⭐⭐⭐精确统计场景
漏桶⭐⭐⭐⭐⭐⭐⭐平滑输出、消息队列
令牌桶⭐⭐⭐⭐⭐⭐互联网API、网关

场景选择决策树

开始
 ├─ 需要应对突发流量?
 │   ├─ 是 → 令牌桶 ⭐⭐⭐
 │   └─ 否 ↓
 │
 ├─ 需要绝对平滑的输出?
 │   ├─ 是 → 漏桶 ⭐⭐⭐
 │   └─ 否 ↓
 │
 ├─ 需要精确统计?
 │   ├─ 是 → 滑动窗口 ⭐⭐⭐
 │   └─ 否 ↓
 │
 └─ 追求极致性能?
     └─ 是 → 固定窗口 ⭐⭐

🌐 分布式限流实现

Redis + Lua实现令牌桶

-- token_bucket.lua
local key = KEYS[1]              -- 令牌桶的key
local capacity = tonumber(ARGV[1])  -- 桶容量
local rate = tonumber(ARGV[2])      -- 生成速率(每秒)
local requested = tonumber(ARGV[3]) -- 请求的令牌数
local now = tonumber(ARGV[4])       -- 当前时间戳

-- 获取当前令牌数和上次更新时间
local token_data = redis.call('hmget', key, 'tokens', 'last_time')
local tokens = tonumber(token_data[1]) or capacity
local last_time = tonumber(token_data[2]) or 0

-- 计算应该生成的令牌数
local elapsed = math.max(0, now - last_time)
local new_tokens = math.min(capacity, tokens + elapsed * rate)

-- 判断令牌是否足够
if new_tokens >= requested then
    new_tokens = new_tokens - requested
    redis.call('hmset', key, 'tokens', new_tokens, 'last_time', now)
    redis.call('expire', key, 60)
    return 1  -- 允许请求
else
    return 0  -- 拒绝请求
end

Java调用代码

@Service
public class RedisRateLimiter {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    private DefaultRedisScript<Long> script;
    
    @PostConstruct
    public void init() {
        script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(
            new ClassPathResource("token_bucket.lua")
        ));
        script.setResultType(Long.class);
    }
    
    public boolean allowRequest(String key, long capacity, long rate, long requested) {
        List<String> keys = Collections.singletonList(key);
        Long result = redisTemplate.execute(
            script,
            keys,
            capacity,
            rate,
            requested,
            System.currentTimeMillis() / 1000
        );
        
        return result != null && result == 1;
    }
}

Sentinel的分布式限流

@Service
public class OrderService {
    
    // 基于QPS的限流
    @SentinelResource(
        value = "createOrder",
        blockHandler = "handleBlock"
    )
    public Order createOrder(OrderRequest request) {
        // 创建订单逻辑
        return orderRepository.save(request.toOrder());
    }
    
    // 限流后的处理方法
    public Order handleBlock(OrderRequest request, BlockException ex) {
        log.warn("订单创建被限流: {}", request);
        throw new BusinessException("系统繁忙,请稍后再试");
    }
}

// 配置限流规则
@Configuration
public class SentinelConfig {
    
    @PostConstruct
    public void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>();
        
        FlowRule rule = new FlowRule();
        rule.setResource("createOrder");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);  // QPS限流
        rule.setCount(100);  // 每秒100个请求
        rule.setStrategy(RuleConstant.STRATEGY_DIRECT);  // 直接拒绝
        
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}

🎯 实战案例

案例1:电商秒杀系统

需求

  • 10万人抢100个商品
  • 保护后端服务不被打垮
  • 给用户友好的提示

方案:多级限流

@RestController
@RequestMapping("/seckill")
public class SeckillController {
    
    // 第一层:网关层限流(令牌桶)
    // 配置:每秒允许1000个请求进入系统
    
    // 第二层:接口层限流(滑动窗口)
    @SentinelResource(
        value = "seckill",
        blockHandler = "seckillBlock"
    )
    @PostMapping("/buy")
    public Result<String> seckill(@RequestParam Long productId,
                                    @RequestParam Long userId) {
        
        // 第三层:热点参数限流
        // 对特定商品ID进行单独限流
        
        // 第四层:Redis库存预扣减
        String stockKey = "stock:" + productId;
        Long stock = redisTemplate.opsForValue().decrement(stockKey);
        
        if (stock < 0) {
            // 回滚
            redisTemplate.opsForValue().increment(stockKey);
            return Result.fail("商品已售罄");
        }
        
        // 异步创建订单
        seckillService.createOrderAsync(productId, userId);
        
        return Result.success("抢购成功,订单生成中...");
    }
    
    // 限流后的友好提示
    public Result<String> seckillBlock(Long productId, Long userId, BlockException ex) {
        log.warn("秒杀被限流: product={}, user={}", productId, userId);
        return Result.fail("人数过多,请稍后再试");
    }
}

案例2:OpenAPI限流

需求

  • 不同用户不同限额
  • VIP用户有更高限额
  • 按小时、按天统计

方案:多维度限流

@Component
public class ApiRateLimiter {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public boolean checkLimit(String apiKey, String api) {
        // 获取用户等级
        UserLevel level = getUserLevel(apiKey);
        
        // 多个维度的限流
        return checkMinuteLimit(apiKey, api, level)
            && checkHourLimit(apiKey, api, level)
            && checkDayLimit(apiKey, api, level);
    }
    
    private boolean checkMinuteLimit(String apiKey, String api, UserLevel level) {
        String key = String.format("limit:minute:%s:%s:%s", 
            apiKey, api, getCurrentMinute());
        
        int limit = level == UserLevel.VIP ? 100 : 10;  // VIP 100次/分钟
        
        Long count = redisTemplate.opsForValue().increment(key);
        redisTemplate.expire(key, 1, TimeUnit.MINUTES);
        
        return count <= limit;
    }
    
    private boolean checkHourLimit(String apiKey, String api, UserLevel level) {
        String key = String.format("limit:hour:%s:%s:%s", 
            apiKey, api, getCurrentHour());
        
        int limit = level == UserLevel.VIP ? 5000 : 500;  // VIP 5000次/小时
        
        Long count = redisTemplate.opsForValue().increment(key);
        redisTemplate.expire(key, 1, TimeUnit.HOURS);
        
        return count <= limit;
    }
    
    private boolean checkDayLimit(String apiKey, String api, UserLevel level) {
        String key = String.format("limit:day:%s:%s:%s", 
            apiKey, api, getCurrentDay());
        
        int limit = level == UserLevel.VIP ? 100000 : 10000;  // VIP 10万次/天
        
        Long count = redisTemplate.opsForValue().increment(key);
        redisTemplate.expire(key, 1, TimeUnit.DAYS);
        
        return count <= limit;
    }
}

💡 最佳实践

1. 限流提示要友好

❌ 糟糕的提示

{
  "code": 429,
  "message": "Too Many Requests"
}

✅ 友好的提示

{
  "code": 429,
  "message": "您的操作过于频繁,请稍后再试",
  "retryAfter": 60,
  "tips": "VIP用户无此限制,升级VIP享更多权益",
  "currentLimit": "10次/分钟",
  "remainingQuota": 0
}

2. 监控和告警

@Component
public class RateLimiterMonitor {
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    public void recordBlock(String resource, String reason) {
        // 记录被限流的次数
        Counter.builder("rate_limiter.blocked")
            .tag("resource", resource)
            .tag("reason", reason)
            .register(meterRegistry)
            .increment();
        
        // 触发告警
        if (getBlockedCount(resource) > 1000) {
            alertService.send("限流告警", 
                resource + " 在1分钟内被限流超过1000次");
        }
    }
}

3. 动态调整限流阈值

@Service
public class DynamicRateLimitService {
    
    @Scheduled(fixedRate = 60000)  // 每分钟检查一次
    public void adjustRateLimit() {
        // 获取当前系统负载
        double cpuUsage = systemMonitor.getCpuUsage();
        double memoryUsage = systemMonitor.getMemoryUsage();
        
        // 根据负载动态调整
        int baseQps = 1000;
        int adjustedQps;
        
        if (cpuUsage > 80 || memoryUsage > 80) {
            // 系统压力大,降低限流阈值
            adjustedQps = (int) (baseQps * 0.7);
        } else if (cpuUsage < 50 && memoryUsage < 50) {
            // 系统空闲,提高限流阈值
            adjustedQps = (int) (baseQps * 1.3);
        } else {
            adjustedQps = baseQps;
        }
        
        updateRateLimitRule("api", adjustedQps);
    }
}

🎓 面试高频问题

Q1:令牌桶和漏桶的区别?

A

对比项令牌桶漏桶
处理速率可以突发,有令牌就能处理恒定速率,严格匀速
令牌/水定期生成令牌定期漏出水
闲时积累令牌,可应对突发无法积累,闲置就浪费
应用API限流、网关消息队列、流量整形

生活比喻

  • 令牌桶:健身房月卡,可以连续来几天,也可以攒着不来
  • 漏桶:传送带,无论你放多少东西,输出速度永远是固定的

Q2:分布式环境下如何限流?

A

方案1:Redis集中式限流

// 优点:全局统一、精确
// 缺点:依赖Redis、有网络开销

方案2:Nginx限流

# 限制每个IP每秒10个请求
limit_req_zone $binary_remote_addr zone=myLimit:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=myLimit burst=20 nodelay;
    }
}

方案3:本地限流 + 动态调整

// 每个节点独立限流
// 通过配置中心动态调整各节点限额
// 优点:性能高、无中心依赖
// 缺点:不够精确

Q3:高并发下如何优化限流性能?

A

  1. 使用原子操作
// 使用AtomicLong代替synchronized
private AtomicLong counter = new AtomicLong(0);
  1. 减少锁粒度
// 分段锁:将限流器拆分为多个
RateLimiter[] limiters = new RateLimiter[10];
int index = Math.abs(userId.hashCode() % 10);
limiters[index].acquire();
  1. 使用本地缓存
// 缓存限流配置,避免频繁查询Redis
LoadingCache<String, RateLimiter> cache = CacheBuilder.newBuilder()
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .build(new CacheLoader<String, RateLimiter>() {
        public RateLimiter load(String key) {
            return RateLimiter.create(getRate(key));
        }
    });

🎉 总结

核心要点回顾

  1. 四大算法

    • 固定窗口:简单但有临界问题
    • 滑动窗口:精确但复杂
    • 漏桶:平滑但无法突发
    • 令牌桶:灵活,最常用 ⭐
  2. 选择依据

    • 需要应对突发?→ 令牌桶
    • 需要绝对平滑?→ 漏桶
    • 需要精确统计?→ 滑动窗口
    • 追求简单高效?→ 固定窗口
  3. 分布式方案

    • Redis + Lua(精确)
    • Nginx限流(入口)
    • 本地限流(高性能)

记忆口诀 📝

限流算法有四种,
令牌漏桶窗口型。

令牌桶里攒令牌,
突发流量能应对。

漏桶恒速很平滑,
输出速率不会变。

滑动窗口最精确,
统计数据很准确。

固定窗口虽简单,
临界问题要小心。

分布式用Redis好,
Lua脚本保原子。

监控告警不能少,
动态调整更灵活!

📚 参考资料

  1. Guava RateLimiter源码
  2. Sentinel限流原理
  3. Token Bucket Algorithm - Wikipedia
  4. Leaky Bucket Algorithm - Wikipedia

最后送你一句话

"限流不是为了拒绝用户,而是为了保护系统,给所有用户更好的体验。"

愿你的系统永不宕机,流量永远丝滑! 🚀✨


表情包时间 🎭

当流量正常时:

😊 系统运行平稳,用户体验良好

当流量突增但有限流:

🛡️ 限流器启动,保护系统稳定

当流量突增但没限流:

💥 服务器:我太难了...(宕机)

当用户被限流时:

😅 "系统繁忙,请稍后再试"
总比看到500错误强吧!