Skip to content

限流与鉴权深度解析

面试官:你知道令牌桶和漏桶算法的区别吗?

:漏桶算法以固定速率处理请求,能平滑流量但无法处理突发;令牌桶以固定速率生成令牌,允许在桶未满时处理突发流量。简单说,漏桶强制限速,令牌桶允许短时突发。

面试官:那 Sentinel 用的是哪种算法?为什么选它?

这个问题考察对主流框架实现的理解。能说清 Sentinel 的「滑动窗口 + 令牌桶」混合策略的候选人,才具备生产实战经验。


链式追问一:限流算法核心原理

Section titled “链式追问一:限流算法核心原理”

Q1:四种限流算法的原理和对比?必考

Section titled “Q1:四种限流算法的原理和对比?”

算法概览

┌─────────────────────────────────────────────────────────────┐
│ 限流算法对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 固定窗口计数器 2. 滑动窗口计数器 │
│ ┌───┬───┐ ┌─┬─┬─┬─┬─┬─┐ │
│ │ 0 │ 1 │ │1│2│3│4│5│6│ │
│ └───┴───┘ └─┴─┴─┴─┴─┴─┘ │
│ 简单但临界突破 精确但内存占用高 │
│ │
│ 3. 漏桶算法 4. 令牌桶算法 │
│ │ ┌───┐ │
│ ▼ ┌──│ ● │──┐ │
│ ┌───┐ 固定速率 │ └───┘ │ │
│ │桶 │ ─────────→ │ 令牌池 │ │
│ └───┘ 流出 └─────────┘ │
│ 强制限速 允许突发 │
│ │
└─────────────────────────────────────────────────────────────┘

1. 固定窗口计数器

场景:每分钟限制 100 次请求
时间线:
00:00 ─────────── 01:00 ─────────── 02:00
│ 窗口1 │ 窗口2 │
│ 100次上限 │ 计数器归0 │
问题演示(临界突破):
00:59 发送 100 次请求 ← 窗口1 满了
01:01 发送 100 次请求 ← 窗口2 重新开始
结果:00:59~01:01 的 2 秒内,实际发送了 200 次请求!
这是固定窗口的致命缺陷:临界时刻可能 2x 突破限制。

2. 滑动窗口计数器

场景:每分钟限制 100 次(拆分为 60 个 1 秒小格)
当前时间:01:30
有效窗口:00:30 ~ 01:30(最近 60 秒)
┌───┬───┬───┬───┬───┬─────┬───┬───┐
│00:│00:│00:│...│01:│01: │01:│01:│
│30 │31 │32 │ │28 │29 │30 │31 │
└───┴───┴───┴───┴───┴─────┴───┴───┘
↑ ↑
最老的小格 最新小格
当前请求计数 = 所有 60 个小格的总和
每秒滑动一格,自然过渡,无临界问题!

Redis 实现(ZSet)

/**
* 滑动窗口限流器
* 使用 Redis ZSet 实现,key 为请求时间戳
*/
public class SlidingWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 检查是否允许请求
* @param key 限流 key(如 "rate_limit:user:123")
* @param limit 限制次数
* @param windowSeconds 窗口大小(秒)
*/
public boolean allowRequest(String key, int limit, int windowSeconds) {
long now = System.currentTimeMillis();
long windowStart = now - windowSeconds * 1000;
// 1. 删除窗口外的旧数据
redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);
// 2. 统计当前窗口内的请求数
Long count = redisTemplate.opsForZSet().count(key, windowStart, now);
if (count == null) count = 0L;
// 3. 判断是否超过限制
if (count >= limit) {
return false; // 拒绝请求
}
// 4. 记录本次请求
String requestId = UUID.randomUUID().toString();
redisTemplate.opsForZSet().add(key, requestId, now);
// 5. 设置过期时间(窗口大小 + 1 秒缓冲)
redisTemplate.expire(key, windowSeconds + 1, TimeUnit.SECONDS);
return true; // 允许请求
}
}

