Java并发编程--44-分布式限流:令牌桶与漏桶算法在网关层的落地

分布式限流:令牌桶与漏桶算法在网关层的落地

作者 :Weisian
发布时间:2026年4月

直击痛点

"双十一流量洪峰瞬间冲垮服务器,99%的开发者只知道加机器,却不知道限流 才是成本最低的保命手段;面试官问'令牌桶和漏桶的区别',80%的人只背了'令牌桶允许突发',却说不出分布式环境下如何保证限流精准性才是落地难点。"

分布式限流是微服务架构中最核心的防御机制之一

  • 四大算法:计数器、滑动窗口、漏桶、令牌桶,各有优劣;
  • 三层限流:Nginx层(防DDoS)、网关层(细粒度)、应用层(熔断降级);
  • 分布式难点:如何跨多个节点精准统计流量?Redis原子操作 vs 本地近似限流;
  • 工业实践:Sentinel、Resilience4j、Spring Cloud Gateway限流。

本文将从生活类比 切入,结合底层原理代码实战面试考点 ,彻底讲透分布式限流的设计与落地:

✅ 四大限流算法深度对比(图解+代码);

✅ 单机限流:Guava RateLimiter实战;

✅ 分布式限流:Redis+Lua原子操作实现;

✅ 三层限流架构:Nginx→网关→应用层;

✅ Sentinel集群限流实战;

✅ 面试高频题标准答案(直接背);

✅ 选型指南:不同场景怎么选限流方案。

📌 核心一句话

限流=给系统装流量阀门 ,分布式限流=给整个集群装总阀门;计数器简单但有临界漏洞,滑动窗口解决临界但内存占用高,漏桶强制匀速限流但无法应对突发,令牌桶兼顾平滑和突发是工业首选;分布式限流需用Redis+Lua保证原子性,或使用Sentinel实现集群限流,网关层是分布式限流的最佳落地位置。
📌 面试金句先记牢

  • 计数器算法(固定窗口):固定时间窗口计数,临界时刻流量翻倍,存在超限流风险;
  • 滑动窗口:拆分小窗口解决临界问题,精度更高,复杂度中等;
  • 漏桶算法 :水流匀速流出,强制平滑流量,无法应对突发合理流量;
  • 令牌桶算法 :桶内存令牌,请求拿令牌,支持匀速+突发流量,生产最常用;
  • 分布式限流必须用原子操作(Redis Lua),否则多机并发会超量;
  • 分布式限流关键:Redis+Lua保证原子性,避免竞态条件;
  • 三层限流:Nginx(IP限流,防DDoS) → Gateway(集群全局限流) → Sentinel(接口限流,单机保护)。

一、为什么要限流?

1.1 限流的核心意义:系统的"保命阀门"

生活类比最容易理解:

  • 限流 = 景区限流:景区最大承载5000人,超过就不让进,避免拥挤踩踏;
  • 分布式限流 = 整个景区连锁园区限流:所有园区入口统一管控,总人数不超标。

高并发下不限流的后果

  1. 服务雪崩:一个接口流量暴涨,拖垮整个微服务集群;
  2. 资源耗尽:数据库连接池、线程池、CPU 100%,服务假死;
  3. SLA不达标:核心业务响应超时,用户流失;
  4. 恶意攻击:DDoS流量直接打垮服务器。

1.2 限流的四个维度

维度 说明 示例
QPS限流 限制每秒请求数 1000 QPS
并发线程数限流 限制同时处理的请求数 50并发
IP限流 限制单个IP的访问频率 100次/分钟
用户限流 限制单个用户的访问频率 10次/秒

1.3 分布式限流 vs 单机限流

类型 适用场景 精准性 实现难度
单机限流 单体应用、单实例服务 高(本地计数)
分布式限流 微服务集群、多实例部署、网关统一管控 高(全局计数)

核心痛点

分布式环境下,流量会负载均衡到N台机器,每台机器单独限流=总流量=N倍单机限流,直接导致限流失效!

例:集群3台机器,单机限流100QPS → 集群总限流300QPS,远超预期100QPS。


二、四大限流算法深度对比

2.1 计数器算法(固定窗口)

