接口被狂刷怎么处理

接口被狂刷怎么处理

"紧急止损→多层防御→监控复盘" 👇

🚨 第一步:紧急止损(先保命!黄金 3 分钟)

  1. 快速扩容:立刻将服务实例数拉满,利用云厂商弹性伸缩能力扛住峰值
  2. 熔断降级:非核心接口直接返回默认值 / 降级页,核心接口开启熔断保护
  3. 流量清洗:接入 CDN/WAF,过滤掉明显恶意请求(相同 IP / 设备号高频访问)
  4. 临时封禁:对异常 IP、UID、设备号执行 15 分钟 - 24 小时临时封禁

🛡️ 第二步:构建多层防御体系(长治久安)

这是核心!单一防护必被突破,必须做到层层设防、纵深防御

⚡ 核心技术:四大限流算法对比

这是面试必问加分项,必须说清各自适用场景

算法名称 核心思想 优点 缺点 适用场景
固定窗口计数器 统计单位时间内请求数,超过阈值则拒绝 实现简单、性能好 存在临界问题(窗口切换时可能出现 2 倍流量) 要求不高的简单限流
滑动窗口计数器 将时间窗口拆分为多个小格子,滑动统计 解决了临界问题 实现稍复杂,需要存储多个格子的计数 大多数业务场景
漏桶算法 请求像水一样进入漏桶,以固定速率流出 强制匀速处理,平滑突发流量 无法应对突发流量,桶满直接丢弃 消息队列削峰填谷
令牌桶算法 以固定速率生成令牌,请求需要拿到令牌才能执行 既能限流又能应对突发流量 实现相对复杂 互联网接口限流(首选)

💡 大厂首选:Redis+Lua 实现分布式令牌桶算法,或者直接使用 Sentinel/Resilience4j 框架

✨ 进阶优化技巧(拉开差距的加分项)

  1. 分布式限流:网关层做全局限流,应用层做接口级限流,避免单点限流失效
  2. 热点隔离:将热点接口单独部署在独立集群,避免拖垮整个服务
  3. 防爬虫策略:设备指纹识别、User-Agent 检测、Cookie 校验、动态 Token
  4. 缓存防护:布隆过滤器防穿透、热点缓存永不过期、互斥锁防击穿
  5. 灰度发布:新接口先切 10% 流量,观察稳定后再全量发布

🔍 第三步:监控告警与事后复盘

  1. 实时监控:重点关注接口 QPS、响应时间、错误率、服务器 CPU / 内存 / 磁盘
  2. 多级告警:设置梯度阈值(警告→严重→紧急),通过短信 / 电话 / 邮件通知
  3. 事后复盘:攻击结束后分析攻击来源、攻击方式、防护漏洞,输出复盘报告并优化防护策略

📝 总结

接口防刷不是单点问题,而是系统性工程 。核心思路是:先止损保命,再层层设防,最后通过监控复盘持续优化。同时要根据业务场景选择合适的限流算法和防护策略,在安全性和用户体验之间找到平衡点。

💻 核心代码实现(带技术亮点标注)

1. Redis+Lua 分布式令牌桶实现(大厂首选 ✅)

技术亮点:利用 Lua 脚本保证 Redis 操作的原子性,避免并发下的计数错误,单实例性能可达 10w+ QPS,支持分布式环境。

第一步:令牌桶核心 Lua 脚本

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 调用工具类

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 项目首选)

技术亮点:注解式开发,侵入性极低;支持控制台动态调整规则,无需重启服务;内置熔断、降级、热点参数限流等功能。

java 复制代码
@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% 以上的恶意穿透请求。

java 复制代码
@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 + LuaSpring Cloud Gateway + Redis 实现:

  • IP 令牌桶限流:单 IP 每秒最多 20 次,超限返回 429
  • 动态黑名单:连续触发限流 N 次,自动封禁该 IP 1 小时
  • UA/设备指纹校验:请求里缺少正常浏览器特征,直接咔嚓 🚫