3. 漏桶算法(Leaky Bucket)

原理:请求像水一样流入桶中,桶以固定速率漏水(处理请求)
请求流量(突发)
┌─────────────────┐
│ 漏桶(队列) │ ← 固定容量(如 100 个请求)
│ ┌───┬───┬───┐ │
│ │ r1│ r2│ r3│ │
│ └───┴───┴───┘ │
└─────────────────┘
▼ 固定速率流出(如每秒 10 个)
┌──────┐
│ 处理 │
└──────┘
特性:
• 桶满则拒绝新请求(保护系统)
• 流出速率固定,无论输入多大
• 无法处理突发流量(即使下游有空闲,也不能加速)
适用场景:
• 保护数据库等脆弱系统
• 需要严格限制处理速率
• 流量削峰填谷

4. 令牌桶算法(Token Bucket)

原理:系统以固定速率向桶中放令牌,请求消耗令牌
固定速率放令牌(如每秒 100 个)
┌─────────────────┐
│ 令牌桶 │ ← 最大容量(如 200 个)
│ ● ● ● ● ● │ 允许令牌积累
│ ● ● ● ● ● │
│ ● ● ● ● │
└─────────────────┘
│ 请求到达,取走令牌
┌─────────────────┐
│ 处理请求 │ 有令牌 → 处理
└─────────────────┘ 无令牌 → 拒绝/等待
特性:
• 平时令牌积累,突发时可快速消耗
• 突发量 = 桶容量(如 200 个)
• 平均速率 = 放令牌速率(如每秒 100 个)
适用场景:
• API 限流(允许短时突发)
• 用户操作限制
• 网络流量控制

完整对比表格

算法突发处理流量平滑精确度实现复杂度内存占用适用场景
固定窗口临界 2x 突破极简O(1)简单限流,要求不高
滑动窗口准确限制中等O(N)需要精确控制的场景
漏桶❌ 不允许最好中等O(桶容量)保护下游,削峰填谷
令牌桶✅ 允许(到桶容量)较好中等O(1)API 限流,允许突发

Q2:Sentinel 用的是什么限流算法?为什么?高频

Section titled “Q2:Sentinel 用的是什么限流算法?为什么?”

Sentinel 的混合策略

Sentinel 限流策略:
1. QPS 限流
└── 滑动时间窗口算法
• 统计每秒请求数
• 1 秒拆分为 2 个 500ms 的样本窗口
• 循环复用,减少内存占用
2. 并发线程数限流
└── 计数器算法
• 统计当前正在处理的线程数
• 超过阈值拒绝新请求
3. 预热(Warm Up)模式
└── 令牌桶算法(Guava RateLimiter)
• 冷启动时,令牌生成速率从 阈值/3 逐渐提升到 阈值
• 防止冷系统突然被大流量打垮
4. 匀速排队
└── 漏桶算法
• 请求排队,固定速率处理
• 适合消息队列等场景

滑动窗口实现细节

/**
* Sentinel 滑动窗口核心实现
* 使用数组循环复用,避免频繁创建对象
*/
public class SlidingWindow {
// 窗口大小:1000ms
private final int windowLengthInMs = 500;
// 样本窗口数量:2 个
private final int sampleCount = 2;
// 循环数组存储每个样本窗口的计数
private final AtomicLong[] array = new AtomicLong[sampleCount];
public boolean canPass(int threshold) {
long currentTimeMs = System.currentTimeMillis();
// 计算当前样本窗口索引
int idx = (int) ((currentTimeMs / windowLengthInMs) % sampleCount);
// 获取当前样本窗口的开始时间
long windowStart = currentTimeMs - (currentTimeMs % windowLengthInMs);
// 重置过期的样本窗口
AtomicLong old = array[idx];
if (old == null) {
array[idx] = new AtomicLong(1);
return true;
}
// 如果样本窗口的时间不对,重置
if (old.get() >> 32 != windowStart) {
array[idx] = new AtomicLong((windowStart << 32) + 1);
return true;
}
// 统计当前窗口的总请求数
long sum = 0;
for (AtomicLong counter : array) {
if (counter != null) {
sum += counter.get() & 0xFFFFFFFFL;
}
}
// 判断是否超过阈值
if (sum >= threshold) {
return false; // 拒绝
}
// 计数 +1
old.incrementAndGet();
return true;
}
}

