秒杀活动开始,瞬间涌入10万QPS,系统直接被打挂------这就是没有限流的后果。本文将揭秘大厂如何通过网关层、应用层、Redis层、数据库层的多层限流架构,让系统在面对突发流量时稳如泰山。

文章目录
-
- 一、场景引入:一次秒杀活动的系统崩溃
-
- [1.1 真实案例](#1.1 真实案例)
- [1.2 为什么需要分层限流?](#1.2 为什么需要分层限流?)
- 二、解决方案:全链路分层限流架构
-
- [2.1 四层限流架构](#2.1 四层限流架构)
- [2.2 各层限流算法选择](#2.2 各层限流算法选择)
- 三、实战代码:四层限流实现
-
- [3.1 第一层:Nginx网关限流](#3.1 第一层:Nginx网关限流)
- [3.2 第二层:Spring Cloud Gateway限流](#3.2 第二层:Spring Cloud Gateway限流)
- [3.3 第三层:Redis分布式限流(Lua脚本)](#3.3 第三层:Redis分布式限流(Lua脚本))
- [3.4 应用层限流(Sentinel)](#3.4 应用层限流(Sentinel))
- [3.5 第四层:数据库连接池限流](#3.5 第四层:数据库连接池限流)
- 四、高级进阶:熔断与降级
-
- [4.1 Sentinel熔断配置](#4.1 Sentinel熔断配置)
- [4.2 降级策略](#4.2 降级策略)
- 五、预判问题与解答
- 六、面试高频考点
-
- 考点1:常见的限流算法有哪些?
- [考点2:Redis Lua脚本限流的优势?](#考点2:Redis Lua脚本限流的优势?)
- 考点3:Sentinel和Hystrix的区别?
- 考点4:全链路限流的最佳实践?
- 七、总结与最佳实践
-
- [7.1 核心要点回顾](#7.1 核心要点回顾)
- [7.2 性能提升数据](#7.2 性能提升数据)
- 八、参考与拓展
一、场景引入:一次秒杀活动的系统崩溃
1.1 真实案例
某电商平台举办秒杀活动,流量瞬间暴涨:
时间线:
T+0秒:秒杀开始,用户涌入
T+1秒:QPS从1000飙升到50000
T+2秒:Nginx连接数打满,部分请求502
T+3秒:网关层CPU飙到90%,响应变慢
T+5秒:应用层线程池耗尽,请求堆积
T+10秒:Redis连接数打满,缓存击穿
T+15秒:数据库连接池耗尽,大量超时
T+20秒:系统全面崩溃,服务重启
T+30秒:用户投诉刷屏,活动被迫中止
后果:
- 直接经济损失:秒杀商品被超卖
- 品牌信誉受损
- 技术团队通宵救火
- 后续活动不敢再办
问题根源:没有任何限流措施,突发流量直接穿透到数据库,导致系统雪崩。
1.2 为什么需要分层限流?
单层限流的问题:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 如果只在网关层限流: │
│ ├── 网关挡住了大部分流量 │
│ └── 但恶意用户直接绕过网关访问应用层 │
│ │
│ 如果只在应用层限流: │
│ ├── 应用层挡住了业务流量 │
│ └── 但Redis层和数据库层仍然可能被击穿 │
│ │
│ 如果只在数据库层限流: │
│ ├── 数据库保住了 │
│ └── 但前面的服务已经过载,用户体验极差 │
│ │
│ 结论:每层都需要限流,形成多层防护! │
│ │
└─────────────────────────────────────────────────────────────┘
二、解决方案:全链路分层限流架构
2.1 四层限流架构
全链路分层限流:
┌─────────────────────────────────────────────────────────────────────┐
│ 第一层:网关层(Nginx) │
│ - IP维度限流(防止单IP刷接口) │
│ - 接口维度限流(防止单接口被刷) │
│ - 静态资源CDN缓存 │
│ 过滤掉:80%的无效请求 │
├─────────────────────────────────────────────────────────────────────┤
│ 第二层:应用层(Spring Cloud Gateway) │
│ - 用户维度限流(令牌桶算法) │
│ - API维度限流(滑动窗口) │
│ - 黑名单/白名单 │
│ 过滤掉:15%的异常请求 │
├─────────────────────────────────────────────────────────────────────┤
│ 第三层:Redis层 │
│ - 分布式令牌桶(Lua脚本保证原子性) │
│ - 热点Key限流 │
│ - 库存预扣(防止超卖) │
│ 过滤掉:4%的重复请求 │
├─────────────────────────────────────────────────────────────────────┤
│ 第四层:数据库层 │
│ - 连接池最大连接数限制 │
│ - 慢查询监控与熔断 │
│ - 数据库级限流(MySQL max_connections) │
│ 最终到达:1%的有效请求 │
└─────────────────────────────────────────────────────────────────────┘
2.2 各层限流算法选择
| 层级 | 限流算法 | 维度 | 工具/实现 |
|---|---|---|---|
| 网关层 | 固定窗口/漏桶 | IP、接口 | Nginx limit_req |
| 应用层 | 令牌桶 | 用户、API | Guava RateLimiter / Sentinel |
| Redis层 | 分布式令牌桶 | 用户、接口 | Redis + Lua |
| 数据库层 | 连接池限制 | 连接数 | HikariCP max_connections |
三、实战代码:四层限流实现
3.1 第一层:Nginx网关限流
nginx
# nginx.conf
# 1. IP维度限流:单IP每秒最多10个请求
limit_req_zone $binary_remote_addr zone=ip_limit:10m rate=10r/s;
# 2. 接口维度限流:秒杀接口每秒最多1000个请求
limit_req_zone $uri zone=api_limit:10m rate=1000r/s;
server {
listen 80;
server_name api.example.com;
# 静态资源CDN缓存
location ~* \.(jpg|png|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 秒杀接口:双重限流
location /api/seckill {
# IP限流
limit_req zone=ip_limit burst=20 nodelay;
# 接口限流
limit_req zone=api_limit burst=200 nodelay;
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 普通接口:IP限流
location /api/ {
limit_req zone=ip_limit burst=50 nodelay;
proxy_pass http://backend;
}
}
3.2 第二层:Spring Cloud Gateway限流
java
/**
* Gateway限流配置
*/
@Configuration
public class GatewayRateLimitConfig {
/**
* 用户维度限流:每秒最多5个请求
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-User-Id")
);
}
/**
* IP维度限流
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
}
/**
* 限流规则配置
*/
@Configuration
public class RateLimiterConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 秒杀接口:用户维度,每秒2个请求
.route("seckill_route", r -> r.path("/api/seckill/**")
.filters(f -> f.requestRateLimiter(config -> {
config.setRateLimiter(redisRateLimiter());
config.setKeyResolver(userKeyResolver());
config.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
}))
.uri("lb://seckill-service"))
// 普通接口:IP维度,每秒10个请求
.route("api_route", r -> r.path("/api/**")
.filters(f -> f.requestRateLimiter(config -> {
config.setRateLimiter(redisRateLimiter());
config.setKeyResolver(ipKeyResolver());
}))
.uri("lb://api-service"))
.build();
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(2, 4); // replenishRate, burstCapacity
}
}
3.3 第三层:Redis分布式限流(Lua脚本)
java
/**
* Redis分布式限流器
* 使用Lua脚本保证原子性
*/
@Component
@Slf4j
public class RedisRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 令牌桶限流Lua脚本
*/
private static final String TOKEN_BUCKET_SCRIPT =
"local key = KEYS[1]\n" +
"local rate = tonumber(ARGV[1])\n" +
"local capacity = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"local requested = tonumber(ARGV[4])\n" +
"\n" +
"local fill_time = capacity / rate\n" +
"local ttl = math.floor(fill_time * 2)\n" +
"\n" +
"local last_tokens = redis.call('get', key)\n" +
"if last_tokens == false then\n" +
" last_tokens = capacity\n" +
"end\n" +
"\n" +
"local last_updated = redis.call('get', key .. ':last_updated')\n" +
"if last_updated == false then\n" +
" last_updated = 0\n" +
"end\n" +
"\n" +
"local delta = math.max(0, now - tonumber(last_updated))\n" +
"local filled_tokens = math.min(capacity, tonumber(last_tokens) + (delta * rate))\n" +
"local allowed = filled_tokens >= requested\n" +
"local new_tokens = filled_tokens\n" +
"if allowed then\n" +
" new_tokens = filled_tokens - requested\n" +
"end\n" +
"\n" +
"redis.call('setex', key, ttl, new_tokens)\n" +
"redis.call('setex', key .. ':last_updated', ttl, now)\n" +
"\n" +
"return allowed\n";
private RedisScript<Boolean> tokenBucketScript;
@PostConstruct
public void init() {
tokenBucketScript = new RedisScript<Boolean>() {
@Override
public String getSha1() {
return redisTemplate.execute(
(RedisCallback<String>) connection ->
connection.scriptLoad(TOKEN_BUCKET_SCRIPT.getBytes())
);
}
@Override
public Class<Boolean> getResultType() {
return Boolean.class;
}
@Override
public String getScriptAsString() {
return TOKEN_BUCKET_SCRIPT;
}
};
}
/**
* 尝试获取令牌
*
* @param key 限流key
* @param rate 令牌产生速率(每秒)
* @param capacity 令牌桶容量
* @return 是否允许通过
*/
public boolean tryAcquire(String key, double rate, long capacity) {
long now = System.currentTimeMillis() / 1000;
try {
Boolean allowed = redisTemplate.execute(
tokenBucketScript,
Collections.singletonList(key),
String.valueOf(rate),
String.valueOf(capacity),
String.valueOf(now),
"1"
);
return Boolean.TRUE.equals(allowed);
} catch (Exception e) {
log.error("❌ Redis限流异常: key={}", key, e);
// 限流器故障时,默认允许通过(降级策略)
return true;
}
}
/**
* 滑动窗口限流
*/
public boolean slidingWindow(String key, long windowSize, long limit) {
long now = System.currentTimeMillis();
long windowStart = now - windowSize;
String luaScript =
"redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1])\n" +
"local current = redis.call('zcard', KEYS[1])\n" +
"if tonumber(current) < tonumber(ARGV[2]) then\n" +
" redis.call('zadd', KEYS[1], ARGV[3], ARGV[4])\n" +
" redis.call('pexpire', KEYS[1], ARGV[5])\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end\n";
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(windowStart),
String.valueOf(limit),
String.valueOf(now),
UUID.randomUUID().toString(),
String.valueOf(windowSize)
);
return result != null && result == 1;
}
}
3.4 应用层限流(Sentinel)
java
/**
* Sentinel限流配置
*/
@Configuration
public class SentinelConfig {
@PostConstruct
public void init() {
// 秒杀接口:每秒最多1000个请求
FlowRule seckillRule = new FlowRule();
seckillRule.setResource("seckill");
seckillRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
seckillRule.setCount(1000);
seckillRule.setLimitApp("default");
seckillRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
seckillRule.setWarmUpPeriodSec(10);
// 下单接口:每秒最多500个请求
FlowRule orderRule = new FlowRule();
orderRule.setResource("createOrder");
orderRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
orderRule.setCount(500);
// 用户维度:每秒最多10个请求
ParamFlowRule userRule = new ParamFlowRule();
userRule.setResource("api");
userRule.setParamIdx(0); // 第一个参数是userId
userRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
userRule.setCount(10);
FlowRuleManager.loadRules(Arrays.asList(seckillRule, orderRule));
ParamFlowRuleManager.loadRules(Collections.singletonList(userRule));
}
}
/**
* 业务层使用Sentinel限流
*/
@Service
@Slf4j
public class SeckillService {
@Autowired
private RedisRateLimiter redisRateLimiter;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 秒杀下单(多层限流)
*/
public SeckillResult doSeckill(Long userId, Long skuId) {
// 第一层:用户维度限流(Redis分布式令牌桶)
String userKey = "seckill:limit:user:" + userId;
if (!redisRateLimiter.tryAcquire(userKey, 1, 5)) {
log.warn("⛔ 用户限流: userId={}", userId);
return SeckillResult.fail("操作太频繁,请稍后再试");
}
// 第二层:商品维度限流
String skuKey = "seckill:limit:sku:" + skuId;
if (!redisRateLimiter.tryAcquire(skuKey, 100, 1000)) {
log.warn("⛔ 商品限流: skuId={}", skuId);
return SeckillResult.fail("该商品太火爆了,请稍后再试");
}
// 第三层:Sentinel限流
if (!SphU.entry("seckill")) {
return SeckillResult.fail("系统繁忙,请稍后再试");
}
try {
// 库存预扣(Lua脚本保证原子性)
boolean stockDeducted = deductStock(skuId);
if (!stockDeducted) {
return SeckillResult.fail("库存不足");
}
// 异步下单(MQ削峰)
asyncCreateOrder(userId, skuId);
return SeckillResult.success("秒杀成功");
} finally {
SphU.exit();
}
}
/**
* 库存预扣(Redis Lua脚本)
*/
private boolean deductStock(Long skuId) {
String stockKey = "seckill:stock:" + skuId;
String luaScript =
"if tonumber(redis.call('get', KEYS[1])) > 0 then\n" +
" return redis.call('decr', KEYS[1])\n" +
"else\n" +
" return -1\n" +
"end\n";
RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(stockKey)
);
return result != null && result >= 0;
}
/**
* 异步创建订单(MQ削峰)
*/
private void asyncCreateOrder(Long userId, Long skuId) {
// 发送MQ消息,异步处理下单逻辑
// 这样即使MQ堆积,也不会影响秒杀接口的响应速度
}
}
3.5 第四层:数据库连接池限流
java
/**
* 数据库连接池配置(HikariCP)
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariConfig hikariConfig() {
HikariConfig config = new HikariConfig();
// 连接池大小 = (core_count * 2) + effective_spindle_count
// 4核8线程的服务器,连接池大小约 8 * 2 + 1 = 17
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
// 连接超时:如果连接池已满,等待多久后抛出异常
config.setConnectionTimeout(3000); // 3秒
// 空闲连接超时
config.setIdleTimeout(600000); // 10分钟
// 连接最大生命周期
config.setMaxLifetime(1800000); // 30分钟
// 连接测试
config.setConnectionTestQuery("SELECT 1");
return config;
}
@Bean
public DataSource dataSource(HikariConfig hikariConfig) {
return new HikariDataSource(hikariConfig);
}
}
四、高级进阶:熔断与降级
4.1 Sentinel熔断配置
java
/**
* Sentinel熔断降级配置
*/
@Configuration
public class DegradeConfig {
@PostConstruct
public void init() {
// 慢调用比例熔断
DegradeRule slowCallRule = new DegradeRule();
slowCallRule.setResource("createOrder");
slowCallRule.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType());
slowCallRule.setCount(0.5); // 慢调用比例阈值50%
slowCallRule.setTimeWindow(10); // 熔断时长10秒
slowCallRule.setSlowRatioThreshold(0.5); // 慢调用比例
slowCallRule.setStatIntervalMs(1000); // 统计时长1秒
// 异常比例熔断
DegradeRule errorRule = new DegradeRule();
errorRule.setResource("createOrder");
errorRule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType());
errorRule.setCount(0.5); // 异常比例阈值50%
errorRule.setTimeWindow(30); // 熔断时长30秒
DegradeRuleManager.loadRules(Arrays.asList(slowCallRule, errorRule));
}
}
4.2 降级策略
java
/**
* 降级处理
*/
@Service
@Slf4j
public class DegradeService {
/**
* 秒杀接口降级:返回"已售罄"页面
*/
@SentinelResource(
value = "seckill",
blockHandler = "seckillBlockHandler",
fallback = "seckillFallback"
)
public SeckillResult seckill(Long userId, Long skuId) {
// 正常业务逻辑
return doSeckill(userId, skuId);
}
/**
* 限流降级
*/
public SeckillResult seckillBlockHandler(Long userId, Long skuId, BlockException ex) {
log.warn("⛔ 秒杀被限流: userId={}, skuId={}", userId, skuId);
return SeckillResult.fail("活动太火爆了,请稍后再试");
}
/**
* 异常降级
*/
public SeckillResult seckillFallback(Long userId, Long skuId, Throwable ex) {
log.error("❌ 秒杀异常: userId={}, skuId={}", userId, skuId, ex);
return SeckillResult.fail("系统繁忙,请稍后再试");
}
}
五、预判问题与解答
Q1:四层限流的阈值怎么设置?
A:
阈值设置原则:
1. 网关层:
- IP限流:根据正常用户行为设置(如每秒10次)
- 接口限流:根据接口容量设置(如秒杀接口每秒1000次)
2. 应用层:
- 用户限流:根据业务场景(如每秒5次)
- API限流:根据压测结果(如每秒500次)
3. Redis层:
- 根据Redis集群QPS容量设置
- 单节点Redis约5万QPS
4. 数据库层:
- 根据数据库连接池大小设置
- 连接池大小 = (CPU核数 * 2) + 1
调优方法:
- 先设置宽松阈值
- 根据监控数据逐步收紧
- 预留20%的缓冲空间
Q2:限流和熔断有什么区别?
A:
| 特性 | 限流 | 熔断 |
|---|---|---|
| 目的 | 防止流量过大 | 防止故障扩散 |
| 触发条件 | QPS超过阈值 | 错误率/慢调用率超过阈值 |
| 行为 | 拒绝部分请求 | 暂时停止调用 |
| 恢复 | 请求减少后自动恢复 | 超时后自动恢复 |
| 关系 | 预防性措施 | 补救性措施 |
最佳实践:限流 + 熔断配合使用
- 限流防止流量过大
- 熔断防止故障扩散
- 降级提供兜底方案
Q3:分布式限流和单机限流怎么选?
A:
选择原则:
1. 单机限流(Guava RateLimiter):
- 适用:单体应用、负载均衡已分配流量
- 优点:性能好,无网络开销
- 缺点:无法全局控制
2. 分布式限流(Redis + Lua):
- 适用:微服务、需要全局控制
- 优点:全局一致,可动态调整
- 缺点:有网络开销,依赖Redis
3. 组合使用:
- 网关层:分布式限流(全局控制)
- 应用层:单机限流(保护单实例)
Q4:限流器故障怎么办?
A:
故障处理策略:
1. 降级策略:
- 限流器故障时,默认允许通过
- 记录日志,发送告警
2. 本地缓存:
- 限流规则本地缓存
- Redis不可用时,使用本地规则
3. 熔断机制:
- 限流器响应超时,触发熔断
- 熔断期间,使用本地限流
4. 监控告警:
- 限流器故障率监控
- 故障时立即通知运维
Q5:怎么防止恶意用户绕过限流?
A:
防护措施:
1. 多层限流:
- IP限流(防止单IP刷接口)
- 用户限流(防止单用户刷接口)
- 设备限流(防止多账号同一设备)
2. 验证码:
- 高频请求触发验证码
- 图形验证码、滑块验证码
3. 行为分析:
- 分析用户行为模式
- 异常行为加入黑名单
4. WAF防护:
- Web应用防火墙
- 识别和拦截恶意请求
六、面试高频考点
考点1:常见的限流算法有哪些?
参考答案:
常见限流算法:
1. 计数器(固定窗口):
- 实现简单
- 边界问题(窗口切换时可能突发2倍流量)
2. 滑动窗口:
- 解决固定窗口的边界问题
- 实现复杂,需要记录每个请求的时间
3. 令牌桶(推荐):
- 平滑限流,允许突发流量
- 适合大多数场景
4. 漏桶:
- 严格限流,不允许突发
- 适合需要严格控制的场景
考点2:Redis Lua脚本限流的优势?
参考答案:
优势:
1. 原子性:
- Lua脚本在Redis中执行是原子的
- 不会出现竞态条件
2. 性能:
- 减少网络往返
- 一次请求完成判断和更新
3. 一致性:
- 分布式环境下,所有节点看到的状态一致
4. 灵活性:
- 可以实现复杂的限流逻辑
- 如令牌桶、滑动窗口等
考点3:Sentinel和Hystrix的区别?
参考答案:
| 特性 | Sentinel | Hystrix |
|---|---|---|
| 限流 | 原生支持 | 需配合其他工具 |
| 熔断 | 支持 | 支持 |
| 控制台 | 有(可视化配置) | 无 |
| 热点参数限流 | 支持 | 不支持 |
| 系统自适应限流 | 支持 | 不支持 |
| 活跃度 | 活跃(阿里维护) | 停止维护 |
考点4:全链路限流的最佳实践?
参考答案:
最佳实践:
1. 分层防护:
- 每层都有限流,形成纵深防御
- 越靠近用户,限流越粗粒度
- 越靠近数据库,限流越细粒度
2. 动态调整:
- 限流阈值通过配置中心动态调整
- 根据监控数据自动优化
3. 降级策略:
- 限流时返回友好的提示
- 熔断时提供兜底方案
4. 监控告警:
- 实时监控限流触发情况
- 异常时及时告警
七、总结与最佳实践
7.1 核心要点回顾
全链路分层限流核心流程:
┌─────────────────────────────────────────────────────────────┐
│ 1. 网关层(Nginx) │
│ ├── IP限流:防止单IP刷接口 │
│ ├── 接口限流:防止单接口过载 │
│ └── 静态资源缓存:CDN加速 │
│ │
│ 2. 应用层(Gateway + Sentinel) │
│ ├── 用户维度限流:令牌桶算法 │
│ ├── API维度限流:滑动窗口 │
│ └── 熔断降级:防止故障扩散 │
│ │
│ 3. Redis层 │
│ ├── 分布式令牌桶:Lua脚本保证原子性 │
│ ├── 热点Key限流:防止缓存击穿 │
│ └── 库存预扣:防止超卖 │
│ │
│ 4. 数据库层 │
│ ├── 连接池限流:防止连接耗尽 │
│ ├── 慢查询监控:及时发现性能问题 │
│ └── SQL优化:从根本上减少数据库压力 │
└─────────────────────────────────────────────────────────────┘
7.2 性能提升数据
某电商平台秒杀活动实测数据:
| 指标 | 优化前(无限流) | 优化后(四层限流) | 提升 |
|---|---|---|---|
| 系统可用性 | 60%(频繁崩溃) | 99.9% | 显著提升 |
| 峰值QPS | 3000(系统崩溃) | 50000 | 16倍↑ |
| 平均响应时间 | 5s+ | 50ms | 99%↓ |
| 超卖率 | 5% | 0% | 100%↓ |
| 用户体验 | 极差 | 良好 | 显著提升 |
八、参考与拓展
互动讨论:你们公司是怎么做限流的?有没有遇到过被流量打挂的经历?欢迎在评论区分享!
如果本文对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,持续获取更多Java后端技术干货!