高并发下如何优雅限流?4种方案保护你的SpringBoot应用

69 阅读12分钟

《高并发下如何优雅限流?4种方案保护你的SpringBoot应用》

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

每天5分钟,掌握一个SpringBoot核心知识点。大家好,我是SpringBoot指南的小坏。前四期我们聊了自动配置、异常处理、配置管理和接口文档,今天来聊聊一个更"刺激"的话题——如何防止你的系统被流量打爆?

📊 先看数据:为什么你的系统需要限流?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

真实案例:某电商平台去年双十一,0点瞬间涌入 1200万 用户,其中:

  • 正常用户:1180万
  • 恶意爬虫:15万
  • 刷单程序:5万

如果没有限流

  • 数据库连接池撑爆(1000个连接瞬间占满)
  • CPU飙升至100%,持续10分钟
  • 核心服务全部宕机
  • 直接损失:500万+ 订单

有了限流之后

  • 核心服务正常运行
  • 异常请求被优雅拒绝
  • 用户体验几乎无影响
  • 节省成本:80%+ 的服务器资源

今天,我就带你用 4种方案 为你的系统穿上"防弹衣"!

一、限流的本质:4种算法快速理解

在开始代码之前,先搞懂这4种核心算法:

1. 固定窗口(计数器)

比喻:电影院售票窗口,每分钟卖100张票

// 伪代码理解
if (当前分钟请求数 < 100) {
    允许通过;
} else {
    拒绝请求;
}

缺点:窗口切换时可能双倍流量(如59秒和1秒)

2. 滑动窗口

比喻:移动的时间窗口,统计最近1分钟请求

// 伪代码理解
窗口 = [第1秒, 第2秒, ..., 第60秒];  // 记录每秒请求数
当前请求数 = 最近60秒的请求总和;
if (当前请求数 < 阈值) {
    允许通过;
}

优点:更平滑,解决临界问题

3. 令牌桶算法(最常用)

比喻:一个桶,以固定速率放令牌,请求需要拿到令牌才能通过

// 伪代码理解
if (桶里有令牌) {
    取走一个令牌;
    允许通过;
} else {
    拒绝请求;
}

// 后台线程:每秒往桶里放10个令牌

4. 漏桶算法

比喻:一个漏水的桶,请求像水一样流入,以固定速率流出

// 伪代码理解
if (桶没满) {
    请求入桶排队;
    等待被处理;  // 以固定速率流出
} else {
    拒绝请求;    // 水溢出
}

理解了原理,我们开始实战!

二、方案一:Guava RateLimiter(单机首选)

2.1 5分钟快速上手

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

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    
    // 创建限流器:每秒最多处理10个请求
    private RateLimiter rateLimiter = RateLimiter.create(10.0);
    
    @PostMapping("/create")
    public ResponseEntity<String> createOrder(@RequestBody OrderRequest request) {
        // 1. 尝试获取令牌(非阻塞)
        if (!rateLimiter.tryAcquire()) {
            return ResponseEntity.status(429)
                .body("请求太频繁,请稍后再试");
        }
        
        // 2. 执行业务逻辑
        return ResponseEntity.ok("订单创建成功");
    }
}

2.2 高级用法:预热模式

// 预热模式:系统启动时逐渐增加处理能力
// 参数说明:每秒10个请求,预热时间5秒
private RateLimiter warmupLimiter = RateLimiter.create(10.0, 5, TimeUnit.SECONDS);

@GetMapping("/detail/{id}")
public String getDetail(@PathVariable String id) {
    // 获取令牌,如果当前速率不够,会等待
    double waitTime = warmupLimiter.acquire();
    
    if (waitTime > 1.0) {
        log.warn("请求等待时间过长: {}秒", waitTime);
    }
    
    return "商品详情";
}

2.3 按用户限流:不同用户不同限制

@Service
public class UserRateLimitService {
    
    // 为每个用户维护一个限流器
    private Map<Long, RateLimiter> userLimiters = new ConcurrentHashMap<>();
    
    public boolean allowRequest(Long userId) {
        // 获取或创建用户的限流器
        RateLimiter limiter = userLimiters.computeIfAbsent(userId, 
            id -> RateLimiter.create(5.0)); // 每个用户每秒5次
        
        return limiter.tryAcquire();
    }
    