Guava RateLimiter(令牌桶)

/**
* Guava RateLimiter 使用示例
* 支持预热模式
*/
public class RateLimiterExample {
public static void main(String[] args) {
// 1. 普通模式:每秒 100 个令牌
RateLimiter rateLimiter = RateLimiter.create(100.0);
// 2. 预热模式:每秒 100 个,预热时间 10 秒
// 冷启动时从 33.3/s 逐渐提升到 100/s
RateLimiter warmupLimiter = RateLimiter.create(100.0, 10, TimeUnit.SECONDS);
// 3. 阻塞等待获取令牌
rateLimiter.acquire(); // 阻塞直到获取到令牌
doProcess();
// 4. 非阻塞尝试获取
if (rateLimiter.tryAcquire()) {
doProcess();
} else {
// 快速失败
throw new RateLimitException("请求太频繁");
}
// 5. 尝试获取,最多等待 100ms
if (rateLimiter.tryAcquire(100, TimeUnit.MILLISECONDS)) {
doProcess();
} else {
throw new RateLimitException("等待超时");
}
}
}

为什么 Sentinel 选择混合策略

场景算法选择原因
API QPS 限流滑动窗口精确统计,无临界问题
并发线程数计数器简单高效,实时统计
冷启动保护令牌桶(预热)渐进提升容量,避免系统过载
消息队列漏桶(匀速)固定速率消费,避免消息堆积

Q3:分布式限流如何实现?高频

Section titled “Q3:分布式限流如何实现?”

单机限流 vs 分布式限流

单机限流:
Gateway 1: 100 QPS
Gateway 2: 100 QPS
Gateway 3: 100 QPS
总计:300 QPS(如果配置 100 QPS/台)
问题:
• 无法精确控制全局 QPS
• 请求分布不均时,某台可能超限
分布式限流:
Gateway 1 ─┐
Gateway 2 ─┼─→ Redis(全局计数器)→ 全局 100 QPS
Gateway 3 ─┘

Redis + Lua 实现令牌桶

/**
* Redis 分布式令牌桶限流器
* 使用 Lua 脚本保证原子性
*/
public class RedisTokenBucketRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
// Lua 脚本:原子性地获取令牌
private static final String SCRIPT =
"local key = KEYS[1] " +
"local permits = tonumber(ARGV[1]) " + // 请求令牌数
"local maxPermits = tonumber(ARGV[2]) " + // 桶容量
"local rate = tonumber(ARGV[3]) " + // 令牌生成速率(个/秒)
"local now = tonumber(ARGV[4]) " + // 当前时间(毫秒)
// 获取当前令牌数和上次更新时间
"local info = redis.call('hmget', key, 'tokens', 'lastRefillTime') " +
"local tokens = tonumber(info[1]) or maxPermits " +
"local lastRefillTime = tonumber(info[2]) or now " +
// 计算补充的令牌数
"local interval = (now - lastRefillTime) / 1000 " +
"local addedTokens = math.floor(interval * rate) " +
// 更新令牌数(不超过桶容量)
"tokens = math.min(maxPermits, tokens + addedTokens) " +
// 判断是否有足够令牌
"if tokens >= permits then " +
" tokens = tokens - permits " +
" redis.call('hmset', key, 'tokens', tokens, 'lastRefillTime', now) " +
" redis.call('expire', key, 3600) " +
" return 1 " + // 成功获取
"else " +
" return 0 " + // 令牌不足
"end";
/**
* 尝试获取令牌
*/
public boolean tryAcquire(String key, int permits, int maxPermits, int rate) {
long now = System.currentTimeMillis();
Long result = redisTemplate.execute(
new DefaultRedisScript<>(SCRIPT, Long.class),
Collections.singletonList(key),
String.valueOf(permits),
String.valueOf(maxPermits),
String.valueOf(rate),
String.valueOf(now)
);
return result != null && result == 1;
}
}

