Redis 限流
1. 为什么需要限流
Section titled “1. 为什么需要限流”限流(Rate Limiting)是保护系统的重要手段,防止突发流量压垮服务:
| 场景 | 问题 | 限流手段 |
|---|---|---|
| 接口被刷 | 恶意请求耗尽资源 | 按 IP / 用户限流 |
| 秒杀活动 | 瞬间洪峰压垮 DB | 按接口全局限流 |
| 第三方 API | 超出调用配额 | 按服务限流 |
| 短信验证码 | 短信轰炸 | 按手机号限流 |
2. 四种限流算法对比
Section titled “2. 四种限流算法对比”固定窗口: [0s ─────────────── 60s] 允许 100 次 ↑ 窗口重置,计数归零
滑动窗口: ←────── 60s 滑动 ──────→ 始终统计最近 60s 内的请求数
令牌桶: 桶(容量=100) ←─── 匀速生成令牌 ←─── 请求消耗令牌 允许突发(桶满时可瞬间消耗)
漏桶: 请求 ──▶ [桶] ──▶ 匀速流出 ──▶ 服务 强制匀速,完全不允许突发| 算法 | 突发流量 | 精度 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | ❌ 边界双倍流量 | 低 | 简单 | 粗粒度限流 |
| 滑动窗口 | 一般 | 高 | 中等 | 接口限流 |
| 令牌桶 | ✅ 允许适度突发 | 高 | 较复杂 | API 网关、通用限流 |
| 漏桶 | ❌ 完全平滑 | 高 | 较复杂 | 强制匀速场景 |
3. 方案一:固定窗口(INCR + EXPIRE)
Section titled “3. 方案一:固定窗口(INCR + EXPIRE)”最简单的实现,适合粗粒度限流场景:
@Service@RequiredArgsConstructorpublic class FixedWindowRateLimiter {
private final StringRedisTemplate redisTemplate;
/** * 固定窗口限流 * @param key 限流维度(如 "limit:sms:13800138000") * @param limit 窗口内允许的最大请求数 * @param window 窗口大小(秒) * @return true=允许,false=被限流 */ public boolean isAllowed(String key, int limit, int window) { Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) { // 第一次请求,设置过期时间(窗口开始计时) redisTemplate.expire(key, window, TimeUnit.SECONDS); }
return count <= limit; }}4. 方案二:滑动窗口(ZSet + Lua)
Section titled “4. 方案二:滑动窗口(ZSet + Lua)”用 ZSet 记录请求时间戳,每次请求时删除窗口外的记录,统计当前窗口内的请求数:
@Service@RequiredArgsConstructorpublic class SlidingWindowRateLimiter {
private final StringRedisTemplate redisTemplate;
// Lua 脚本保证原子性 private static final String SLIDING_WINDOW_SCRIPT = """ local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local limit = tonumber(ARGV[3]) local windowStart = now - window * 1000
-- 删除窗口外的过期记录 redis.call('ZREMRANGEBYSCORE', key, '-inf', windowStart)
-- 统计当前窗口内的请求数 local count = redis.call('ZCARD', key)
if count < limit then -- 未超限:记录本次请求(score = 时间戳,member = 时间戳+随机数保证唯一) redis.call('ZADD', key, now, now .. math.random()) redis.call('EXPIRE', key, window + 1) return 1 -- 允许 else return 0 -- 拒绝 end """;
private final RedisScript<Long> slidingWindowScript = new DefaultRedisScript<>(SLIDING_WINDOW_SCRIPT, Long.class);
public boolean isAllowed(String key, int limit, int windowSeconds) { long now = System.currentTimeMillis();
Long result = redisTemplate.execute( slidingWindowScript, Collections.singletonList(key), String.valueOf(now), String.valueOf(windowSeconds), String.valueOf(limit) );
return Long.valueOf(1L).equals(result); }}滑动窗口工作示意:
时间轴:──────────────────────────────────────────────────▶ [────────── 60s 窗口 ──────────] | | 窗口起点 当前时间 自动删除窗口外的请求记录 统计窗口内剩余请求数 → 判断是否超限5. 方案三:Redisson 令牌桶(推荐)
Section titled “5. 方案三:Redisson 令牌桶(推荐)”Redisson 内置令牌桶实现(RRateLimiter),线程安全,支持分布式,无需手写 Lua 脚本:
@Configurationpublic class RateLimiterConfig {
@Bean public RRateLimiter smsRateLimiter(RedissonClient redissonClient) { RRateLimiter limiter = redissonClient.getRateLimiter("rl:sms:global"); // 令牌生成速率:每秒 10 个令牌,桶容量 10 limiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS); return limiter; }}@Service@RequiredArgsConstructorpublic class SmsService {
private final RedissonClient redissonClient;
public void sendSms(String phone) { // 全局限流 RRateLimiter globalLimiter = redissonClient.getRateLimiter("rl:sms:global");
// 按手机号限流(每分钟最多 1 次) RRateLimiter phoneLimiter = redissonClient.getRateLimiter("rl:sms:" + phone); phoneLimiter.trySetRate(RateType.OVERALL, 1, 1, RateIntervalUnit.MINUTES);
// 非阻塞尝试获取令牌 if (!globalLimiter.tryAcquire()) { throw new RuntimeException("系统繁忙,请稍后重试"); } if (!phoneLimiter.tryAcquire()) { throw new RuntimeException("发送过于频繁,请 1 分钟后再试"); }
doSendSms(phone); }}RRateLimiter 常用 API:
// 初始化:整体限流(OVERALL)或单客户端限流(PER_CLIENT)limiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);
// 获取 1 个令牌(非阻塞,失败返回 false)boolean ok = limiter.tryAcquire();
// 获取 N 个令牌(非阻塞)boolean ok = limiter.tryAcquire(3);
// 获取令牌,最多等待 500ms(阻塞)boolean ok = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
// 阻塞直到获取到令牌limiter.acquire();6. 封装限流注解
Section titled “6. 封装限流注解”@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface RateLimit { String key() default ""; // SpEL,空则用方法签名 int rate() default 100; // 每个时间窗口允许的请求数 int interval() default 1; // 时间窗口大小 RateIntervalUnit unit() default RateIntervalUnit.SECONDS; String message() default "请求过于频繁,请稍后重试";}@Aspect@Component@RequiredArgsConstructorpublic class RateLimitAspect {
private final RedissonClient redissonClient; private final ExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { String key = buildKey(joinPoint, rateLimit);
RRateLimiter limiter = redissonClient.getRateLimiter(key); limiter.trySetRate(RateType.OVERALL, rateLimit.rate(), rateLimit.interval(), rateLimit.unit());
if (!limiter.tryAcquire()) { throw new RuntimeException(rateLimit.message()); }
return joinPoint.proceed(); }
private String buildKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) { if (!rateLimit.key().isEmpty()) { // 解析 SpEL MethodSignature sig = (MethodSignature) joinPoint.getSignature(); EvaluationContext ctx = new StandardEvaluationContext(); String[] names = sig.getParameterNames(); Object[] args = joinPoint.getArgs(); for (int i = 0; i < names.length; i++) ctx.setVariable(names[i], args[i]); return "rl:" + parser.parseExpression(rateLimit.key()).getValue(ctx, String.class); } // 默认:类名.方法名 MethodSignature sig = (MethodSignature) joinPoint.getSignature(); return "rl:" + sig.getDeclaringTypeName() + "." + sig.getName(); }}// 使用@RestControllerpublic class SmsController {
// 全局:每秒最多 10 次 @RateLimit(rate = 10, interval = 1) @PostMapping("/sms/send") public void send(@RequestParam String phone) { ... }
// 按手机号:每分钟最多 1 次 @RateLimit(key = "#phone", rate = 1, interval = 1, unit = RateIntervalUnit.MINUTES, message = "发送过于频繁,请 1 分钟后重试") @PostMapping("/sms/sendByPhone") public void sendByPhone(@RequestParam String phone) { ... }}7. 限流方案总结
Section titled “7. 限流方案总结”| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 简单接口粗粒度限流 | 固定窗口(INCR) | 实现简单,够用 |
| 精确接口限流(防边界问题) | 滑动窗口(ZSet + Lua) | 精度高,无边界双倍问题 |
| 允许适度突发的限流 | Redisson RRateLimiter | 令牌桶,功能完善,推荐 |
| 强制匀速输出 | 漏桶(Redisson + 自定义实现) | 少见,多用于流量整形 |