    // 定期清理不活跃的用户
    @Scheduled(fixedRate = 600000) // 每10分钟
    public void cleanUp() {
        userLimiters.entrySet().removeIf(entry -> 
            // 这里可以添加判断逻辑,比如用户最近是否活跃
            isUserInactive(entry.getKey())
        );
    }
}

适用场景:单机应用、快速验证、开发测试

三、方案二:Redisson分布式限流(集群必选)

3.1 为什么需要分布式限流?

假设你有3台服务器,每台限流10QPS:

  • Guava方案:每台都能处理10个,总共30个 → 超过预期!
  • Redisson方案:3台共享一个计数器,总共10个 → 符合预期!

3.2 2步集成Redisson

步骤1:添加依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.2</version>
</dependency>

步骤2:配置文件

spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0

3.3 3种分布式限流实现

@Service
public class DistributedRateLimitService {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 方法1:固定窗口(简单计数器)
     * @param key 限流key(如:接口名+用户ID)
     * @param windowSize 窗口大小(秒)
     * @param limit 限制次数
     */
    public boolean tryAcquireFixed(String key, int windowSize, int limit) {
        String redisKey = "rate:limit:" + key;
        
        // 使用Redis的INCR和EXPIRE
        RAtomicLong counter = redissonClient.getAtomicLong(redisKey);
        
        // 第一次访问,设置过期时间
        if (counter.get() == 0) {
            counter.expire(windowSize, TimeUnit.SECONDS);
        }
        
        // 增加计数并检查
        long count = counter.incrementAndGet();
        return count <= limit;
    }
    
    /**
     * 方法2:滑动窗口(更精确)
     */
    public boolean tryAcquireSliding(String key, int windowSize, int limit) {
        long now = System.currentTimeMillis();
        long windowMillis = windowSize * 1000L;
        
        // 使用ZSet存储请求时间戳
        RScoredSortedSet<String> window = redissonClient.getScoredSortedSet(key);
        
        // 删除窗口外的旧记录
        window.removeRangeByScore(0, true, now - windowMillis, true);
        
        // 检查当前窗口内请求数
        if (window.size() < limit) {
            window.add(now, UUID.randomUUID().toString());
            window.expire(windowSize + 1, TimeUnit.SECONDS);
            return true;
        }
        
        return false;
    }
    
    /**
     * 方法3:Redisson内置限流器(推荐)
     */
    public boolean tryAcquireRRateLimiter(String key, int permitsPerSecond) {
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        
        // 初始化:1秒内允许的请求数
        rateLimiter.trySetRate(RateType.OVERALL, permitsPerSecond, 1, RateIntervalUnit.SECONDS);
        
        return rateLimiter.tryAcquire();
    }
}

// 使用示例
@RestController
public class ApiController {
    
    @Autowired
    private DistributedRateLimitService rateLimitService;
    
    @GetMapping("/api/products")
    public ResponseEntity<?> getProducts(@RequestParam String keyword,
                                         @RequestHeader("X-User-Id") String userId) {
        
        // 用户维度限流:每个用户每秒10次
        String userKey = "product:search:" + userId;
        if (!rateLimitService.tryAcquireRRateLimiter(userKey, 10)) {
            return ResponseEntity.status(429)
                .body(Map.of("code", 429, "message", "搜索太频繁了,歇会儿吧"));
        }
        
        // IP维度限流:每个IP每分钟60次
        String ip = getClientIp();
        String ipKey = "product:search:ip:" + ip;
        if (!rateLimitService.tryAcquireFixed(ipKey, 60, 60)) {
            return ResponseEntity.status(429)
                .body(Map.of("code", 429, "message", "IP请求超限"));
        }
        
        // 执行业务逻辑
        return ResponseEntity.ok(searchService.search(keyword));
    }
}

适用场景:微服务集群、分布式系统、生产环境

四、方案三:Sentinel(阿里生产级方案)

4.1 Sentinel vs 其他方案

特性GuavaRedissonSentinel
分布式
实时监控
规则持久化
熔断降级
系统保护
控制台