性能优化:本地缓存 + 异步同步

纯 Redis 方案的问题:
• 每次请求都访问 Redis,延迟高(~1ms)
• Redis 成为性能瓶颈
优化方案(本地缓存 80% 流量):
Gateway 本地令牌桶(80 个/s)+ Redis 全局限流(20 个/s)
┌─────────────────────────────────────┐
│ Gateway 本地令牌桶 │
│ • 处理 80% 请求(快速响应) │
│ • 定期从 Redis 同步配额 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Redis 全局限流 │
│ • 处理剩余 20% 请求 │
│ • 精确控制全局总量 │
└─────────────────────────────────────┘
性能提升:延迟从 ~1ms 降到 ~0.1ms

Q4:JWT 和 Session 的区别?各自适用什么场景?必考

Section titled “Q4:JWT 和 Session 的区别?各自适用什么场景?”

架构对比

Session 方案(服务端状态):
客户端 服务端
│ │
│ 1. 登录请求 │
├────────────────────────►│
│ │ 创建 Session(Redis)
│ │ session_id = "abc123"
│ 2. 返回 Session ID │
│◄────────────────────────┤ Set-Cookie: JSESSIONID=abc123
│ │
│ 3. 后续请求(带 Cookie) │
├────────────────────────►│
│ Cookie: JSESSIONID=... │ 查询 Session(Redis)
│ │ 验证用户身份
│ │
│ 4. 返回数据 │
│◄────────────────────────┤
JWT 方案(客户端状态):
客户端 服务端
│ │
│ 1. 登录请求 │
├────────────────────────►│
│ │ 生成 JWT(私钥签名)
│ │ token = "xxx.yyy.zzz"
│ 2. 返回 JWT │
│◄────────────────────────┤ { "token": "xxx.yyy.zzz" }
│ │
│ 本地存储 JWT │
│ │
│ 3. 后续请求(带 Token) │
├────────────────────────►│
│ Authorization: Bearer..│ 验证签名(无需查询)
│ │ 解析用户信息
│ 4. 返回数据 │
│◄────────────────────────┤

详细对比

维度Session(服务端状态)JWT(客户端状态)
存储位置服务端(内存/Redis)客户端(Cookie/LocalStorage)
服务端存储需要存储 Session 数据无需存储(无状态)
分布式需要共享 Session(Redis)天然支持
扩展性受 Redis 容量限制无限制
注销立即生效(删除 Session)有延迟(需要黑名单)
安全性Session ID 泄露可立即失效Token 泄露前无法失效
Token 大小Session ID(~50B)JWT(~500B+)
跨域需要配置 Cookie天然支持(Header)
移动端Cookie 支持差天然支持
性能需查询 Redis无需查询(验证签名)

JWT 结构详解

JWT = Header.Payload.Signature
示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
┌─────────────────────────────────────────────────────────┐
│ Header(Base64 编码) │
├─────────────────────────────────────────────────────────┤
│ { │
│ "alg": "HS256", // 签名算法 │
│ "typ": "JWT" // Token 类型 │
│ } │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Payload(Base64 编码) │
├─────────────────────────────────────────────────────────┤
│ { │
│ "sub": "user_123", // 用户 ID(标准声明) │
│ "name": "John Doe", // 用户名 │
│ "iat": 1516239022, // 签发时间(标准声明) │
│ "exp": 1516242622, // 过期时间(标准声明) │
│ "roles": ["USER", "ADMIN"], // 自定义声明 │
│ "permissions": ["read", "write"] │
│ } │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Signature(签名) │
├─────────────────────────────────────────────────────────┤
│ HMACSHA256( │
│ base64(header) + "." + base64(payload), │
│ secret // 服务端密钥,只有服务端知道 │
│ ) │
│ │
│ 作用:防止篡改。篡改 Payload 后,Signature 验证失败。 │
└─────────────────────────────────────────────────────────┘

