本文目录导读:

Java案例如何实现请求防刷?从原理到实战的完整指南
目录导读
- 什么是请求防刷?为什么需要它?
- 常见防刷机制原理对比
- Java实现防刷的三种核心方案
- 实战案例:基于令牌桶+IP限流+时间窗口
- 高频问题FAQ:防刷踩坑与优化
- 如何选择最适合的防刷策略
什么是请求防刷?为什么需要它?
Q:什么是请求防刷?
请求防刷(Anti-Crawler / Rate Limiting)是指通过技术手段限制客户端在单位时间内的请求次数,防止恶意用户、爬虫或攻击者通过高并发请求耗尽服务器资源、窃取数据或破坏业务逻辑。
Q:为什么必须做防刷?
- 资源保护:防止API被高频调用导致数据库连接池耗尽、CPU爆满。
- 业务安全:避免短信验证码接口被刷(如1秒发送100条验证码,造成经济损失)。
- 数据安全:防止爬虫批量抓取用户信息、商品价格等核心数据。
- 合规要求:部分平台(如电商、金融)对接口访问频率有强制限制。
常见防刷机制原理对比
| 机制 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 计数器(固定窗口) | 在固定时间窗口(如1分钟)内计数,超过阈值则拒绝 | 简单易实现 | 临界值问题(如第59秒大量请求,刷新窗口) |
| 滑动窗口 | 将时间窗口划分为更小的时间片,动态统计 | 更平滑 | 需要存储多个时间片数据,内存开销稍大 |
| 令牌桶(Token Bucket) | 以固定速率生成令牌,请求需消耗令牌 | 允许突发流量 | 需维护令牌生成器 |
| 漏桶(Leaky Bucket) | 请求进入桶内以固定速率流出 | 稳定输出 | 无法应对突发流量 |
| IP黑/白名单 | 直接封锁异常IP | 快速阻断 | 误封风险高,动态IP难处理 |
| 设备指纹+行为分析 | 基于浏览器指纹、操作间隔等综合判断 | 准确率高 | 实现复杂,需算法支持 |
Q:以上机制中,Java最推荐哪种?
对于中小型项目,令牌桶+IP级别的滑动窗口是最平衡的方案,令牌桶可应对业务高峰,滑动窗口能精确控制单一IP的请求频率。
Java实现防刷的三种核心方案
基于本地内存(Guava RateLimiter)
适用于单机场景,无需外部中间件,但无法跨服务共享限流状态。
// 使用Guava的RateLimiter实现令牌桶
import com.google.common.util.concurrent.RateLimiter;
public class LocalRateLimiter {
private static final RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒10个令牌
public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
}
缺陷:分布式部署时,每台机器独立限流,总QPS可能乘数级超限。
基于Redis + Lua脚本(推荐)
为什么选Redis Lua?
- 原子性:Lua脚本在Redis中执行时不会被其他命令打断。
- 高性能:纯内存操作,单机可达10万+QPS。
- 分布式共享:所有应用节点共用同一限流数据。
核心实现步骤:
- 设计滑动窗口数据结构:使用ZSET(有序集合),以请求时间戳为score,IP+接口路径为key。
- 清理过期的窗口数据(如1秒前的时间戳)。
- 统计当前窗口内的请求数,若超阈值则返回拒绝。
Q:为什么不直接用Redis的INCR+TTL?
INCR+TTL属于固定窗口,存在临界值问题(如59秒时大量请求+重刷窗口),滑动窗口用ZSET更精确,但需要清理过期元素,性能低于纯计数方案——建议根据业务容忍度选择。
基于网关层(Spring Cloud Gateway / Zuul)
网关统一拦截,方便集中管理策略,适合微服务架构。
# Spring Cloud Gateway结合Redis请求限制
spring:
cloud:
gateway:
routes:
- id: api_limiter
uri: lb://service
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
实战案例:基于令牌桶+IP限流+时间窗口
场景:一个用户信息查询接口 /api/user/info,要求每个IP每10秒最多请求5次,突发流量可容忍15次/秒。
完整代码(Spring Boot + Redis + Lua):
Lua限流脚本(sliding_window_limiter.lua)
-- KEYS[1]: 限流键(如 "rate_limiter:192.168.1.1:/api/user/info")
-- ARGV[1]: 窗口大小(秒),如10
-- ARGV[2]: 限制次数,如5
-- ARGV[3]: 当前时间戳(毫秒)
local key = KEYS[1]
local window = tonumber(ARGV[1]) * 1000 -- 转为毫秒
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local min_score = now - window
-- 移除窗口外的历史记录
redis.call(‘ZREMRANGEBYSCORE’, key, 0, min_score)
-- 统计当前窗口内请求数
local count = redis.call(‘ZCARD’, key)
-- 判断是否超限
if count and count >= limit then
-- 返回剩余时间(下次允许请求的时间)
local oldest = redis.call(‘ZRANGEBYSCORE’, key, min_score, now, ‘LIMIT’, 0, 1)
local next_time = oldest[1] + window
return {0, math.ceil((next_time - now) / 1000)}
else
redis.call(‘ZADD’, key, now, now)
redis.call(‘EXPIRE’, key, ARGV[1] + 1) -- 设置过期时间,避免僵尸key
return {1, 0}
end
Java调用类
@Component
public class RateLimiterService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LUA_SCRIPT = new ClassPathResource(“sliding_window_limiter.lua”).getPath();
public boolean checkRate(String ip, String apiPath, int windowSeconds, int limit) {
DefaultRedisScript<List> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(“sliding_window_limiter.lua”)));
redisScript.setResultType(List.class);
String key = “rate_limiter:” + ip + “:” + apiPath;
Long now = System.currentTimeMillis();
List<Long> result = redisTemplate.execute(redisScript,
Collections.singletonList(key),
String.valueOf(windowSeconds),
String.valueOf(limit),
String.valueOf(now));
if (result != null && result.get(0) == 1) {
return true; // 允许通过
} else {
// 可抛出异常或返回自定义拒绝信息
throw new RateLimitException(“请求过于频繁,请 ” + result.get(1) + “ 秒后重试”);
}
}
}
在Controller中使用
@RestController
public class UserController {
@Autowired
private RateLimiterService rateLimiter;
@GetMapping(“/api/user/info”)
public Result getUserInfo(HttpServletRequest request) {
String clientIp = request.getRemoteAddr();
rateLimiter.checkRate(clientIp, “/api/user/info”, 10, 5);
// 业务逻辑
return Result.success(userService.getInfo());
}
}
Q:这个方案如何处理突发流量?
上述脚本是严格的滑动窗口,不允许任何突发,若要允许突发(如每秒限5次,但允许单次瞬时10次),可改用 令牌桶+Redis ZSET:令牌生成速率为5个/秒,桶容量为10,需单独维护一个ZSET记录令牌消耗情况。
高频问题FAQ:防刷踩坑与优化
Q1:分布式部署下,多台服务器时间不同步怎么办?
解决方案:
- 统一使用Redis的时间戳(通过
TIME命令获取)。 - 所有节点时间必须通过NTP同步(误差控制在100ms内)。
- 建议在Lua脚本中传递Redis时间:
local redis_time = redis.call(‘TIME’)[1]。
Q2:对移动端APP接口防刷,IP限流不够准确怎么办?
优化:
- 改用 用户ID + Token 作为限流key(注意防Token伪造)。
- 结合 设备指纹(如设备ID、UA+MAC哈希)。
- 对非登录接口(如验证码),使用 IP+来源渠道+时间片 加权。
Q3:限流后如何给用户友好提示?
不要直接返回空白或500错误,可以:
- HTTP 429 Too Many Requests。
- JSON返回:
{“code”:429, “message”:“操作太频繁,请稍后再试”, “retryAfter”: 5}。 - 前端根据
retryAfter倒计时禁用按钮。
Q4:如何实时监控防刷命中情况?
- 在Lua脚本返回结果后,异步发送日志到Kafka/ELK。
- 统计每个接口的拒绝率、被刷IP TOP10。
- 设置告警:当某接口拒绝率超过20%时,钉钉/邮件通知运维。
Q5:防刷性能损耗大吗?
Redis ZSET的写操作(ZADD)时间复杂度是O(log N),当窗口内请求量极大时(如单IP每秒数万次),可改用 滑动窗口优化版:只存储时间片哈希(如每秒记一个count),而非每条请求记录一个元素,推荐用Redisson的RRateLimiter,其底层使用令牌桶+哈希结构,性能更优。
如何选择最适合的防刷策略
| 项目规模 | 推荐方案 | 理由 |
|---|---|---|
| 单体应用、低并发(<1000 QPS) | Guava RateLimiter + IP黑名单 | 简单快速,无外部依赖 |
| 微服务、中等并发(<10万 QPS) | Redis Lua滑动窗口 + 网关过滤器 | 分布式共享,精确控制 |
| 高并发、复杂业务(>10万 QPS) | 网关层+Redis漏斗+设备指纹 | 分层限流(网关+业务层+算法层) |
最后提醒:防刷不是一劳永逸的,攻击手段会不断演化(如代理池、模拟浏览器行为),建议采用“基础限流(IP+频率)+ 异常检测(行为分析) + 人工申诉”的三层防御体系,同时保留正常的API调用通道。
文章结束