4.2 10分钟搭建Sentinel

步骤1:添加依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2022.0.0.0</version>
</dependency>

步骤2:基础配置

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080  # Sentinel控制台
        port: 8719
      eager: true  # 立即初始化

步骤3:代码中使用

@RestController
public class PaymentController {
    
    // 1. 定义资源
    @SentinelResource(
        value = "createPayment",
        blockHandler = "createPaymentBlocked",  // 限流处理
        fallback = "createPaymentFallback"     // 异常降级
    )
    @PostMapping("/payment")
    public PaymentResult createPayment(@RequestBody PaymentRequest request) {
        // 业务逻辑
        return paymentService.process(request);
    }
    
    // 2. 限流处理方法
    public PaymentResult createPaymentBlocked(PaymentRequest request, 
                                              BlockException ex) {
        return PaymentResult.fail("系统繁忙,请稍后重试");
    }
    
    // 3. 降级处理方法
    public PaymentResult createPaymentFallback(PaymentRequest request, 
                                               Throwable throwable) {
        return PaymentResult.fail("支付服务暂时不可用");
    }
}

4.3 Sentinel控制台:可视化配置

启动控制台(Docker方式):

docker run --name sentinel -p 8858:8858 \
  -d bladex/sentinel-dashboard:1.8.6

访问:http://localhost:8858 (账号/密码:sentinel/sentinel)

控制台功能

  1. 实时监控:查看QPS、响应时间、异常数
  2. 规则管理:在线配置限流规则、熔断规则
  3. 集群限流:支持集群维度的流量控制
  4. 热点规则:针对特定参数的限流
  5. 系统保护:CPU、负载、线程数保护

4.4 实战:电商秒杀场景

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

@Service
public class SeckillService {
    
    // 秒杀商品维度限流
    @SentinelResource(
        value = "seckillItem",
        blockHandler = "seckillBlocked"
    )
    public SeckillResult seckill(Long itemId, Long userId) {
        // 1. 检查库存
        if (!checkStock(itemId)) {
            return SeckillResult.fail("库存不足");
        }
        
        // 2. 扣减库存
        reduceStock(itemId);
        
        // 3. 创建订单
        return createOrder(itemId, userId);
    }
    
    // 限流处理:返回排队中
    public SeckillResult seckillBlocked(Long itemId, Long userId, 
                                        BlockException ex) {
        // 记录到队列,异步处理
        queueService.addToQueue(itemId, userId);
        
        return SeckillResult.queue("排队中,请等待...");
    }
}

// Sentinel规则配置(可以在控制台动态调整)
/*
限流规则:
- 资源名:seckillItem
- 阈值类型:QPS
- 单机阈值:1000
- 流控模式:直接
- 流控效果:快速失败

降级规则:
- 资源名:seckillItem  
- 熔断策略:慢调用比例
- 最大RT:100ms
- 比例阈值:0.5
- 熔断时长:5s
*/

适用场景:大型电商、金融系统、需要精细化控制的场景

五、方案四:Resilience4j(轻量级选择)

5.1 为什么选择Resilience4j?

如果你想要:

  • ✅ 比Sentinel更轻量
  • ✅ 函数式编程风格
  • ✅ 与Spring Boot完美集成
  • ✅ 支持熔断、限流、重试、隔离

那么Resilience4j是你的菜!

5.2 快速集成

步骤1:添加依赖

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>2.1.0</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
    <version>2.1.0</version>
</dependency>

步骤2:配置文件

resilience4j:
  ratelimiter:
    instances:
      userService:
        limit-for-period: 10      # 每个周期允许的请求数
        limit-refresh-period: 1s  # 周期长度
        timeout-duration: 500ms   # 等待超时时间

步骤3:使用注解

@RestController
public class UserController {
    
    // 1. 最简单的使用方式
    @RateLimiter(name = "userService")
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // 2. 自定义降级方法
    @RateLimiter(name = "userService", fallbackMethod = "getUserFallback")
    @GetMapping("/users/v2/{id}")
    public User getUserV2(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    // 降级方法
    private User getUserFallback(Long id, RequestNotPermitted ex) {
        log.warn("用户查询被限流,id: {}", id);
        return User.defaultUser();  // 返回默认用户
    }
}

5.3 组合使用:熔断 + 限流 + 重试

@Service
public class PaymentService {
    