Q5:JWT 如何实现注销(踢出登录)?高频

Section titled “Q5:JWT 如何实现注销(踢出登录)?”

JWT 的天然缺陷:无状态意味着签发后无法撤销(只要未过期就有效)。

方案对比

方案延迟性能实现复杂度适用场景
短过期 + Refresh Token最多 15 分钟推荐方案
Token 黑名单即时中(Redis 查询)需要立即注销
双 Token 验证即时低(两次验证)高安全场景

方案一:短过期 + Refresh Token(推荐)

┌─────────────────────────────────────────────────────────┐
│ 双 Token 机制 │
├─────────────────────────────────────────────────────────┤
│ │
│ Access Token: │
│ • 过期时间短(5~15 分钟) │
│ • 存储在客户端(LocalStorage) │
│ • 用于访问业务接口 │
│ │
│ Refresh Token: │
│ • 过期时间长(7 天) │
│ • 存储在 Redis(可撤销) │
│ • 用于刷新 Access Token │
│ │
└─────────────────────────────────────────────────────────┘
流程:
1. 登录成功
└── 返回 Access Token + Refresh Token
2. 业务请求
└── 携带 Access Token(Header: Authorization: Bearer ...)
3. Access Token 过期
└── 返回 401 Unauthorized
└── 客户端用 Refresh Token 换新 Access Token
4. 注销(踢出登录)
└── 删除 Redis 中的 Refresh Token
└── 用户无法刷新 Access Token
└── 最多等 15 分钟,Access Token 自然过期

代码实现

/**
* JWT 双 Token 认证服务
*/
@Service
public class JwtAuthService {
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${jwt.access-token-expiration:900}") // 15 分钟
private long accessTokenExpiration;
@Value("${jwt.refresh-token-expiration:604800}") // 7 天
private long refreshTokenExpiration;
/**
* 登录:生成双 Token
*/
public LoginResponse login(String username, String password) {
// 1. 验证用户名密码
User user = authenticate(username, password);
// 2. 生成 Access Token
String accessToken = Jwts.builder()
.setSubject(user.getId())
.claim("roles", user.getRoles())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration * 1000))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
// 3. 生成 Refresh Token(随机字符串)
String refreshToken = UUID.randomUUID().toString().replace("-", "");
// 4. Refresh Token 存入 Redis
redisTemplate.opsForValue().set(
"refresh_token:" + refreshToken,
user.getId(),
refreshTokenExpiration,
TimeUnit.SECONDS
);
return new LoginResponse(accessToken, refreshToken, accessTokenExpiration);
}
/**
* 刷新 Access Token
*/
public LoginResponse refreshToken(String refreshToken) {
// 1. 验证 Refresh Token 是否有效
String userId = redisTemplate.opsForValue().get("refresh_token:" + refreshToken);
if (userId == null) {
throw new UnauthorizedException("Refresh Token 无效或已过期");
}
// 2. 查询用户信息
User user = userRepository.findById(userId);
// 3. 生成新的 Access Token
String newAccessToken = Jwts.builder()
.setSubject(user.getId())
.claim("roles", user.getRoles())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration * 1000))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
// 4. 可选:刷新 Refresh Token(延长有效期)
redisTemplate.expire("refresh_token:" + refreshToken, refreshTokenExpiration, TimeUnit.SECONDS);
return new LoginResponse(newAccessToken, refreshToken, accessTokenExpiration);
}
/**
* 注销(踢出登录)
*/
public void logout(String refreshToken) {
// 删除 Redis 中的 Refresh Token
redisTemplate.delete("refresh_token:" + refreshToken);
// 用户无法刷新 Access Token
// 最多等 15 分钟,Access Token 自然过期
}
}