伪代码大概这样:

java 复制代码
if (redis.incr("ratelimit:ip:" + ip) > 20) {
    response.setStatus(429);
    return;
}
redis.expire("ratelimit:ip:" + ip, 1, TimeUnit.SECONDS);

网关就能挡掉大部分无脑脚本。

👨‍💼 面试官:

如果攻击者换 IP 或者用肉鸡,业务层还能怎么办?

🧑‍💻 候选人:

这就是业务层要精准控制的地方,维度可以很丰富 👍

a) 用户/接口级分布式限流

SentinelRedisson RRateLimiter,比如同一个 userId + 领券接口,1 秒只能通过 1 次。超限返回"操作太频繁,请稍后再试~" ⏳,不丢用户体验。

b) 幂等性设计(防重放核心)

强制客户端传全局唯一 requestId 或业务幂等键(如 userId + couponId + activityId),后端用 Redis SETNX 做轻量锁:

java 复制代码
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 脚本保证「判断-计数-封禁」的原子性,避免竞态窗口。

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 内置了令牌桶实现,支持集群,省去自己造轮子。

java 复制代码
@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(防重放利器)

通过声明式注解,零侵入给任何接口加上幂等保护。

java 复制代码
// 注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    /** 幂等键前缀 */
    String prefix();
    /** 键过期秒数 */
    int expire() default 5;
    /** SpEL表达式,从参数中提取业务唯一标识 */
    String keySpEL();
}
java 复制代码
// 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;
        }
    }
}
java 复制代码
// 业务使用,简单到飞起
@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 压力骤降。

伪代码:

java 复制代码
if (!localLimiter.tryAcquire()) {
    return fastFail("系统繁忙"); // 本地直接扔
}
if (!redisLimiter.tryAcquire(userId, apiKey)) {
    return fastFail("操作频繁");
}
// 正常业务处理

同时如果 Redis 挂了,本地限流还能降级兜底,牺牲一点精度换取可用性

👨‍💼 面试官:

把性能和可靠性都考虑进去了,不错。这个场景还有别的难点吗?

🧑‍💻 候选人:

还有一个容易被忽略的:重放攻击的 RequestId 生成不可信

如果 RequestId 由客户端生成,攻击者完全可以每次都传新 ID 绕过幂等。所以实践中我会 结合服务端签名

  • 客户端先请求一个 antiReplayToken(服务端签发,含时间戳+签名);
  • 提交时带上此 token,后端校验签名和有效期,一次性消费。

这样即使脚本伪造 RequestId,拿不到合法 Token 也没法刷。

这个点经常在面试里被忽略,但线上特别重要 🔐。

👨‍💼 面试官:

非常有深度。看来你对防刷体系的"道"和"术"都把握住了,期待你能来我们团队落地这些方案。今天的面试就到这里,辛苦啦 🎉

🧑‍💻 候选人:

谢谢,我也很期待!如果后面有具体场景,可以再深入探讨 😊🤝


公众号"Rain的Java大神之路"

个人博客"www.javadashen.com"

相关推荐
Rain的Java大神实战圈1 天前
线上在Docker中跑MySQL会怎样
场景设计题
Rain的Java大神实战圈2 天前
Docker搭建Redis集群完全指南
场景设计题
Rain的Java大神实战圈5 天前
短信接口被狂刷怎么处理
场景设计题
Rain的Java大神实战圈6 天前
如何避免订单重复提交
场景设计题
Rain的Java大神实战圈7 天前
MySQL误删数据如何恢复
场景设计题
Rain的Java大神实战圈8 天前
介绍一下分布式事务
场景设计题
Rain的Java大神实战圈9 天前
频繁FullGC如何优化
场景设计题
Rain的Java大神实战圈13 天前
MQ重复消费问题怎么解决
场景设计题
Rain的Java大神实战圈14 天前
线上服务器反应慢如何排查
场景设计题