【SpringBoot 项目使用 Redis 对用户 IP 进行接口限流
·
固定窗口限流实现
基于Redis的固定窗口计数器算法,简单粗暴但存在临界问题。核心是利用Redis的incr和过期时间控制单位时间内的请求量。
@RestController
public class FixedWindowController {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LIMIT_KEY_PREFIX = "limit:fixed:";
private static final int MAX_REQUESTS = 100;
private static final int WINDOW_SECONDS = 60;
@GetMapping("/api")
public ResponseEntity<String> api(@RequestHeader String clientIp) {
String key = LIMIT_KEY_PREFIX + clientIp;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, WINDOW_SECONDS, TimeUnit.SECONDS);
}
if (count > MAX_REQUESTS) {
return ResponseEntity.status(429).body("Too many requests");
}
return ResponseEntity.ok("Success");
}
}
滑动窗口限流实现
通过Redis的ZSet实现精确的滑动窗口控制,解决固定窗口的临界突刺问题,但消耗更多内存。
@RestController
public class SlidingWindowController {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LIMIT_KEY_PREFIX = "limit:sliding:";
private static final int MAX_REQUESTS = 100;
private static final int WINDOW_SECONDS = 60;
@GetMapping("/api")
public ResponseEntity<String> api(@RequestHeader String clientIp) {
long now = System.currentTimeMillis();
long windowStart = now - WINDOW_SECONDS * 1000L;
String key = LIMIT_KEY_PREFIX + clientIp;
redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);
long currentCount = redisTemplate.opsForZSet().zCard(key);
if (currentCount >= MAX_REQUESTS) {
return ResponseEntity.status(429).body("Too many requests");
}
redisTemplate.opsForZSet().add(key, String.valueOf(now), now);
redisTemplate.expire(key, WINDOW_SECONDS, TimeUnit.SECONDS);
return ResponseEntity.ok("Success");
}
}
分布式令牌桶实现
结合Lua脚本保证原子性,实现平滑的流量控制。适合需要平稳处理突发流量的场景。
@RestController
public class TokenBucketController {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LIMIT_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" +
"local last_tokens = tonumber(redis.call('get', key) or capacity)\n" +
"local last_refreshed = tonumber(redis.call('get', key..':ts') or now)\n" +
"local delta = math.max(0, now - last_refreshed)\n" +
"local new_tokens = math.min(capacity, last_tokens + delta * rate)\n" +
"local allowed = new_tokens >= requested\n" +
"local result = 0\n" +
"if allowed then\n" +
" new_tokens = new_tokens - requested\n" +
" redis.call('set', key, new_tokens)\n" +
" redis.call('set', key..':ts', now)\n" +
" result = 1\n" +
"end\n" +
"return result";
@GetMapping("/api")
public ResponseEntity<String> api(@RequestHeader String clientIp) {
String key = "limit:token:" + clientIp;
Long result = redisTemplate.execute(
new DefaultRedisScript<>(LIMIT_SCRIPT, Long.class),
Collections.singletonList(key),
"10", "100", String.valueOf(System.currentTimeMillis() / 1000), "1"
);
if (result == 0) {
return ResponseEntity.status(429).body("Too many requests");
}
return ResponseEntity.ok("Success");
}
}
生产环境注意事项
-
Redis集群模式:所有方案在集群环境下需要确保相同IP的请求路由到同一节点,可通过hash tag实现,如
{limit}:user1 -
限流维度扩展:实际业务中可能需要组合用户ID+接口URI作为key,例如
limit:/order:user123 -
性能监控:建议通过AOP记录限流触发情况,关键指标包括:
- 请求总数
- 被限流数
- 限流key分布
-
动态配置:将规则配置存储在数据库或配置中心,实现运行时调整:
@RefreshScope
@Configuration
public class RateLimitConfig {
@Value("${rate.limit.max:100}")
private int maxRequests;
// getter省略
}
-
降级策略:达到限流阈值时可考虑返回缓存数据或排队机制,避免简单返回429
-
多级限流:结合Nginx层的基础限流和业务层的精细限流,形成多级防护
-
白名单机制:对内部IP或特殊用户免除限流
private boolean isWhitelist(String ip) {
return Arrays.asList("10.0.0.1", "192.168.1.100").contains(ip);
}
更多推荐
所有评论(0)