方案二:Token 黑名单

/**
* JWT 黑名单方案
*/
@Service
public class JwtBlacklistService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 注销时将 Token 加入黑名单
*/
public void logout(String token) {
// 解析 Token 获取过期时间
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
Date expiration = claims.getExpiration();
// 计算 TTL(剩余有效时间)
long ttl = (expiration.getTime() - System.currentTimeMillis()) / 1000;
if (ttl > 0) {
// 将 Token 加入黑名单,TTL = 剩余有效期
redisTemplate.opsForValue().set(
"blacklist:" + token,
"1",
ttl,
TimeUnit.SECONDS
);
}
}
/**
* 验证 Token 时检查黑名单
*/
public boolean validateToken(String token) {
// 1. 检查黑名单
if (Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:" + token))) {
return false; // 已注销
}
// 2. 验证签名和过期时间
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
}

性能对比

无黑名单:
验证 JWT → 解析签名 → 通过(~0.1ms)
有黑名单:
验证 JWT → 检查 Redis 黑名单 → 解析签名 → 通过(~1ms)
影响:
• 每次 JWT 验证都需要查询 Redis
• 增加 Redis 压力
• 延迟增加 ~1ms

Q6:网关层如何统一实现 JWT 鉴权?高频

Section titled “Q6:网关层如何统一实现 JWT 鉴权?”

完整实现

/**
* 网关层 JWT 认证过滤器
* 统一鉴权,透传用户信息到下游服务
*/
@Component
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private StringRedisTemplate redisTemplate;
// 白名单路径(无需认证)
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/auth/login",
"/api/auth/register",
"/api/auth/refresh",
"/api/public/**",
"/health",
"/actuator/**"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
// 1. 白名单路径直接放行
if (isWhiteListed(path)) {
return chain.filter(exchange);
}
// 2. 提取 Token
String token = extractToken(request);
if (token == null) {
return unauthorized(exchange, "缺少认证 Token");
}
try {
// 3. 验证 Token
Claims claims = jwtUtil.validateToken(token);
// 4. 可选:检查黑名单
if (isInBlacklist(token)) {
return unauthorized(exchange, "Token 已失效");
}
// 5. 可选:检查用户状态(是否被封禁)
String userId = claims.getSubject();
if (isUserDisabled(userId)) {
return unauthorized(exchange, "用户已被禁用");
}
// 6. 透传用户信息到下游服务(关键!)
ServerHttpRequest newRequest = request.mutate()
.header("X-User-Id", userId)
.header("X-User-Name", claims.get("name", String.class))
.header("X-User-Roles", String.join(",", claims.get("roles", List.class)))
.header("X-Token", token) // 下游可能需要 Token 调用其他服务
.build();
// 7. 继续过滤器链
return chain.filter(exchange.mutate().request(newRequest).build());
} catch (ExpiredJwtException e) {
return unauthorized(exchange, "Token 已过期");
} catch (JwtException e) {
return unauthorized(exchange, "Token 无效");
}
}
/**
* 提取 Token
*/
private String extractToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
// 支持从 Cookie 提取
HttpCookie cookie = request.getCookies().getFirst("access_token");
if (cookie != null) {
return cookie.getValue();
}
return null;
}
/**
* 检查白名单
*/
private boolean isWhiteListed(String path) {
for (String pattern : WHITE_LIST) {
if (pattern.endsWith("/**")) {
String prefix = pattern.substring(0, pattern.length() - 3);
if (path.startsWith(prefix)) {
return true;
}
} else if (path.equals(pattern)) {
return true;
}
}
return false;
}
/**
* 检查黑名单
*/
private boolean isInBlacklist(String token) {
return Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:" + token));
}
/**
* 检查用户是否被禁用
*/
private boolean isUserDisabled(String userId) {
String status = redisTemplate.opsForValue().get("user:status:" + userId);
return "DISABLED".equals(status);
}
/**
* 返回 401 错误
*/
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> body = new HashMap<>();
body.put("code", 401);
body.put("message", message);
body.put("timestamp", System.currentTimeMillis());
DataBuffer buffer = response.bufferFactory()
.wrap(JSON.toJSONString(body).getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 高优先级,尽早拦截
}
}