原理:将时间划分为固定窗口,每个窗口内计数,超过阈值则拒绝。

复制代码
时间线:  |---1s---||---1s---||---1s---|
请求数:    1000      1000      1000
         窗口1      窗口2      窗口3

临界问题

  • 窗口1的最后100ms(999个请求)+ 窗口2的前100ms(999个请求)= 1998个请求
  • 实际1s内通过了1998个请求,远超阈值1000
java 复制代码
/**
 * 计数器限流(有临界问题)
 */
public class CounterRateLimiter {
    private long limit = 1000;  // 每秒限制1000个请求
    private long interval = 1000;  // 窗口大小1秒
     
    private long windowStart = System.currentTimeMillis();    // 起始窗口
    private long count = 0;     // 计数器
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
		// 窗口过期,重置
        if (now - windowStart > interval) {
            // 进入下一个窗口
            windowStart = now;
            count = 0;
        }
        
		// 判断是否超限
        if (count < limit) {
            count++;
            return true;
        }
        return false;
    }
}

2.2 滑动窗口算法

原理

  1. 把大窗口拆分为多个小格子(如1秒拆为10个100ms小窗口);

  2. 每次请求只统计最近1秒的所有小窗口总和;

  3. 随着时间推移,窗口持续滑动,淘汰旧数据。

    时间线: |---|---|---|---|---|---|---|
    格子: 1 2 3 4 5 6 7 8 (每个100ms)
    滑动窗口: [5 6 7 8 9 10 11 12 13 14] → 统计滑动的10个格子

java 复制代码
/**
 * 滑动窗口限流
 */
public class SlidingWindowRateLimiter {
    private int limit = 1000;  // 每秒限制1000个请求
    private int windowSize = 10;  // 拆分为10个格子
    private long slotSize = 100;  // 每个格子100ms
    
    private long[] counters = new long[windowSize];
    private long currentSlot = 0;
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long slotIndex = (now / slotSize) % windowSize;
        
        // 重置过期格子
        if (slotIndex != currentSlot) {
            counters[(int) slotIndex] = 0;
            currentSlot = slotIndex;
        }
        
        // 统计当前窗口总请求数
        long total = 0;
        for (long counter : counters) {
            total += counter;
        }
        
        if (total >= limit) {
            return false;
        }
        
        counters[(int) slotIndex]++;
        return true;
    }
}
优势
  • 解决固定窗口临界问题;
  • 精度可通过小格子数量调整;
  • 实现比漏桶/令牌桶简单。

2.3 漏桶算法(强制平滑流量)

原理:请求先进入漏桶,以恒定速率处理,桶满则拒绝。

生活类比

漏斗装水,无论倒多快,水流速度永远固定,水满就溢出。

复制代码
    请求流入
       ↓
    ┌─────┐
    │ 桶  │  ← 恒定速率流出
    └─────┘
       ↓
     处理

特点:强行平滑突发流量,但无法应对瞬时高峰。

java 复制代码
/**
 * 漏桶限流
 */
public class LeakyBucketRateLimiter {
    private long capacity = 1000;  // 桶容量
    private long rate = 100;  // 每秒处理100个请求
    private long water = 0;  // 当前水量
    private long lastLeakTime = System.currentTimeMillis();
    
    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        
        // 计算漏掉的水量
        long leakCount = (now - lastLeakTime) * rate / 1000;
        water = Math.max(0, water - leakCount);
        lastLeakTime = now;
        
        // 判断是否溢出
        if (water < capacity) {
            water++;
            return true;
        }
        return false;
    }
}

2.4 令牌桶算法(工业首选)

原理

  1. 系统以恒定速率向桶中放入令牌;
  2. 每个请求必须获取1个令牌才能执行;
  3. 桶满后,多余令牌被丢弃;
  4. 桶内有大量令牌时,可瞬间处理突发流量。

生活类比

餐厅发号器,每秒发10个号,号可以存起来,有号就能吃饭,没号就等待;号攒多了,可同时接待多人。

复制代码
    令牌放入
       ↓
    ┌─────┐
    │ 桶  │  ← 请求获取令牌
    └─────┘
       ↓
     处理

特点:允许一定程度的突发(桶容量决定),Guava RateLimiter基于此实现。