    // 组合拳:先熔断,再限流,失败重试
    @CircuitBreaker(name = "paymentService")
    @RateLimiter(name = "paymentService")  
    @Retry(name = "paymentService")
    @Bulkhead(name = "paymentService")  // 隔离舱
    public PaymentResult pay(PaymentRequest request) {
        // 调用第三方支付
        return thirdPartyPaymentService.pay(request);
    }
    
    // 熔断降级
    public PaymentResult payFallback(PaymentRequest request, Exception ex) {
        // 1. 记录到本地,后续补偿
        // 2. 返回友好提示
        return PaymentResult.fail("支付服务暂时不可用,请稍后重试");
    }
}

// 配置示例
/*
resilience4j:
  circuitbreaker:
    instances:
      paymentService:
        failure-rate-threshold: 50        # 失败率阈值
        wait-duration-in-open-state: 10s  # 熔断后等待时间
  
  ratelimiter:
    instances:
      paymentService:
        limit-for-period: 100
        limit-refresh-period: 1s
  
  retry:
    instances:
      paymentService:
        max-attempts: 3          # 最大重试次数
        wait-duration: 500ms     # 重试间隔
  
  bulkhead:
    instances:
      paymentService:
        max-concurrent-calls: 10  # 最大并发数
*/

适用场景:需要多种容错机制、函数式编程偏好、轻量级集成

六、实战:电商系统限流架构设计

6.1 四层限流防护体系

第一层:Nginx网关层
  ↓ 全局QPS限制,防DDoS
第二层:应用入口层  
  ↓ 接口级别限流,用Sentinel
第三层:业务服务层
  ↓ 用户级别限流,用Redisson
第四层:方法级别
  ↓ 关键方法保护,用Resilience4j

6.2 具体配置示例

# 1. Nginx层限流
# nginx.conf
http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
    
    server {
        location /api/ {
            limit_req zone=api burst=50 nodelay;
            proxy_pass http://backend;
        }
    }
}

# 2. Spring Boot配置
# application.yml
sentinel:
  transport:
    dashboard: sentinel-dashboard:8858
  
  # 流控规则
  datasource:
    flow:
      nacos:
        server-addr: nacos:8848
        data-id: ${spring.application.name}-flow-rules
        rule-type: flow

# 3. Redis分布式限流配置
redis:
  rate-limit:
    # 用户维度
    user:
      window-size: 60  # 60秒
      max-requests: 100
    # IP维度  
    ip:
      window-size: 3600  # 1小时
      max-requests: 1000

6.3 核心代码实现

@Component
public class MultiLevelRateLimiter {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 多层限流检查
     */
    public RateLimitResult checkRateLimit(HttpServletRequest request, 
                                         String apiPath, 
                                         Long userId) {
        // 1. 全局频率检查(防刷)
        if (!checkGlobalRate(request)) {
            return RateLimitResult.globalLimit();
        }
        
        // 2. IP频率检查
        if (!checkIpRate(request)) {
            return RateLimitResult.ipLimit();
        }
        
        // 3. 用户频率检查
        if (!checkUserRate(userId)) {
            return RateLimitResult.userLimit();
        }
        
        // 4. 接口频率检查
        if (!checkApiRate(apiPath, userId)) {
            return RateLimitResult.apiLimit();
        }
        
        return RateLimitResult.success();
    }
    
    private boolean checkGlobalRate(HttpServletRequest request) {
        String key = "rate:global:" + getMinuteKey();
        Long count = redisTemplate.opsForValue().increment(key, 1);
        
        if (count == 1) {
            redisTemplate.expire(key, 70, TimeUnit.SECONDS); // 多10秒缓冲
        }
        
        return count <= 10000; // 全局每分钟1万次
    }
    
    private boolean checkIpRate(HttpServletRequest request) {
        String ip = getClientIp(request);
        String key = "rate:ip:" + ip + ":" + getHourKey();
        
        Long count = redisTemplate.opsForValue().increment(key, 1);
        
        if (count == 1) {
            redisTemplate.expire(key, 3660, TimeUnit.SECONDS);
        }
        
        return count <= 1000; // 每个IP每小时1000次
    }
    