下游服务使用用户信息

/**
* 下游服务控制器
* 直接从 Header 获取用户信息,无需再解析 JWT
*/
@RestController
public class OrderController {
@PostMapping("/api/orders")
public Order createOrder(@RequestBody OrderRequest request,
@RequestHeader("X-User-Id") String userId,
@RequestHeader("X-User-Roles") String roles) {
// 网关已经完成认证,直接使用用户信息
return orderService.createOrder(userId, request);
}
@GetMapping("/api/orders")
public List<Order> getMyOrders(@RequestHeader("X-User-Id") String userId) {
// 直接查询当前用户的订单
return orderService.findByUserId(userId);
}
}

架构优势

传统方案(每个服务都验证 JWT):
Gateway → 用户服务(验证 JWT)
→ 订单服务(验证 JWT)
→ 支付服务(验证 JWT)
问题:
• JWT 解析开销重复
• 每个服务都要处理认证逻辑
网关统一认证方案:
Gateway(验证 JWT,透传用户信息)→ 用户服务(信任 Header)
→ 订单服务(信任 Header)
→ 支付服务(信任 Header)
优势:
• JWT 解析只执行一次
• 下游服务无需关心认证
• 统一的安全策略
注意:
• 内网服务间调用,信任网关透传的 Header
• 外网直接访问服务,需要额外保护

Q7:如何实现基于用户级别的精细化限流?高频

Section titled “Q7:如何实现基于用户级别的精细化限流?”

多维度限流策略

限流维度:
1. IP 级别(防 DDoS)
└── 单个 IP 每秒最多 100 次请求
2. 用户级别(防刷量)
└── 单个用户每分钟最多 60 次请求
3. 接口级别(保护核心接口)
└── 查询接口:100 QPS
└── 下单接口:10 QPS
4. 租户级别(SaaS 场景)
└── 基础版:1000 QPS
└── 专业版:5000 QPS
└── 企业版:不限

Gateway 实现

/**
* 多维度限流过滤器
*/
@Component
public class MultiDimensionRateLimitFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
// 1. IP 限流(防 DDoS)
String ip = getClientIp(request);
if (!checkRateLimit("ip:" + ip, 100, 1)) {
return tooManyRequests(exchange, "IP 请求过于频繁");
}
// 2. 用户限流(需要先认证)
String userId = request.getHeaders().getFirst("X-User-Id");
if (userId != null) {
// 获取用户等级(不同等级不同配额)
UserLevel level = getUserLevel(userId);
int userLimit = level.getRateLimit(); // VIP: 100/min, 普通用户: 60/min
if (!checkRateLimit("user:" + userId, userLimit, 60)) {
return tooManyRequests(exchange, "用户请求过于频繁");
}
}
// 3. 接口限流
String apiLimit = getApiLimit(path); // 查询接口:100,下单接口:10
if (!checkRateLimit("api:" + path, apiLimit, 1)) {
return tooManyRequests(exchange, "接口繁忙");
}
// 4. 租户限流(SaaS)
String tenantId = request.getHeaders().getFirst("X-Tenant-Id");
if (tenantId != null) {
int tenantLimit = getTenantLimit(tenantId);
if (!checkRateLimit("tenant:" + tenantId, tenantLimit, 1)) {
return tooManyRequests(exchange, "租户配额已用尽");
}
}
return chain.filter(exchange);
}
/**
* 滑动窗口限流检查
*/
private boolean checkRateLimit(String key, int limit, int windowSeconds) {
long now = System.currentTimeMillis();
long windowStart = now - windowSeconds * 1000;
// 使用 Redis ZSet 实现滑动窗口
String redisKey = "rate_limit:" + key;
// 1. 删除过期数据
redisTemplate.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);
// 2. 统计当前窗口请求数
Long count = redisTemplate.opsForZSet().count(redisKey, windowStart, now);
if (count == null) count = 0L;
// 3. 判断是否超限
if (count >= limit) {
return false;
}
// 4. 记录本次请求
redisTemplate.opsForZSet().add(redisKey, UUID.randomUUID().toString(), now);
redisTemplate.expire(redisKey, windowSeconds + 1, TimeUnit.SECONDS);
return true;
}
@Override
public int getOrder() {
return -90; // 在认证之后
}
}