java 复制代码
/**
 * 令牌桶限流
 */
public class TokenBucketRateLimiter {
    private long capacity = 1000;  // 桶容量
    private long rate = 100;  // 每秒生成100个令牌
    private long tokens = capacity;  // 当前令牌数
    private long lastRefillTime = System.currentTimeMillis();
    
    public synchronized boolean tryAcquire(int required) {
        long now = System.currentTimeMillis();
        
        // 补充令牌
        long refillCount = (now - lastRefillTime) * rate / 1000;
        tokens = Math.min(capacity, tokens + refillCount);
        lastRefillTime = now;
        
        // 获取令牌
        if (tokens >= required) {
            tokens -= required;
            return true;
        }
        return false;
    }
}
优势
  • 支持匀速+突发流量,最贴合实际业务;
  • 桶容量控制最大突发量;
  • 分布式环境易实现。

2.5 四大算法对比

算法 突发处理 平滑度 复杂度 适用场景
计数器 差(临界问题) 粗略限流
滑动窗口 监控统计
漏桶 无(强行平滑) 恒定速率处理
令牌桶 有(桶容量) 工业通用

三、单机限流:Guava RateLimiter实战

3.1 Guava RateLimiter使用

java 复制代码
import com.google.common.util.concurrent.RateLimiter;

/**
 * Guava令牌桶限流
 */
@Service
@Slf4j
public class GuavaRateLimiterService {
    
    // 每秒生成10个令牌(QPS=10)
    private RateLimiter rateLimiter = RateLimiter.create(10.0);
    
    /**
     * 非阻塞获取
     */
    public boolean tryAcquire() {
        return rateLimiter.tryAcquire();
    }
    
    /**
     * 阻塞获取
     */
    public void acquire() {
        rateLimiter.acquire();
    }
    
    /**
     * 超时获取
     */
    public boolean tryAcquire(long timeout, TimeUnit unit) {
        return rateLimiter.tryAcquire(timeout, unit);
    }
}

/**
 * 使用示例
 */
@RestController
public class DemoController {
    
    @Autowired
    private GuavaRateLimiterService rateLimiterService;
    
    @GetMapping("/api/test")
    public Result test() {
        if (!rateLimiterService.tryAcquire()) {
            return Result.error("系统繁忙,请稍后重试");
        }
        // 业务逻辑
        return Result.success();
    }
}

3.2 核心API

  • RateLimiter.create(qps):创建令牌桶,指定每秒令牌数;
  • acquire():阻塞获取令牌;
  • tryAcquire():非阻塞获取,失败直接返回false;
  • tryAcquire(timeout, unit):超时获取。

3.3 Guava RateLimiter的局限

  • 单机有效:多实例部署时,总限流值 = 实例数 × 单机限流值,超出预期
  • 无集群协调:无法统计全局流量

适用场景

单体应用、单实例服务、无需集群协调的场景。


四、分布式限流:Redis+Lua原子操作

4.1 为什么用 Redis + Lua?

分布式限流核心要求:原子性

  • 多机器同时请求Redis,若先查再改,会出现并发超量问题;
  • Lua脚本可以把多个Redis命令打包为一个原子操作,保证计数精准;
  • Redis单线程执行Lua,无并发竞争,性能极高。

4.2 令牌桶Lua脚本

lua 复制代码
-- Redis令牌桶限流Lua脚本
-- KEYS[1]: 令牌桶Key
-- ARGV[1]: 每秒生成的令牌数
-- ARGV[2]: 桶容量
-- ARGV[3]: 当前时间戳(毫秒)
-- ARGV[4]: 需要的令牌数
-- 返回值: 1-允许, 0-拒绝

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

-- 获取上次时间和当前令牌数
local lastRefillTime = redis.call('hget', key, 'last_time')
local tokens = redis.call('hget', key, 'tokens')

if lastRefillTime == false then
    -- 第一次请求,初始化
    tokens = capacity
    lastRefillTime = now
else
    tokens = tonumber(tokens)
    lastRefillTime = tonumber(lastRefillTime)
    
    -- 计算应该补充的令牌数
    local delta = math.max(0, now - lastRefillTime) * rate
    tokens = math.min(capacity, tokens + delta)