    private boolean checkUserRate(Long userId) {
        if (userId == null) return true;
        
        String key = "rate:user:" + userId + ":" + getMinuteKey();
        Long count = redisTemplate.opsForValue().increment(key, 1);
        
        if (count == 1) {
            redisTemplate.expire(key, 70, TimeUnit.SECONDS);
        }
        
        return count <= 60; // 每个用户每分钟60次
    }
    
    private String getMinuteKey() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
    }
    
    private String getHourKey() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
    }
}

七、如何选择?一张图告诉你

graph TD
    A[需要限流吗?] --> B{场景选择};
    B --> C[单机应用/测试环境];
    B --> D[分布式系统/生产环境];
    B --> E[复杂场景/需要监控];
    B --> F[微服务/云原生];
    
    C --> G[Guava RateLimiter<br/>简单快速];
    D --> H[Redisson<br/>分布式限流];
    E --> I[Sentinel<br/>生产级方案];
    F --> J[Resilience4j<br/>轻量级容错];
    
    style G fill:#ccf,stroke:#333
    style H fill:#ccf,stroke:#333  
    style I fill:#ccf,stroke:#333
    style J fill:#ccf,stroke:#333

选择建议

  1. 个人项目/测试 → 用 Guava,5分钟搞定
  2. 中小型分布式系统 → 用 Redisson,分布式支持
  3. 大型电商/金融系统 → 用 Sentinel,功能全面
  4. 微服务/云原生 → 用 Resilience4j,轻量集成

八、避坑指南

坑1:限流key设计不合理

// ❌ 错误:所有用户共用一个key
String key = "api:limit:getUser";

// ✅ 正确:按用户区分
String key = "api:limit:getUser:" + userId;

// ✅ 更细粒度:按用户+接口+时间
String key = String.format("api:limit:%s:%s:%s", 
    userId, apiName, getMinuteKey());

坑2:忘记设置过期时间

// ❌ 错误:Redis key永不过期,内存泄漏!
redisTemplate.opsForValue().increment(key);

// ✅ 正确:设置合适的过期时间
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, windowSize + 10, TimeUnit.SECONDS); // 多10秒缓冲

坑3:单点瓶颈

// ❌ 错误:所有请求都访问同一个Redis节点
String key = "rate:limit:global";

// ✅ 正确:使用分片或集群
String key = "rate:limit:global:" + (userId % 10); // 分10个key

坑4:忽略用户体验

// ❌ 错误:直接返回"系统繁忙"
return ResponseEntity.status(429).body("系统繁忙");

// ✅ 正确:友好提示 + 建议重试时间
long retryAfter = 60; // 60秒后重试
return ResponseEntity.status(429)
    .header("Retry-After", String.valueOf(retryAfter))
    .body(Map.of(
        "code", 429,
        "message", "请求太频繁啦,休息一下再试试",
        "retryAfter", retryAfter
    ));

九、今日小结

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。

今天我们学习了4种限流方案:

  1. Guava RateLimiter - 单机快速上手
  2. Redisson分布式限流 - 集群环境必备
  3. Sentinel - 生产级全功能方案
  4. Resilience4j - 轻量级容错库

核心要点

  • 限流不只是防刷,更是保护系统的"保险丝"
  • 从简单方案开始,根据业务复杂度升级
  • 一定要监控和告警,不要"黑盒"运行
  • 考虑用户体验,给出明确的错误提示

🤔 哥哥们今日思考题?

如果你的社交APP突然爆火,日活从10万暴涨到1000万,你会如何设计限流策略?考虑以下维度:

  1. 新用户注册(防止机器注册)
  2. 消息发送(防止刷屏)
  3. 图片上传(防止带宽打满)
  4. API调用(防止接口被刷)

欢迎在评论区分享你的设计方案!


下期预告:《SpringBoot日志:从打印到ELK的完整方案》—— 让你的日志不再只是"printf"!

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java 回复"Swagger源码",获取本文所有示例代码、配置模板及导出工具。