限流配置管理

# 限流规则配置(Nacos)
rate-limit:
# IP 限流
ip:
default: 100 # 默认 100 次/秒
blacklist: # 黑名单(直接拒绝)
- 192.168.1.100
# 用户限流
user:
normal: 60 # 普通用户 60 次/分钟
vip: 200 # VIP 用户 200 次/分钟
svip: 1000 # SVIP 用户 1000 次/分钟
# 接口限流
api:
/api/orders/create: 10 # 下单接口 10 QPS
/api/orders/query: 100 # 查询接口 100 QPS
/api/payments/create: 5 # 支付接口 5 QPS
# 租户限流
tenant:
basic: 1000 # 基础版 1000 QPS
pro: 5000 # 专业版 5000 QPS
enterprise: -1 # 企业版不限

场景:秒杀活动,100 件商品,10 万用户参与。

问题

  • 瞬时流量达到 10 万 QPS
  • 直接打垮数据库
  • 库存超卖

解决方案

多层限流架构:
┌─────────────────────────────────────────────────────────┐
│ CDN 限流(第一层) │
│ • 静态资源缓存 │
│ • 请求频率限制 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Nginx 限流(第二层) │
│ • IP 限流:单 IP 10 次/秒 │
│ • 全局限流:总 QPS 限制在 5000 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Gateway 限流(第三层) │
│ • 用户限流:单用户 1 次/秒 │
│ • 接口限流:秒杀接口 1000 QPS │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Redis 预扣库存(第四层) │
│ • Lua 脚本原子性扣减 │
│ • 库存不足直接返回 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ MQ 异步下单(第五层) │
│ • 请求写入消息队列 │
│ • 数据库异步消费 │
└─────────────────────────────────────────────────────────┘

核心代码

/**
* 秒杀接口限流
*/
@RestController
public class SeckillController {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 秒杀接口
*/
@PostMapping("/api/seckill/{productId}")
public Result seckill(@PathVariable String productId,
@RequestHeader("X-User-Id") String userId) {
// 1. 用户限流(1 秒只能抢 1 次)
String userKey = "seckill:user:" + userId + ":" + productId;
Boolean success = redisTemplate.opsForValue().setIfAbsent(
userKey, "1", 1, TimeUnit.SECONDS
);
if (!success) {
return Result.fail("操作过于频繁");
}
// 2. 预扣库存(Lua 脚本保证原子性)
String script =
"if redis.call('get', KEYS[1]) <= '0' then " +
" return 0 " +
"end " +
"return redis.call('decr', KEYS[1])";
Long stock = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("seckill:stock:" + productId)
);
if (stock == null || stock < 0) {
return Result.fail("商品已售罄");
}
// 3. 发送消息到 MQ
SeckillOrder order = new SeckillOrder(userId, productId);
rabbitTemplate.convertAndSend("seckill.order", order);
return Result.success("抢购成功,订单处理中");
}
}