end

-- 判断是否放行
if tokens >= requested then
    -- 扣减令牌
    tokens = tokens - requested
    redis.call('hset', key, 'tokens', tokens)
    redis.call('hset', key, 'last_time', now)
    redis.call('expire', key, 10)  -- 设置过期时间
    return 1
else
    return 0
end

4.3 Java实现Redis分布式限流

java 复制代码
@Component
@Slf4j
public class RedisRateLimiter {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    private static final String LUA_SCRIPT = 
        "local key = KEYS[1] " +
        "local rate = tonumber(ARGV[1]) " +
        "local capacity = tonumber(ARGV[2]) " +
        "local now = tonumber(ARGV[3]) " +
        "local requested = tonumber(ARGV[4]) " +
        "local lastRefillTime = redis.call('hget', key, 'last_time') " +
        "local tokens = redis.call('hget', key, 'tokens') " +
        "if lastRefillTime == false then " +
        "    tokens = capacity " +
        "    lastRefillTime = now " +
        "else " +
        "    tokens = tonumber(tokens) " +
        "    lastRefillTime = tonumber(lastRefillTime) " +
        "    local delta = math.max(0, now - lastRefillTime) * rate " +
        "    tokens = math.min(capacity, tokens + delta) " +
        "end " +
        "if tokens >= requested then " +
        "    tokens = tokens - requested " +
        "    redis.call('hset', key, 'tokens', tokens) " +
        "    redis.call('hset', key, 'last_time', now) " +
        "    redis.call('expire', key, 10) " +
        "    return 1 " +
        "else " +
        "    return 0 " +
        "end";
    
    private DefaultRedisScript<Long> redisScript;
    
    @PostConstruct
    public void init() {
        redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(LUA_SCRIPT);
        redisScript.setResultType(Long.class);
    }
    
    /**
     * 尝试获取令牌
     * @param key 限流Key(如:rate_limit:api:/order/create)
     * @param rate 每秒生成令牌数(QPS)
     * @param capacity 桶容量(允许的突发流量)
     * @param requested 需要的令牌数(通常为1)
     * @return true-允许,false-拒绝
     */
    public boolean tryAcquire(String key, double rate, int capacity, int requested) {
        try {
            long now = System.currentTimeMillis() / 1000;  // 转换为秒
            List<String> keys = Collections.singletonList(key);
            
            Long result = redisTemplate.execute(
                redisScript,
                keys,
                String.valueOf(rate),
                String.valueOf(capacity),
                String.valueOf(now),
                String.valueOf(requested)
            );
            
            return result != null && result == 1;
            
        } catch (Exception e) {
            log.error("Redis限流异常", e);
            // 异常时放行(熔断降级)
            return true;
        }
    }
    
    /**
     * 简化方法:QPS限流
     */
    public boolean tryAcquire(String key, int qps) {
        return tryAcquire(key, qps, qps, 1);
    }
}

/**
 * 限流注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    String key() default "";
    int qps() default 100;
    int capacity() default 100;
}

/**
 * 限流AOP切面
 */
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
    
    @Autowired
    private RedisRateLimiter rateLimiter;
    
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) 
            throws Throwable {
        
        // 获取限流Key
        String key = rateLimit.key();
        if (StringUtils.isEmpty(key)) {
            // 从请求中获取:方法名+用户ID/IP
            key = getKeyFromRequest(joinPoint);
        }
        
        // 限流判断
        if (!rateLimiter.tryAcquire(key, rateLimit.qps(), rateLimit.capacity(), 1)) {
            log.warn("限流拦截, key: {}", key);
            return Result.error("系统繁忙,请稍后重试");
        }
        
        return joinPoint.proceed();
    }
    
    private String getKeyFromRequest(ProceedingJoinPoint joinPoint) {
        // 获取请求IP、用户ID等
        HttpServletRequest request = getCurrentRequest();
        String ip = request.getRemoteAddr();
        String method = joinPoint.getSignature().toShortString();
        return "rate_limit:" + method + ":" + ip;
    }
}

/**
 * 使用示例
 */
@RestController
public class OrderController {
    
