线上服务最怕被恶意刷接口或者突发流量把系统打垮。单机限流用 Guava RateLimiter 就行,但微服务多实例部署时必须用分布式限流------所有实例共享同一个限流状态。
一、限流的常见算法
| 算法 | 原理 | 特点 |
|---|---|---|
| 固定窗口 | 1 分钟内最多 N 个请求 | 简单但有突刺问题 |
| 滑动窗口 | 把窗口切小段,更平滑 | 推荐,较常用 |
| 漏桶 | 请求进桶,匀速流出 | 平滑流量,但突发处理慢 |
| 令牌桶 | 匀速放令牌,拿到的才通过 | 允许一定突发,最均衡 |
Redis 实现限流的本质: 用 Redis 计数器/有序集合记录请求次数,加上过期时间自动清理。
二、固定窗口限流
最简单的方案:一个 key 记录 1 分钟内的请求数。
java
@Component
public class FixedWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 固定窗口限流
* @param key 限流 key(如 "rate:user:1001")
* @param max 窗口内最大请求数
* @param window 窗口大小(秒)
* @return true 通过,false 被限
*/
public boolean tryAcquire(String key, long max, long window) {
// INCR 原子自增
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
// 第一次访问,设置过期时间
redisTemplate.expire(key, window, TimeUnit.SECONDS);
}
return count <= max;
}
}
java
@RestController
public class TestController {
@Autowired
private FixedWindowRateLimiter rateLimiter;
@GetMapping("/api/test")
public ResultVO<?> test() {
// 限制每个用户每秒最多 5 次
if (!rateLimiter.tryAcquire("rate:user:1001", 5, 1)) {
return ResultVO.error(429, "请求太频繁,请稍后");
}
return ResultVO.success("成功");
}
}
问题: 窗口边界会有突刺------比如第 59 秒和第 61 秒各发 5 个请求,理论上通过 10 个,但看起来像 1 秒内 10 个。
三、滑动窗口限流(推荐)
用 Redis ZSet(有序集合)记录每个请求的时间戳,滑动窗口更精确。
java
@Component
public class SlidingWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 滑动窗口限流
* @param key 限流 key
* @param max 窗口内最大请求数
* @param window 窗口大小(毫秒)
* @return true 通过,false 被限
*/
public boolean tryAcquire(String key, long max, long windowMs) {
long now = System.currentTimeMillis();
long windowStart = now - windowMs;
// 使用 Lua 脚本保证原子性
String luaScript =
"redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]) " + // 移除窗口外的记录
"local count = redis.call('ZCARD', KEYS[1]) " + // 统计窗口内请求数
"if count < tonumber(ARGV[2]) then " +
" redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3]) " + // 加入当前请求
" redis.call('EXPIRE', KEYS[1], ARGV[4]) " + // 设置过期(秒)
" return 1 " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(windowStart),
String.valueOf(max),
String.valueOf(now),
String.valueOf(windowMs / 1000 + 1)
);
return Long.valueOf(1).equals(result);
}
}
四、注解式限流
把限流逻辑封装成注解,用在需要的接口上,代码更干净。
1. 定义注解
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/** 限流 key(支持 SpEL) */
String key() default "";
/** 限流维度:用户、IP、接口 */
String type() default "user"; // user、ip、api
/** 最大请求数 */
long max() default 10;
/** 窗口大小(秒) */
long window() default 1;
}
2. 实现 AOP 切面
java
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private SlidingWindowRateLimiter rateLimiter;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
// 构建 key
String key = buildKey(rateLimit, pjp);
// 执行限流
boolean acquired = rateLimiter.tryAcquire(
key, rateLimit.max(), rateLimit.window() * 1000);
if (!acquired) {
throw new RuntimeException("请求太频繁,请稍后重试");
}
return pjp.proceed();
}
private String buildKey(RateLimit rateLimit, ProceedingJoinPoint pjp) {
String prefix = "rate:";
switch (rateLimit.type()) {
case "ip":
// 从请求中获取 IP(需要注入 RequestContextHolder)
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getRemoteAddr();
return prefix + "ip:" + ip;
case "api":
// 按接口限流
String methodName = pjp.getSignature().toShortString();
return prefix + "api:" + methodName;
default:
// 按用户限流(需要从 Token 获取用户 ID)
String userId = StpUtil.getLoginIdAsString();
return prefix + "user:" + userId;
}
}
}
// 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResultVO<?> handle(RuntimeException e) {
if (e.getMessage().contains("请求太频繁")) {
return ResultVO.error(429, "请求太频繁,请稍后重试");
}
return ResultVO.error(500, e.getMessage());
}
}
3. 使用
java
@RestController
@RequestMapping("/api")
public class OrderController {
@PostMapping("/order")
@RateLimit(key = "createOrder", type = "user", max = 5, window = 10)
public ResultVO<?> createOrder(@RequestBody OrderDTO dto) {
// 每个用户 10 秒内最多 5 次下单
return ResultVO.success("下单成功");
}
@GetMapping("/product/list")
@RateLimit(type = "api", max = 100, window = 1)
public ResultVO<?> list() {
// 接口级别限流:每秒最多 100 次
return ResultVO.success("商品列表");
}
}
五、秒杀系统的限流
在秒杀系统中,限流是保护系统的第一道防线:
java
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@PostMapping("/do/{productId}")
@RateLimit(type = "user", max = 1, window = 10) // 每个用户10秒只能秒杀1次
public ResultVO<?> doSeckill(@PathVariable Long productId) {
long userId = StpUtil.getLoginIdAsLong();
return seckillService.doSeckill(productId, userId, "用户");
}
}
秒杀系统的多层防护:
用户请求
↓
① 限流(Redis 滑动窗口)→ 被限返回"操作频繁"
↓
② 参数校验 → 不合法直接返回
↓
③ 时间校验 → 没开始/已结束直接返回
↓
④ 重复提交校验(SETNX)→ 已提交直接返回
↓
⑤ Redis 预减库存 → 库存不足直接返回
↓
⑥ 数据库扣库存(乐观锁)→ 失败回滚
↓
⑦ 生成订单
限流在最外层,把大部分无效请求挡在门外。
六、IP 限流
java
@GetMapping("/api/public")
@RateLimit(type = "ip", max = 30, window = 60)
public ResultVO<?> publicApi() {
// 同一 IP 每分钟最多 30 次
return ResultVO.success("公开接口");
}
获取真实 IP(经过 Nginx 代理时):
java
public static String getRealIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理时取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
七、限流配置管理
把限流参数放在配置中心或 yml 中,不用改代码:
yaml
rate-limit:
defaults:
max: 10
window: 1
rules:
- key: "createOrder"
max: 5
window: 10
- key: "seckill"
max: 1
window: 10
- key: "publicApi"
max: 30
window: 60
java
@ConfigurationProperties(prefix = "rate-limit")
@Data
public class RateLimitProperties {
private Rule defaults = new Rule();
private List<Rule> rules = new ArrayList<>();
@Data
public static class Rule {
private String key;
private long max;
private long window;
}
}
八、限流效果验证
bash
# 快速请求测试限流效果
for i in {1..20}; do
curl -X POST http://localhost:9090/api/order
echo ""
done
# 正常返回:{"code":200,"message":"下单成功"}
# 被限返回:{"code":429,"message":"请求太频繁,请稍后重试"}
九、限流 vs 熔断 vs 降级
| 限流 Ratelimit | 熔断 CircuitBreaker | 降级 Degrade | |
|---|---|---|---|
| 作用 | 控制请求速率 | 防止故障扩散 | 舍弃非核心功能 |
| 时机 | 请求进来时 | 错误率超标时 | 资源不足时 |
| 工具 | Redis + Lua | Sentinel、Resilience4j | Sentinel、Hystrix |
建议: 限流是必须的,熔断和降级在大流量的核心系统上建议加上。
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。