本文目录导读:

我来为您提供一个完整的Spring Boot接口限流案例,包含多种实现方式。
项目依赖配置
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis (用于分布式限流) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
自定义限流注解
package com.example.ratelimiter.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 限流注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流key
*/
String key() default "rate:limit:";
/**
* 限流时间窗口(秒)
*/
int timeWindow() default 60;
/**
* 时间窗口内最大请求数
*/
int limit() default 100;
/**
* 限流类型
*/
RateLimitType type() default RateLimitType.TOKEN_BUCKET;
/**
* 限流提示信息
*/
String message() default "系统繁忙,请稍后再试";
enum RateLimitType {
/**
* 计数器限流
*/
COUNTER,
/**
* 令牌桶限流
*/
TOKEN_BUCKET,
/**
* 滑动窗口限流
*/
SLIDING_WINDOW,
/**
* 漏桶限流
*/
LEAKY_BUCKET
}
}
限流实现类
1 计数器限流(单机版)
package com.example.ratelimiter.limiter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 计数器限流器(单机版)
*/
@Slf4j
@Component
public class CounterRateLimiter {
// 存储每个key的计数信息
private final ConcurrentHashMap<String, AtomicLong> counters = new ConcurrentHashMap<>();
// 存储每个key的窗口开始时间
private final ConcurrentHashMap<String, Long> windowStartTimes = new ConcurrentHashMap<>();
/**
* 是否允许请求
*/
public boolean tryAcquire(String key, int limit) {
long currentTime = System.currentTimeMillis();
long windowSize = 1000L; // 1秒窗口
// 获取或创建计数器
AtomicLong counter = counters.computeIfAbsent(key, k -> new AtomicLong(0));
// 获取或创建窗口开始时间
Long windowStartTime = windowStartTimes.putIfAbsent(key, currentTime);
if (windowStartTime == null) {
windowStartTime = currentTime;
}
// 如果是新的时间窗口,重置计数器和开始时间
if (currentTime - windowStartTime >= windowSize) {
synchronized (this) {
if (currentTime - windowStartTimes.get(key) >= windowSize) {
counter.set(0);
windowStartTimes.put(key, currentTime);
}
}
}
// 判断是否超过限制
long currentCount = counter.incrementAndGet();
return currentCount <= limit;
}
/**
* 清理过期数据
*/
public void cleanup() {
long currentTime = System.currentTimeMillis();
counters.entrySet().removeIf(entry ->
currentTime - windowStartTimes.getOrDefault(entry.getKey(), 0L) >= 10000L
);
windowStartTimes.entrySet().removeIf(entry ->
currentTime - entry.getValue() >= 10000L
);
}
}
2 令牌桶限流器
package com.example.ratelimiter.limiter;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
/**
* 令牌桶限流器(单机版)
*/
@Slf4j
public class TokenBucketRateLimiter {
private final ConcurrentHashMap<String, TokenBucket> buckets = new ConcurrentHashMap<>();
/**
* 创建或获取令牌桶
*/
private TokenBucket getBucket(String key, int capacity) {
return buckets.computeIfAbsent(key, k -> new TokenBucket(capacity));
}
/**
* 是否允许请求
*/
public boolean tryAcquire(String key, int capacity, int refillRate) {
TokenBucket bucket = getBucket(key, capacity);
return bucket.tryConsume(refillRate);
}
/**
* 令牌桶内部类
*/
private static class TokenBucket {
private final int capacity; // 桶容量
private double tokens; // 当前令牌数
private long lastRefillTime; // 上次填充时间
public TokenBucket(int capacity) {
this.capacity = capacity;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
}
/**
* 尝试消耗一个令牌
*/
public synchronized boolean tryConsume(double refillRate) {
refill(refillRate);
if (tokens >= 1) {
tokens -= 1;
return true;
}
return false;
}
/**
* 填充令牌
*/
private void refill(double refillRate) {
long now = System.currentTimeMillis();
double tokensToAdd = (now - lastRefillTime) / 1000.0 * refillRate;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
}
}
3 Redis滑动窗口限流器
package com.example.ratelimiter.limiter;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 基于Redis的滑动窗口限流器(分布式)
*/
@Component
@RequiredArgsConstructor
public class RedisSlidingWindowRateLimiter {
private final RedisTemplate<String, String> redisTemplate;
// Lua脚本:原子性实现滑动窗口计数
private static final String SLIDING_WINDOW_SCRIPT =
"local key = KEYS[1] " +
"local now = tonumber(ARGV[1]) " +
"local windowSize = tonumber(ARGV[2]) " +
"local limit = tonumber(ARGV[3]) " +
"local windowStart = now - windowSize * 1000 " +
"" +
"redis.call('ZREMRANGEBYSCORE', key, 0, windowStart) " +
"" +
"local currentCount = redis.call('ZCARD', key) " +
"if currentCount < limit then " +
" redis.call('ZADD', key, now, now .. '-' .. math.random(1, 999999)) " +
" redis.call('PEXPIRE', key, windowSize * 1000) " +
" return 1 " +
"else " +
" return 0 " +
"end";
/**
* 尝试通过滑动窗口
*/
public boolean tryAcquire(String key, int windowSize, int limit) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(SLIDING_WINDOW_SCRIPT, Long.class);
List<String> keys = Collections.singletonList(key);
Object[] args = new Object[]{
String.valueOf(System.currentTimeMillis()),
String.valueOf(windowSize),
String.valueOf(limit)
};
Long result = redisTemplate.execute(script, keys, args);
return result != null && result == 1;
}
}
4 漏桶限流器
package com.example.ratelimiter.limiter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
/**
* 漏桶限流器(单机版)
*/
public class LeakyBucketRateLimiter {
private final ConcurrentHashMap<String, LeakyBucket> buckets = new ConcurrentHashMap<>();
/**
* 是否允许请求
*/
public boolean tryAcquire(String key, int capacity, double leakRate) {
LeakyBucket bucket = buckets.computeIfAbsent(key, k -> new LeakyBucket(capacity, leakRate));
return bucket.tryAcquire();
}
/**
* 漏桶内部类
*/
private static class LeakyBucket {
private final int capacity; // 桶容量(缓存请求数)
private final double leakRate; // 漏出速率(每秒)
private AtomicInteger water; // 当前水量(请求数)
private AtomicLong lastLeakTime; // 上次漏出时间
public LeakyBucket(int capacity, double leakRate) {
this.capacity = capacity;
this.leakRate = leakRate;
this.water = new AtomicInteger(0);
this.lastLeakTime = new AtomicLong(System.currentTimeMillis());
}
/**
* 尝试获取
*/
public synchronized boolean tryAcquire() {
// 先漏水
leak();
// 检查水位
if (water.get() < capacity) {
water.incrementAndGet();
return true;
}
return false;
}
/**
* 漏水操作
*/
private void leak() {
long now = System.currentTimeMillis();
long lastTime = lastLeakTime.get();
double elapsedTime = (now - lastTime) / 1000.0;
int leakedWater = (int) (elapsedTime * leakRate);
if (leakedWater > 0) {
water.accumulateAndGet(-leakedWater, (current, delta) ->
Math.max(0, current + delta)
);
lastLeakTime.set(now);
}
}
}
}
AOP拦截器实现
package com.example.ratelimiter.aspect;
import com.example.ratelimiter.annotation.RateLimit;
import com.example.ratelimiter.exception.RateLimitException;
import com.example.ratelimiter.limiter.TokenBucketRateLimiter;
import com.example.ratelimiter.limiter.RedisSlidingWindowRateLimiter;
import com.example.ratelimiter.utils.IPUtils;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* 限流切面
*/
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
private final RedisTemplate<String, String> redisTemplate;
private final TokenBucketRateLimiter tokenBucketRateLimiter;
private final RedisSlidingWindowRateLimiter redisSlidingWindowRateLimiter;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取限流key
String key = buildKey(joinPoint, rateLimit);
// 根据限流类型执行不同的限流策略
boolean allowed = checkLimit(key, rateLimit);
if (!allowed) {
throw new RateLimitException(rateLimit.message());
}
return joinPoint.proceed();
}
/**
* 构建限流key
*/
private String buildKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
StringBuilder keyBuilder = new StringBuilder(rateLimit.key());
// 获取当前方法名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
keyBuilder.append(signature.getDeclaringTypeName())
.append(".")
.append(signature.getName());
// 获取IP地址(如果配置了IP限流)
if (rateLimit.type() == RateLimit.RateLimitType.COUNTER) {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
keyBuilder.append(":").append(IPUtils.getClientIp(request));
}
}
return keyBuilder.toString();
}
/**
* 检查是否允许请求
*/
private boolean checkLimit(String key, RateLimit rateLimit) {
boolean allowed;
switch (rateLimit.type()) {
case TOKEN_BUCKET:
// 令牌桶:每秒填充rateLimit.limit()/rateLimit.timeWindow()个令牌
double refillRate = (double) rateLimit.limit() / rateLimit.timeWindow();
allowed = tokenBucketRateLimiter.tryAcquire(key, rateLimit.limit(), refillRate);
break;
case SLIDING_WINDOW:
// Redis滑动窗口
allowed = redisSlidingWindowRateLimiter.tryAcquire(key, rateLimit.timeWindow(), rateLimit.limit());
break;
case COUNTER:
// 计数器
allowed = counterLimit(key, rateLimit);
break;
case LEAKY_BUCKET:
// 漏桶
allowed = leakyBucketLimit(key, rateLimit);
break;
default:
allowed = true;
break;
}
return allowed;
}
/**
* 计数器限流
*/
private boolean counterLimit(String key, RateLimit rateLimit) {
// 使用Redis INCR实现计数器
String counterKey = "rate:limit:counter:" + key;
Long count = redisTemplate.opsForValue().increment(counterKey);
if (count != null && count == 1) {
redisTemplate.expire(counterKey, rateLimit.timeWindow(), java.util.concurrent.TimeUnit.SECONDS);
}
return count != null && count <= rateLimit.limit();
}
/**
* 漏桶限流
*/
private boolean leakyBucketLimit(String key, RateLimit rateLimit) {
// 漏桶实现(这里简化,实际可以复用漏桶类)
return true;
}
}
自定义异常
package com.example.ratelimiter.exception;
/**
* 限流异常
*/
public class RateLimitException extends RuntimeException {
public RateLimitException(String message) {
super(message);
}
public RateLimitException(String message, Throwable cause) {
super(message, cause);
}
}
全局异常处理
package com.example.ratelimiter.handler;
import com.example.ratelimiter.exception.RateLimitException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RateLimitException.class)
public ResponseEntity<Map<String, Object>> handleRateLimitException(RateLimitException e) {
log.warn("触发限流: {}", e.getMessage());
Map<String, Object> response = new HashMap<>();
response.put("code", 429);
response.put("message", e.getMessage());
response.put("success", false);
response.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(response);
}
}
测试控制器
package com.example.ratelimiter.controller;
import com.example.ratelimiter.annotation.RateLimit;
import com.example.ratelimiter.annotation.RateLimit.RateLimitType;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 测试控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/test")
public class TestController {
/**
* 计数器限流测试
* - 60秒内最多100次请求
*/
@GetMapping("/counter")
@RateLimit(
key = "test:counter:",
timeWindow = 60,
limit = 100,
type = RateLimitType.COUNTER,
message = "请求过于频繁,请稍后再试"
)
public Map<String, Object> counterTest() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "计数器限流测试成功");
result.put("timestamp", System.currentTimeMillis());
return result;
}
/**
* 令牌桶限流测试
* - 速率:每秒10个令牌
* - 容量:100个
*/
@GetMapping("/token")
@RateLimit(
key = "test:token:",
timeWindow = 10, // 10秒
limit = 100, // 100个令牌
type = RateLimitType.TOKEN_BUCKET,
message = "服务繁忙,请稍后重试"
)
public Map<String, Object> tokenBucketTest() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "令牌桶限流测试成功");
result.put("timestamp", System.currentTimeMillis());
return result;
}
/**
* Redis滑动窗口限流测试
* - 60秒窗口内最多50次请求
*/
@GetMapping("/sliding")
@RateLimit(
key = "test:sliding:",
timeWindow = 60,
limit = 50,
type = RateLimitType.SLIDING_WINDOW,
message = "请求过多,请稍后再试"
)
public Map<String, Object> slidingWindowTest() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "滑动窗口限流测试成功");
result.put("timestamp", System.currentTimeMillis());
return result;
}
/**
* 不加限流的测试接口
*/
@GetMapping("/normal")
public Map<String, Object> normalTest() throws InterruptedException {
// 模拟业务处理
TimeUnit.MILLISECONDS.sleep(100);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "正常请求成功");
result.put("timestamp", System.currentTimeMillis());
return result;
}
}
配置类
package com.example.ratelimiter.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 设置序列化器
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
工具类(IP获取)
package com.example.ratelimiter.utils;
import javax.servlet.http.HttpServletRequest;
/**
* IP工具类
*/
public class IPUtils {
/**
* 获取客户端IP地址
*/
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
定时清理任务(可选)
package com.example.ratelimiter.task;
import com.example.ratelimiter.limiter.CounterRateLimiter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 定时清理任务
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CleanupTask {
private final CounterRateLimiter counterRateLimiter;
/**
* 每天凌晨2点清理过期数据
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupExpiredData() {
log.info("开始清理限流过期数据");
counterRateLimiter.cleanup();
log.info("清理限流过期数据完成");
}
}
使用示例
package com.example.ratelimiter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 启动类
*/
@SpringBootApplication
@EnableScheduling
public class RateLimiterApplication {
public static void main(String[] args) {
SpringApplication.run(RateLimiterApplication.class, args);
System.out.println("限流应用启动成功!");
System.out.println("测试接口:");
System.out.println(" - http://localhost:8080/api/test/counter (计数器限流)");
System.out.println(" - http://localhost:8080/api/test/token (令牌桶限流)");
System.out.println(" - http://localhost:8080/api/test/sliding (滑动窗口限流)");
System.out.println(" - http://localhost:8080/api/test/normal (普通接口)");
}
}
测试脚本(并发测试)
# 使用 curl 并发测试
for i in $(seq 1 200); do
curl -s -o /dev/null -w "请求$i: HTTP %{http_code}\n" http://localhost:8080/api/test/counter &
done
wait
这个完整的限流案例包含了:
- 四种限流算法:计数器、令牌桶、滑动窗口、漏桶
- 多种限流实现:单机版(本地内存)和分布式版(Redis)
- AOP切面实现:无需修改业务代码,通过注解即可实现限流
- 自定义注解:灵活配置限流参数
- 全局异常处理:统一处理限流异常
- 完整测试示例:包含测试控制器和测试脚本
可以根据实际需求选择合适的限流方式:
- 单机应用:使用本地内存实现
- 分布式应用:使用Redis实现
- 对精度要求高:使用令牌桶或滑动窗口
- 业务突发流量:使用漏桶算法