    @PostMapping("/order/create")
    @RateLimit(key = "create_order", qps = 100, capacity = 200)
    public Result createOrder(@RequestBody OrderRequest request) {
        // 业务逻辑
        return Result.success();
    }
}

五、三层限流架构

5.1 架构图

复制代码
客户端请求
    ↓
┌─────────────────────────────────────────┐
│  第一层:Nginx(IP限流,防DDoS)          │
│  - limit_req_zone: 100r/s per IP        │
└─────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────┐
│  第二层:Spring Cloud Gateway(用户限流) │
│  - Redis令牌桶: 1000 QPS per user       │
│  - 按API分组限流                         │
└─────────────────────────────────────────┘
    ↓
┌─────────────────────────────────────────┐
│  第三层:Sentinel(接口熔断降级)         │
│  - QPS限流、并发线程数限流               │
│  - 熔断降级、热点参数限流                │
└─────────────────────────────────────────┘
    ↓
后端服务

分布式系统必须做多层限流,层层防护,避免单点失效:

层级 组件 作用 粒度
接入层 Nginx/OpenResty 防DDoS、IP粗粒度限流
网关层 Spring Cloud Gateway/APISIX/Kong 集群全局限流、用户/接口限流
应用层 Sentinel/Resilience4j 单机保护、熔断降级 极细
资源层 Semaphore/连接池 数据库/线程池限流 本地

5.2 Nginx层限流

nginx 复制代码
# nginx.conf
http {
    # 定义限流区域(10MB内存,按IP限流)
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
    
    server {
        location /api/ {
            # 使用限流(burst=20允许排队,nodelay立即响应)
            limit_req zone=mylimit burst=20 nodelay;
            
            # 限流时返回503
            limit_req_status 503;
            
            proxy_pass http://backend;
        }
    }
}

5.3 Spring Cloud Gateway限流

分布式限流最佳落地位置,为什么选择网关层?

  1. 统一入口:所有流量必须经过网关,一次配置,全集群生效;
  2. 业务无侵入:无需修改业务代码,专注流量管控;
  3. 精细化维度:支持IP、用户ID、接口、设备ID限流;
  4. 提前拦截:流量在网关层被拒绝,不会进入后端服务,节省资源。
yaml 复制代码
# application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: order_route
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@ipKeyResolver}"
                redis-rate-limiter.replenishRate: 100   # 每秒令牌数
                redis-rate-limiter.burstCapacity: 200   # 桶容量
java 复制代码
@Bean
public KeyResolver ipKeyResolver() {
    return exchange -> Mono.just(
        exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
    );
}

@Bean
public KeyResolver userKeyResolver() {
    return exchange -> {
        String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
        return Mono.just(userId != null ? userId : "anonymous");
    };
}

5.4 Sentinel应用层限流

java 复制代码
@Service
@Slf4j
public class SentinelService {
    
    /**
     * 使用注解定义限流规则
     */
    @SentinelResource(
        value = "createOrder",
        blockHandler = "blockHandler",
        fallback = "fallbackHandler"
    )
    public Result createOrder(OrderRequest request) {
        // 业务逻辑
        return Result.success();
    }
    
    /**
     * 限流降级处理
     */
    public Result blockHandler(OrderRequest request, BlockException ex) {
        log.warn("限流触发: {}", ex.getMessage());
        return Result.error("系统繁忙,请稍后重试");
    }
    
    /**
     * 异常降级处理
     */
    public Result fallbackHandler(OrderRequest request, Throwable ex) {
        log.error("业务异常: {}", ex.getMessage());
        return Result.error("服务异常,请稍后重试");
    }
}

/**
 * 动态限流规则配置
 */
@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);
        rule.setCount(100);  // QPS限制100
        rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        rule.setWarmUpPeriodSec(10);  // 预热时间10秒
        
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}

六、分布式限流的高级策略

6.1 热点参数限流

java 复制代码
@Service
public class HotKeyLimitService {
    
    /**
     * 热点参数限流:同一商品ID限流10 QPS
     */
    @SentinelResource(value = "queryProduct")
    @SentinelHotSpot(
        resource = "queryProduct",
        rules = @HotSpotRule(parameterIdx = 0, count = 10)
    )
    public Product queryProduct(Long productId) {
        return productMapper.selectById(productId);
    }
}

