接口被狂刷怎么处理
接口被狂刷怎么处理
面试官您好,关于接口被狂刷的问题,我会从 "紧急止损→多层防御→监控复盘" 三个阶段系统性处理,形成完整的防护闭环👇
🚨 第一步:紧急止损(先保命!黄金 3 分钟)
- 快速扩容:立刻将服务实例数拉满,利用云厂商弹性伸缩能力扛住峰值
- 熔断降级:非核心接口直接返回默认值 / 降级页,核心接口开启熔断保护
- 流量清洗:接入 CDN/WAF,过滤掉明显恶意请求(相同 IP / 设备号高频访问)
- 临时封禁:对异常 IP、UID、设备号执行 15 分钟 - 24 小时临时封禁
🛡️ 第二步:构建多层防御体系(长治久安)
这是核心!单一防护必被突破,必须做到层层设防、纵深防御
⚡ 核心技术:四大限流算法对比
这是面试必问加分项,必须说清各自适用场景
| 算法名称 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 固定窗口计数器 | 统计单位时间内请求数,超过阈值则拒绝 | 实现简单、性能好 | 存在临界问题(窗口切换时可能出现 2 倍流量) | 要求不高的简单限流 |
| 滑动窗口计数器 | 将时间窗口拆分为多个小格子,滑动统计 | 解决了临界问题 | 实现稍复杂,需要存储多个格子的计数 | 大多数业务场景 |
| 漏桶算法 | 请求像水一样进入漏桶,以固定速率流出 | 强制匀速处理,平滑突发流量 | 无法应对突发流量,桶满直接丢弃 | 消息队列削峰填谷 |
| 令牌桶算法 | 以固定速率生成令牌,请求需要拿到令牌才能执行 | 既能限流又能应对突发流量 | 实现相对复杂 | 互联网接口限流(首选) |
💡 大厂首选:Redis+Lua 实现分布式令牌桶算法,或者直接使用 Sentinel/Resilience4j 框架
✨ 进阶优化技巧(拉开差距的加分项)
- 分布式限流:网关层做全局限流,应用层做接口级限流,避免单点限流失效
- 热点隔离:将热点接口单独部署在独立集群,避免拖垮整个服务
- 防爬虫策略:设备指纹识别、User-Agent 检测、Cookie 校验、动态 Token
- 缓存防护:布隆过滤器防穿透、热点缓存永不过期、互斥锁防击穿
- 灰度发布:新接口先切 10% 流量,观察稳定后再全量发布
🔍 第三步:监控告警与事后复盘
- 实时监控:重点关注接口 QPS、响应时间、错误率、服务器 CPU / 内存 / 磁盘
- 多级告警:设置梯度阈值(警告→严重→紧急),通过短信 / 电话 / 邮件通知
- 事后复盘:攻击结束后分析攻击来源、攻击方式、防护漏洞,输出复盘报告并优化防护策略
📝 总结
接口防刷不是单点问题,而是系统性工程。核心思路是:先止损保命,再层层设防,最后通过监控复盘持续优化。同时要根据业务场景选择合适的限流算法和防护策略,在安全性和用户体验之间找到平衡点。
💻 核心代码实现(带技术亮点标注)
1. Redis+Lua 分布式令牌桶实现(大厂首选 ✅)
技术亮点:利用 Lua 脚本保证 Redis 操作的原子性,避免并发下的计数错误,单实例性能可达 10w+ QPS,支持分布式环境。
第一步:令牌桶核心 Lua 脚本
-- 限流Key(如:rate_limit:user:123:api:/order)
local key = KEYS[1]
-- 桶容量(最大突发流量)
local capacity = tonumber(ARGV[1])
-- 令牌生成速率(个/秒)
local rate = tonumber(ARGV[2])
-- 请求的令牌数(一般为1)
local requested = tonumber(ARGV[3])
-- 当前时间戳(毫秒)
local now = tonumber(ARGV[4])
-- 桶中剩余令牌数
local remaining = tonumber(redis.call('hget', key, 'remaining') or capacity)
-- 上次令牌生成时间
local last_refill = tonumber(redis.call('hget', key, 'last_refill') or now)
-- 计算从上次到现在生成的令牌数
local delta = math.max(0, now - last_refill) * rate / 1000
-- 更新剩余令牌数(不超过桶容量)
local new_remaining = math.min(capacity, remaining + delta)
-- 更新最后刷新时间
local new_last_refill = now
-- 判断是否有足够的令牌
if new_remaining >= requested then
new_remaining = new_remaining - requested
-- 存储新状态,设置过期时间(避免冷Key占用内存)
redis.call('hmset', key, 'remaining', new_remaining, 'last_refill', new_last_refill)
redis.call('expire', key, 3600)
-- 返回1表示允许通过
return 1
else
-- 返回0表示拒绝
return 0
end第二步:Java 调用工具类
@Component
public class RedisRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
// 预加载Lua脚本,避免每次编译
private final DefaultRedisScript<Long> rateLimitScript;
public RedisRateLimiter() {
rateLimitScript = new DefaultRedisScript<>();
rateLimitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rate_limit.lua")));
rateLimitScript.setResultType(Long.class);
}
/**
* 尝试获取令牌
* @param key 限流唯一标识(接口+IP+UID)
* @param capacity 桶容量
* @param rate 令牌生成速率(个/秒)
* @return true=允许通过,false=限流
*/
public boolean tryAcquire(String key, int capacity, int rate) {
List<String> keys = Collections.singletonList(key);
Long result = redisTemplate.execute(
rateLimitScript,
keys,
String.valueOf(capacity),
String.valueOf(rate),
"1", // 每次请求1个令牌
String.valueOf(System.currentTimeMillis())
);
return result != null && result == 1;
}
}2. Sentinel 注解式限流(SpringCloud 项目首选)
技术亮点:注解式开发,侵入性极低;支持控制台动态调整规则,无需重启服务;内置熔断、降级、热点参数限流等功能。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
// 定义限流资源,指定限流异常处理器
@SentinelResource(
value = "createOrder",
blockHandler = "createOrderBlockHandler",
fallback = "createOrderFallback"
)
@PostMapping("/create")
public ResponseEntity<OrderVO> createOrder(@RequestBody OrderDTO orderDTO) {
return ResponseEntity.ok(orderService.createOrder(orderDTO));
}
// 限流触发时的处理方法
public ResponseEntity<OrderVO> createOrderBlockHandler(OrderDTO orderDTO, BlockException e) {
return ResponseEntity.status(429).body(OrderVO.error("系统繁忙,请稍后再试"));
}
// 业务异常降级方法
public ResponseEntity<OrderVO> createOrderFallback(OrderDTO orderDTO, Throwable e) {
return ResponseEntity.status(500).body(OrderVO.error("订单创建失败,请稍后再试"));
}
}3. Guava 布隆过滤器防穿透(防恶意查询不存在的 Key)
技术亮点:空间效率极高(100 万数据仅需约 1MB 内存),查询速度 O (1),可有效拦截 99% 以上的恶意穿透请求。
@Component
public class BloomFilterService {
// 初始化布隆过滤器:预期插入100万条数据,误判率0.01%
private final BloomFilter<String> orderIdFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000,
0.0001
);
// 系统启动时初始化所有有效订单ID到布隆过滤器
@PostConstruct
public void init() {
List<String> allOrderIds = orderMapper.getAllValidOrderIds();
allOrderIds.forEach(orderIdFilter::put);
}
// 判断订单ID是否可能存在
public boolean mightContain(String orderId) {
return orderIdFilter.mightContain(orderId);
}
// 新增订单时同步到布隆过滤器
public void addOrderId(String orderId) {
orderIdFilter.put(orderId);
}
}🎯 核心技术难点与解决方案(面试必问加分项 ⭐)
| 技术难点 | 问题描述 | 解决方案 | 核心技术亮点 |
|---|---|---|---|
| 分布式限流原子性问题 | 普通 Redis 命令(get+set)在并发下会出现计数错误,导致限流失效 | Redis+Lua 原子脚本 Redisson 分布式限流组件 | Lua 脚本一次性执行所有逻辑,Redis 单线程保证原子性,无并发问题 |
| 热点 Key 突刺问题 | 某个热点接口 / 商品被瞬间百万级访问,普通限流无法扛住,拖垮整个集群 | 1. 热点接口单独集群部署 2. 本地缓存 + Caffeine+Redis 多级缓存 3. 热点 Key 提前预热 4. Sentinel 热点参数限流 | 本地缓存扛住 90% 以上的热点请求,避免 Redis 和数据库被打穿 |
| 缓存穿透 / 击穿 / 雪崩 | 大量不存在的 Key 直接打到数据库;热点 Key 过期瞬间大量请求并发;大量 Key 同时过期 | 穿透:布隆过滤器 + 空值缓存 击穿:互斥锁 + 热点 Key 永不过期 雪崩:缓存过期时间加随机值 + 熔断降级 | 分层防护,从根本上解决缓存三大问题 |
| 恶意请求绕过前端防护 | 攻击者直接调用后端接口,绕过前端验证码、按钮防重复点击等措施 | 1. 网关层请求签名校验(Timestamp+Nonce+Sign) 2. 设备指纹识别 3. 动态 Token(每次请求刷新) 4. 用户行为分析(异常操作封禁) | 从请求合法性层面拦截恶意流量,而不仅仅是限流 |
| 限流粒度难以平衡 | 全局限流太粗会误伤正常用户,用户级限流太细会占用大量内存 | 多级限流架构: 网关层:全局限流(QPS 10w) 应用层:接口级限流(QPS 1w) 用户层:用户级限流(QPS 10) | 层层递进,既保证系统稳定性,又最大限度减少误伤 |
| 服务雪崩效应 | 一个下游服务挂了,导致上游所有调用它的服务都被拖垮,最终整个系统瘫痪 | 1. 服务隔离(线程池隔离 / 信号量隔离) 2. 熔断降级(Sentinel/Hystrix) 3. 超时控制(所有接口设置合理超时时间) | 故障隔离,防止故障蔓延,保证核心服务可用 |
📝 补充总结
接口防刷本质上是 "攻防对抗" 的过程,没有一劳永逸的方案。核心思路是:用最小的成本拦截最多的恶意流量,同时保证正常用户的体验。
真实面试模拟
真实面试模拟
👨💼 面试官:
来,同学,聊个实际的。假设咱们刚上线一个领券活动,现在发现接口被狂刷,疑似有人用脚本疯狂抢券。你作为后端开发,会怎么从系统层面入手去分析和防御?
🧑💻 候选人:
首先我不会一上来就加代码,会先界定一下“狂刷”到底是哪种类型。
- 是同一用户高频请求薅羊毛?
- 还是大量不同IP抓数据的爬虫?
- 或者是单纯重放同一个请求?
看日志、结合业务指标,基本就能定位。假设就是登录用户用脚本高频抢券,那我就围绕“如何让重复提交失效”和“如何识别自动化行为”去分层设防。
👨💼 面试官:
分层?具体怎么分层,说说你的防御体系图。
🧑💻 候选人:
我会搭一个纵深防御的漏斗,每一层解决不同维度的问题,哪怕上层被绕过,下层照样兜底 👇
👨💼 面试官:
图不错。那拆开说说,前端这一层你能做什么?
🧑💻 候选人:
前端其实只是第一层过滤,不信任但有用 🤞
- 防抖/节流:按钮点击后 2 秒内灰显,脚本很难绕过拖拽
- 一次性 page_token:页面加载时下发 token,提交即失效,防止表单重复提交
- 滑动验证码:高风险动作直接弹出,脚本成本一下就上去了
当然这一层防君子不防小人,核心防线还得在后端。
👨💼 面试官:
那网关层怎么扛?如果人家直接调用 API 绕过前端呢?
🧑💻 候选人:
网关做无差别限流和清洗,这是第一个硬门槛。
用 Nginx + Lua 或 Spring Cloud Gateway + Redis 实现:
- IP 令牌桶限流:单 IP 每秒最多 20 次,超限返回 429
- 动态黑名单:连续触发限流 N 次,自动封禁该 IP 1 小时
- UA/设备指纹校验:请求里缺少正常浏览器特征,直接咔嚓 🚫
伪代码大概这样:
if (redis.incr("ratelimit:ip:" + ip) > 20) {
response.setStatus(429);
return;
}
redis.expire("ratelimit:ip:" + ip, 1, TimeUnit.SECONDS);网关就能挡掉大部分无脑脚本。
👨💼 面试官:
如果攻击者换 IP 或者用肉鸡,业务层还能怎么办?
🧑💻 候选人:
这就是业务层要精准控制的地方,维度可以很丰富 👍
a) 用户/接口级分布式限流
用 Sentinel 或 Redisson RRateLimiter,比如同一个 userId + 领券接口,1 秒只能通过 1 次。超限返回“操作太频繁,请稍后再试~” ⏳,不丢用户体验。
b) 幂等性设计(防重放核心)
强制客户端传全局唯一 requestId 或业务幂等键(如 userId + couponId + activityId),后端用 Redis SETNX 做轻量锁:
String key = "idem:submit:" + userId + couponId;
boolean ok = redis.setIfAbsent(key, "1", 5, TimeUnit.SECONDS);
if (!ok) return "请勿重复提交";数据库再加唯一索引兜底,最后一道防线 💾。
c) 风控逐级加压
用户触发限流 → 弹出图形验证码;验证码多次失败 → 短时封禁帐号,同步上报风控系统;再深一层,用 Flink 实时计算设备、行为轨迹、历史风险分,高分直接拒绝,连验证码都不给。
这样就算 IP 换了,帐号和设备指纹也跑不掉。
👨💼 面试官:
思路挺完整。不过你觉得防刷就是拼命堵吗?有没有更高级的玩法?
🧑💻 候选人:
对,纯堵容易把正常用户误伤。我的理解是:防刷本质是成本对抗 💸
让刷子付出更高成本,同时几乎不影响真实用户:
- 新手首券必须绑定实名手机号 📱
- 异常行为不发券,改为“人工审核后发放” 🎫
- 高频访问不封死,改为响应加盐降级,返回缓存数据但隐藏关键信息
- 建立帐号信用体系,信用分低的享受的权益就少
这样攻击者算一笔账,发现不划算,自然放弃。
👨💼 面试官:
很好,最后用一段话总结下你的完整方案吧。
🧑💻 候选人:
我会从 前端防抖→网关IP限流→业务幂等+用户级限流→风控验证码→数据库唯一索引 五层漏斗来设计。
前端减少无意重复;网关挡掉大部分恶意流量;业务层用 Redis 做分布式限流和请求幂等,防止同一用户狂刷;风控系统自动对异常升级验证码或短时封禁;数据库唯一索引最后兜底。核心是分层过滤、逐级加压,同时用成本对抗思维去平衡安全和体验 😊。
👨💼 面试官:
不错,体系已经有了。那能不能贴出几段你认为最有技术亮点的核心代码?不用全写,挑最体现你功底的来。
🧑💻 候选人:
没问题,我挑三段:IP黑名单动态管理、业务层分布式限流、幂等注解+AOP,这三块最能体现高并发下的原子性和扩展性。
1️⃣ 网关层:IP黑名单 + 动态令牌桶(Redis + Lua)
直接用 Lua 脚本保证「判断-计数-封禁」的原子性,避免竞态窗口。
-- ratelimit.lua:IP限流+自动拉黑
local ip = KEYS[1]
local limit = tonumber(ARGV[1]) -- 每秒限制次数
local ban_seconds = tonumber(ARGV[2]) -- 封禁时长
local blacklist_key = "blacklist:" .. ip
local rate_key = "rate:" .. ip
-- 1. 检查是否已在黑名单
if redis.call("exists", blacklist_key) == 1 then
return -1 -- 已被封禁
end
-- 2. 滑动窗口计数(简化令牌桶)
local current = redis.call("incr", rate_key)
if current == 1 then
redis.call("expire", rate_key, 1) -- 1秒窗口
end
-- 3. 超限则加入黑名单
if current > limit then
redis.call("set", blacklist_key, 1)
redis.call("expire", blacklist_key, ban_seconds)
return -1
end
return 1 -- 放行亮点 🎯:
- 整个逻辑在 Redis 服务端原子执行,无并发漏洞。
- 令牌桶用
incr+expire简单实现滑动窗口,性能极高。 - 超限自动拉黑,不需要额外后端定时任务。
2️⃣ 业务层:用户+接口维度的分布式限流(Redisson RRateLimiter)
Redisson 内置了令牌桶实现,支持集群,省去自己造轮子。
@Service
public class RateLimitService {
@Autowired
private RedissonClient redissonClient;
/**
* 尝试获取令牌
* @param userId 用户ID
* @param apiKey 接口标识
* @param permits 每次消耗令牌数
* @return 是否允许
*/
public boolean tryAcquire(String userId, String apiKey, long permits) {
String key = "limit:" + apiKey + ":" + userId;
RRateLimiter limiter = redissonClient.getRateLimiter(key);
// 初始化:每秒生成1个令牌,最大桶容量5(允许小突发)
limiter.trySetRate(RateType.OVERALL, 1, 1, RateIntervalUnit.SECONDS);
return limiter.tryAcquire(permits);
}
}亮点 🎯:
- 利用 Redisson 分布式对象,天然支持多服务实例。
trySetRate只首次设置,后续复用,避免重复初始化开销。- 桶容量可调,适应突发流量,减少误杀。
3️⃣ 幂等拦截:自定义注解 + AOP(防重放利器)
通过声明式注解,零侵入给任何接口加上幂等保护。
// 注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/** 幂等键前缀 */
String prefix();
/** 键过期秒数 */
int expire() default 5;
/** SpEL表达式,从参数中提取业务唯一标识 */
String keySpEL();
}// AOP 切面实现
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Around("@annotation(idempotent)")
public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
// 1. 解析 SpEL 获取业务幂等键
String key = parseSpEL(idempotent.keySpEL(), joinPoint);
String redisKey = "idem:" + idempotent.prefix() + ":" + key;
// 2. SETNX 原子占位
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(redisKey, "1", Duration.ofSeconds(idempotent.expire()));
if (Boolean.FALSE.equals(success)) {
return ApiResult.fail("请勿重复提交");
}
// 3. 执行原业务
try {
return joinPoint.proceed();
} catch (Exception e) {
// 业务异常可主动删除幂等键,允许重试
redisTemplate.delete(redisKey);
throw e;
}
}
}// 业务使用,简单到飞起
@Idempotent(prefix = "claimCoupon", keySpEL = "#req.userId + ':' + #req.couponId")
public Result claimCoupon(ClaimReq req) {
// 核心领券逻辑
}亮点 🎯:
- 声明式编程,业务代码零污染。
- SpEL 灵活组装幂等键,适用任意参数结构。
- 异常时主动删除键,支持安全重试,兼顾防重和可用性。
👨💼 面试官:
代码写得挺工整。那再聊聊,这套防刷体系落地时,你觉得最容易踩坑的技术难点有哪些?你是怎么解决的?
🧑💻 候选人:
我梳理了四个核心难点,边做边优化出来的 👇
| 🚧 技术难点 | 💡 解决方案 | 🔍 关键手段 |
|---|---|---|
| 1. 分布式限流的一致性与性能矛盾 | 用 Redis 做集中式计数,Lua 脚本保证原子性;单节点瓶颈则采用本地预限流+远程最终校验的两级模式 | Lua 原子化、两级限流 |
| 2. 幂等键的粒度定义混乱 | 和产品统一“业务唯一性”口径,例如领券幂等键 = userId + couponId + 活动Id;技术上用 SpEL 自定义键,数据库加唯一索引双保险 | 业务语义明确、DB唯一索引兜底 |
| 3. 黑名单误杀正常用户 | 黑名单加入时间窗口内“疑似阈值”而非一超就封;配合验证码柔性挑战,合法用户可通过答题解封;引入设备指纹而不是纯IP,降低NAT下的误伤 | 软封禁+验证码、设备指纹 |
| 4. 防刷规则硬编码,变更需重启 | 引入配置中心动态下发阈值、黑名单时长、风控开关;甚至用轻量规则引擎(如QLExpress)让运营可配置 | 配置中心、规则引擎热更新 |
👨💼 面试官:
展开说说第一个难点“分布式限流一致性与性能”,这很关键。
🧑💻 候选人:
对,这是个典型的 CAP 权衡问题。
- 如果每次请求都
incr查 Redis,限流精准,但大流量下 Redis 可能成为瓶颈。 - 所以我采用了两级限流架构:
- L1 本地:用 Guava RateLimiter 在服务实例内先粗粒度限流,拒绝掉99%的无效请求;
- L2 远程:只有通过 L1 的请求才去 Redis 做精准计数,这样 Redis 压力骤降。
伪代码:
if (!localLimiter.tryAcquire()) {
return fastFail("系统繁忙"); // 本地直接扔
}
if (!redisLimiter.tryAcquire(userId, apiKey)) {
return fastFail("操作频繁");
}
// 正常业务处理同时如果 Redis 挂了,本地限流还能降级兜底,牺牲一点精度换取可用性。
👨💼 面试官:
把性能和可靠性都考虑进去了,不错。这个场景还有别的难点吗?
🧑💻 候选人:
还有一个容易被忽略的:重放攻击的 RequestId 生成不可信。
如果 RequestId 由客户端生成,攻击者完全可以每次都传新 ID 绕过幂等。所以实践中我会 结合服务端签名:
- 客户端先请求一个
antiReplayToken(服务端签发,含时间戳+签名); - 提交时带上此 token,后端校验签名和有效期,一次性消费。
这样即使脚本伪造 RequestId,拿不到合法 Token 也没法刷。
这个点经常在面试里被忽略,但线上特别重要 🔐。
👨💼 面试官:
非常有深度。看来你对防刷体系的“道”和“术”都把握住了,期待你能来我们团队落地这些方案。今天的面试就到这里,辛苦啦 🎉
🧑💻 候选人:
谢谢,我也很期待!如果后面有具体场景,可以再深入探讨 😊🤝