6.2 动态限流规则(基于Apollo/Nacos)

java 复制代码
@Component
@Slf4j
public class DynamicRateLimitConfig {
    
    @Autowired
    private RedisRateLimiter rateLimiter;
    
    // 从配置中心动态获取限流值
    @Value("${rate.limit.order.create:100}")
    private int orderCreateQps;
    
    @NacosConfigListener(dataId = "rate-limit-config", group = "DEFAULT_GROUP")
    public void onConfigChange(String config) {
        JSONObject configJson = JSON.parseObject(config);
        orderCreateQps = configJson.getIntValue("order.create.qps");
        log.info("限流配置更新: order.create.qps = {}", orderCreateQps);
    }
    
    public boolean checkRateLimit(String key) {
        return rateLimiter.tryAcquire(key, orderCreateQps);
    }
}

七、面试高频真题(标准答案直接背)

Q1:令牌桶和漏桶的区别是什么?

答案

维度 令牌桶 漏桶
突发处理 允许突发(桶容量决定) 不允许突发,强行平滑
输出速率 平均速率 + 突发 恒定速率
实现方式 周期性放令牌 请求入桶,恒速率流出
适用场景 通用(Guava、Sentinel) 恒定流量(数据库限流)

关键区别:令牌桶允许一定程度的突发,漏桶完全平滑。

Q2:分布式限流为什么需要Lua脚本?

答案
原因

  1. 分布式环境需要全局统一计数,Redis提供集中存储;
  2. 多机并发请求时,Lua脚本保证多个Redis命令原子执行,避免超量;
  3. Redis单线程执行,无竞争,性能极高;
  4. 内存操作,响应时间毫秒级,不影响网关性能。

问题示例

复制代码
实例A:读取令牌数=10 → 计算后=9 → 准备写入
实例B:读取令牌数=10 → 计算后=9 → 写入
实例A:写入9
结果:10个请求通过了20个 → 限流失效

解决方案:Redis+Lua脚本保证整个操作原子执行,单线程无竞态。

Q3:如何设计一个支持动态调整规则的分布式限流系统?

答案
架构设计

  1. 配置中心:Apollo/Nacos存储限流规则(QPS、桶容量)
  2. 规则监听:客户端监听配置变更,动态更新限流器
  3. 限流执行:Redis+Lua令牌桶
  4. 监控告警:限流次数、拒绝率上报监控系统

核心代码

java 复制代码
@NacosConfigListener
public void onRuleChange(String config) {
    RateLimitRule newRule = parseRule(config);
    // 更新限流器配置
    rateLimiter.updateRule(newRule);
}

Q4:Sentinel和Guava RateLimiter有什么区别?

答案

维度 Guava RateLimiter Sentinel
范围 单机 分布式(支持集群)
功能 仅限流 限流+熔断+降级+热点
规则管理 代码配置 动态配置(Apollo/Nacos)
监控 控制台实时监控
适用 单体应用 微服务架构

Q5:Nginx、Gateway、Sentinel三层限流怎么分工?

答案

层级 作用 粒度 策略
Nginx 防DDoS IP 单IP 10r/s
Gateway 用户限流 用户/API 每用户1000 QPS
Sentinel 应用保护 接口/方法 接口200 QPS + 熔断

原则:层层递减,外层粗粒度防攻击,内层细粒度保核心。

  1. 网关层:统一入口、全局精准、业务无侵入,拦截无效流量;
  2. 应用层:单机保护、熔断降级,防止自身资源耗尽;
  3. 生产必须两者结合,网关拦大部分,应用做兜底。

Q6:限流阈值怎么设置?

答案
计算公式

复制代码
单机QPS上限 = (CPU核数 × 1000) / 平均响应时间(ms)
集群QPS上限 = 单机QPS上限 × 实例数 × 0.7(预留30%buffer)

压测确定

  1. 压力测试找到系统瓶颈
  2. 设定阈值为瓶颈值的70%
  3. 预留弹性空间应对突发

Q7:设计一个支持动态调整规则的分布式限流系统

答案

  1. 存储层:MySQL存储限流规则(接口、QPS、容量、维度);
  2. 配置中心:Nacos/Apollo推送动态规则,支持实时生效;
  3. 限流核心:Redis+Lua实现令牌桶/漏桶,保证原子性;
  4. 网关层:Spring Cloud Gateway拉取规则,执行限流;
  5. 本地缓存:Caffeine缓存规则,减少配置中心请求;
  6. 监控告警:Prometheus监控限流次数,超阈值告警;
  7. 兜底策略:Redis宕机时,开启本地Guava限流降级。

八、避坑指南:分布式限流90%的人都踩过

  1. 不用Lua脚本,导致超量限流
    先GET再SET,多机并发会出现计数错误,必须用Lua原子操作
  2. 固定窗口临界问题
    核心业务不用固定窗口,改用滑动窗口或令牌桶。
  3. Redis Key不过期
    冷数据残留占用内存,必须设置EXPIRE。
  4. 只做单机限流,不做分布式
    集群环境下,单机限流=无效限流,总流量会超标。
  5. 漏桶用在秒杀场景
    漏桶强制匀速,秒杀突发流量会被错误拒绝。
  6. 网关层不做降级
    Redis宕机时,限流服务不可用,必须开启本地降级兜底
  7. 不区分维度限流
    全部接口用同一规则,核心接口被非核心接口拖垮。

总结

1. 核心知识点速记口诀

复制代码
四大算法要记牢,计数器简单有临界,
滑动窗口细粒度,漏桶平滑突发出,
令牌桶是工业王,允许突发平滑流。

单机限流用Guava,分布式Redis+Lua,
三层架构层层防,Nginx网关Sentinel。

面试常问令牌桶,突发平滑都能做,
Lua脚本保原子,分布式下不出错。

2. 核心要点回顾

  1. 四大算法:计数器(有临界)、滑动窗口(解决临界)、漏桶(强行平滑)、令牌桶(兼顾突发)
  2. 单机限流:Guava RateLimiter,令牌桶实现
  3. 分布式限流:Redis+Lua保证原子性
  4. 三层架构:Nginx(IP) → Gateway(用户) → Sentinel(接口)
  5. Sentinel:限流+熔断+降级,工业级方案

3. 实战建议

  • 简单场景:Guava RateLimiter
  • 分布式场景:Redis+Lua令牌桶
  • 微服务架构:Sentinel(推荐)
  • 网关层:Spring Cloud Gateway + Redis限流
  • 防DDoS :Nginx limit_req
  • 秒杀/营销活动网关令牌桶限流+Sentinel降级
  • 第三方API调用漏桶算法,保证匀速请求;

写在最后

从计数器的临界问题到令牌桶的平滑突发,限流算法的演进史就是在精准度和复杂度之间不断权衡的过程。单机用Guava,分布式用Redis+Lua,微服务用Sentinel------没有银弹,只有场景适配。

很多开发者以为限流就是加个计数器,结果临界时刻流量翻倍打垮系统;有人过度设计分布式限流,用重量级方案解决简单问题。记住:Nginx防DDoS,网关限用户,Sentinel保核心,三层防线层层递进,才能在高并发下立于不败之地。

如果觉得有帮助,欢迎点赞、收藏、转发!

相关推荐
SamDeepThinking1 小时前
秒杀系统怎么区分真实用户和黄牛脚本?
java·后端·架构
2301_792674861 小时前
java学习day31(redis)
java·redis·学习
小碗羊肉1 小时前
【从零开始学Java | 第四十一篇】深入多线程
java·开发语言
xuhaoyu_cpp_java1 小时前
MyBatis学习(一)
java·经验分享·笔记·学习·mybatis
wuxinyan1232 小时前
Java面试题50:Kubernetes 全栈知识体系之一
java·kubernetes·面试题
不吃肥肉的傲寒2 小时前
Graphify安装与结合claude code使用指南
java·python·ai编程·图搜索
seven97_top2 小时前
Tomcat的架构设计和启动过程详解
java·tomcat
我是无敌小恐龙2 小时前
Java SE 零基础入门 Day05 类与对象核心详解(封装+构造方法+内存+变量)
java·开发语言·人工智能·python·机器学习·计算机视觉·数据挖掘
va学弟2 小时前
Agent入门开发(2):个性化功能添加
java·服务器·ai