Guava——并发(二)

并发

4、RateLimiter

Guava 中的 RateLimiter(位于 com.google.common.util.concurrent 包)是专门用于限制流量、实现限流防护的核心工具类。

它基于 令牌桶算法(Token Bucket Algorithm) 实现,广泛应用于 API 接口限流、高并发系统保护、第三方服务调用频率限制(如限制每秒最多发送 N N N 次请求)以及后台批处理任务的平滑降速。

4.1、底层原理

令牌桶算法 (Token Bucket)

核心工作机制:

  • 令牌以固定速率生成:系统以设定的速率(如每秒产生 N N N 个令牌)平滑往桶里放入令牌。
  • 桶有容量上限:如果桶满了,新生成的令牌就会被丢弃。
  • 请求消耗令牌:每个请求在执行前需要先从桶中获取相应数量的令牌(通常为 1 个)。如果桶中有足够令牌:请求立即通过,桶中令牌减少。如果令牌不足:请求可以选择阻塞等待直到分配到令牌,或者立即放弃/降级(非阻塞模式)。

4.2、核心API

分类 核心 API 方法 说明
创建工厂 create(double permitsPerSecond) 创建平滑突发限流器(SmoothBursty),按固定速率生成令牌
创建工厂 create(double permitsPerSecond, Duration warmupPeriod) 创建预热限流器(SmoothWarmingUp),在预热期内渐进增加速率
阻塞式获取 acquire() 获取 1 个令牌,若没有则阻塞等待,返回实际等待的秒数
阻塞式获取 acquire(int permits) 获取指定数量的令牌,若没有则阻塞等待
非阻塞式尝试 tryAcquire() 尝试获取 1 个令牌,成功返回 true,失败立即返回 false
非阻塞式尝试 tryAcquire(Duration timeout) / tryAcquire(int permits, long timeout, TimeUnit unit) 在指定的超时时间内尝试获取令牌,超时仍未拿到返回 false
动态修改 setRate(double permitsPerSecond) 动态调整限流速率(如根据系统 load 动态降级)
动态修改 getRate() 获取当前的限制速率

4.3、使用示例

1. 基础阻塞模式(acquire)

适合任务处理不能丢失、允许请求排队等待的场景(如数据库批量插入、日志平滑上报)。

java 复制代码
import com.google.common.util.concurrent.RateLimiter;

public class RateLimiterAcquireDemo {
    public static void main(String[] args) {
        // 创建限制为每秒 2 个令牌的 RateLimiter (即每 500ms 产生 1 个令牌)
        RateLimiter limiter = RateLimiter.create(2.0);

        long start = System.currentTimeMillis();

        for (int i = 1; i <= 5; i++) {
            // acquire() 会阻塞直至拿到令牌,返回等待的时间(秒)
            double waitTime = limiter.acquire();
            long elapsed = System.currentTimeMillis() - start;

            System.out.printf("请求 %d 成功获取令牌 | 等待耗时: %.2f秒 | 总耗时: %dms%n", 
                    i, waitTime, elapsed);
        }
    }
}
java 复制代码
请求 1 成功获取令牌 | 等待耗时: 0.00秒 | 总耗时: 2ms
请求 2 成功获取令牌 | 等待耗时: 0.48秒 | 总耗时: 502ms
请求 3 成功获取令牌 | 等待耗时: 0.50秒 | 总耗时: 1002ms
请求 4 成功获取令牌 | 等待耗时: 0.50秒 | 总耗时: 1500ms
请求 5 成功获取令牌 | 等待耗时: 0.50秒 | 总耗时: 2001ms

2. 非阻塞/超时快速失败模式(tryAcquire)

适合高并发 Web 接口防刷、服务自我保护(超出处理能力时直接截流抛异常或提示"系统繁忙,请稍后再试")。

java 复制代码
import com.google.common.util.concurrent.RateLimiter;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class RateLimiterTryAcquireDemo {
    public static void main(String[] args) throws InterruptedException {
        // 每秒允许 5 个请求 (QPS = 5)
        RateLimiter limiter = RateLimiter.create(5.0);
        ExecutorService executor = Executors.newFixedThreadPool(10);

        for (int i = 1; i <= 10; i++) {
            final int requestId = i;
            executor.submit(() -> {
                // 1. 立即尝试获取,拿不到直接失败
                // boolean success = limiter.tryAcquire();

                // 2. 带有超时的尝试:允许最多等待 200ms
                boolean success = limiter.tryAcquire(1, Duration.ofMillis(200));

                if (success) {
                    System.out.println("【成功】请求 " + requestId + " 拿到令牌,开始处理业务");
                } else {
                    System.err.println("【限流降级】请求 " + requestId + " 触发限流,拒绝服务");
                }
            });
        }

        executor.shutdown();
    }
}
java 复制代码
【限流降级】请求 7 触发限流,拒绝服务
【限流降级】请求 1 触发限流,拒绝服务
【限流降级】请求 8 触发限流,拒绝服务
【限流降级】请求 4 触发限流,拒绝服务
【限流降级】请求 3 触发限流,拒绝服务
【限流降级】请求 2 触发限流,拒绝服务
【限流降级】请求 6 触发限流,拒绝服务
【限流降级】请求 9 触发限流,拒绝服务
【成功】请求 5 拿到令牌,开始处理业务
【成功】请求 10 拿到令牌,开始处理业务

3. 预热限流模式(SmoothWarmingUp)

在冷启动或系统刚发布上线时,直接打入峰值流量可能导致数据库缓存未建好而崩溃。冷启动预热机制会在设定时间内让 QPS 从低速率平滑提升至最高速率。

java 复制代码
import com.google.common.util.concurrent.RateLimiter;
import java.time.Duration;

public class RateLimiterWarmupDemo {
    public static void main(String[] args) {
        // QPS 限制为 5,同时设置 3 秒的预热期 (Warmup Period)
        RateLimiter limiter = RateLimiter.create(5.0, Duration.ofSeconds(3));

        System.out.println("--- 阶段 1: 预热期间(获取令牌耗时逐步降低,速率逐渐提升) ---");
        for (int i = 1; i <= 10; i++) {
            double waitTime = limiter.acquire();
            System.out.printf("请求 %d 等待时间: %.3f 秒%n", i, waitTime);
        }

        System.out.println("--- 阶段 2: 预热完成后(维持正常的 0.2 秒固定间隔) ---");
        for (int i = 11; i <= 15; i++) {
            double waitTime = limiter.acquire();
            System.out.printf("请求 %d 等待时间: %.3f 秒%n", i, waitTime);
        }
    }
}
java 复制代码
--- 阶段 1: 预热期间(获取令牌耗时逐步降低,速率逐渐提升) ---
请求 1 等待时间: 0.000 秒
请求 2 等待时间: 0.550 秒
请求 3 等待时间: 0.513 秒
请求 4 等待时间: 0.465 秒
请求 5 等待时间: 0.413 秒
请求 6 等待时间: 0.355 秒
请求 7 等待时间: 0.303 秒
请求 8 等待时间: 0.249 秒
请求 9 等待时间: 0.201 秒
请求 10 等待时间: 0.199 秒
--- 阶段 2: 预热完成后(维持正常的 0.2 秒固定间隔) ---
请求 11 等待时间: 0.198 秒
请求 12 等待时间: 0.196 秒
请求 13 等待时间: 0.195 秒
请求 14 等待时间: 0.195 秒
请求 15 等待时间: 0.198 秒

4.3、注意事项

关键特性:预消费透支机制 (Past-Debt)

RateLimiter 内部有一个非常特殊的机制:允许当次请求透支未来的令牌。

如果你一次性调用 limiter.acquire(10) 请求 10 个令牌,即使当前桶里只有 1 个令牌,本次请求也会立即通过,但是需要由"下一次请求"来承担上一次透支的等待时间。

java 复制代码
import com.google.common.util.concurrent.RateLimiter;

public class RateLimiterDebtDemo {
    public static void main(String[] args) {
        RateLimiter limiter = RateLimiter.create(1.0); // 1秒 1 个令牌

        System.out.println("请求 A 一次性索取 5 个令牌...");
        double waitA = limiter.acquire(5); // 桶里不够,但请求 A 马上通过,等待 0 秒
        System.out.printf("请求 A 成功,等待耗时: %.2f秒%n", waitA);

        System.out.println("请求 B 索取 1 个令牌...");
        double waitB = limiter.acquire(1); // 请求 B 需要为请求 A 的"透支行为"买单,阻塞等待 5 秒!
        System.out.printf("请求 B 成功,等待耗时: %.2f秒%n", waitB);
    }
}
java 复制代码
请求 A 一次性索取 5 个令牌...
请求 A 成功,等待耗时: 0.00秒
请求 B 索取 1 个令牌...
请求 B 成功,等待耗时: 4.98秒

生产实践与局限性说明

  • 单机限流 vs 分布式限流:
    • Guava RateLimiter 是纯单机内存级限流工具,适用于单个 JVM 应用进程内的自我保护。
    • 如果是微服务集群协同限流(如全局限制某用户每分钟只允许请求 100 次),请选用基于 Redis + Lua 脚本 或 Sentinel 等分布式限流方案。
  • 多线程并发安全性:
    • RateLimiter 是线程安全的,底层使用了互斥锁(synchronized)维护全局的预分配时间(nextFreeTicketMicros),在一般吞吐量下性能极高。
  • 动态调速:
    • 在运行时调用 limiter.setRate(newRate) 可以无缝调整限流值,下一次获取令牌时会自动按新的速率重新计算,非常适合与分布式配置中心(如 Nacos、Apollo)集成。

5、Monitor

Guava 中的 Monitor(位于 com.google.common.util.concurrent 包)用于替代传统的 synchronized 关键字以及 ReentrantLock + Condition 组合。

在传统并发编程中,处理"条件等待"需要编写复杂的 while (!condition) { wait(); } 循环,极易发生虚假唤醒(Spurious Wakeup)、死锁或忘记 signalAll() 等问题。Monitor 将互斥锁与布尔条件(Guard)深度结合,显著简化了多线程环境下的同步控制。

5.1、核心概念

Monitor 的核心设计在于将条件判断抽象为 Monitor.Guard 抽象类。

通过定义 Guard 条件,线程可以在进入临界区时指定条件:"只有当条件满足时才获取锁并进入,不满足则自动阻塞等待"。

java 复制代码
Monitor monitor = new Monitor();

// 定义 Condition 评估条件
Monitor.Guard listNotFull = new Monitor.Guard(monitor) {
    @Override
    public boolean isSatisfied() {
        return list.size() < MAX_CAPACITY;
    }
};

5.3、核心API

分类 核心 API 方法 说明
基础加解锁 enter(), leave() 相当于 ReentrantLock.lock() / unlock()
条件阻塞进入 enterWhen(Guard guard) 阻塞直到获取锁且 Guard 条件为 true
条件阻塞进入 enterWhenUninterruptibly(Guard guard) 不可中断地阻塞直到获取锁且 Guard 条件为 true
带有超时的条件进入 enterWhen(Guard guard, Duration timeout) 在超时时间内等待条件满足并进入
尝试非阻塞进入 enterIf(Guard guard) 如果 Guardtrue 则立即获取锁进入并返回 true,否则不获取锁返回 false
条件等待(已持锁) waitFor(Guard guard) 在已持有 Monitor 锁的情况下,释放锁并等待 Guardtrue
状态查询 hasQueuedThreads(), isOccupiedByCurrentThread() 查询当前锁状态与排队线程

5.4、使用示例

基于 Monitor 实现有界阻塞队列

传统的 Producer-Consumer 模式如果用 ReentrantLock 实现,需要手动维护 notEmpty 和 notFull 两个 Condition。使用 Monitor 后,代码会变得极其直观。

java 复制代码
import com.google.common.util.concurrent.Monitor;
import java.util.LinkedList;
import java.util.Queue;

public class BoundedQueue<E> {
    private final Queue<E> queue = new LinkedList<>();
    private final int capacity;
    private final Monitor monitor = new Monitor();

    // 1. 定义 Guard 条件:队列未满
    private final Monitor.Guard NOT_FULL = new Monitor.Guard(monitor) {
        @Override
        public boolean isSatisfied() {
            return queue.size() < capacity;
        }
    };

    // 2. 定义 Guard 条件:队列非空
    private final Monitor.Guard NOT_EMPTY = new Monitor.Guard(monitor) {
        @Override
        public boolean isSatisfied() {
            return !queue.isEmpty();
        }
    };

    public BoundedQueue(int capacity) {
        this.capacity = capacity;
    }

    // 生产者入队方法
    public void put(E item) throws InterruptedException {
        // enterWhen 会自动加锁,并在条件不满足时自动挂起等待
        monitor.enterWhen(NOT_FULL);
        try {
            queue.add(item);
            System.out.println("生产元素: " + item + " | 队列大小: " + queue.size());
        } finally {
            // 离开临界区时自动唤醒等待其他 Guard 的线程
            monitor.leave();
        }
    }

    // 消费者出队方法
    public E take() throws InterruptedException {
        monitor.enterWhen(NOT_EMPTY);
        try {
            E item = queue.poll();
            System.out.println("消费元素: " + item + " | 队列大小: " + queue.size());
            return item;
        } finally {
            monitor.leave();
        }
    }
}

5.5、注意事项

维度 synchronized / wait-notify ReentrantLock + Condition Guava Monitor
条件控制 单一等待队列,易错发 notify 需手动创建并维护多个 Condition 声明式定义 Guard,按需使用
条件轮询与唤醒 需手动编写 while(condition) 需手动编写 while(condition) 框架全自动处理,无需显式 signal
可读性与安全性 代码冗长,容易发生虚假唤醒 中等,需要显式 await/signal 极高,语义明确且内置安全校验
  • 自动唤醒机制(Implicit Signaling):
    在标准 Condition 模式下,程序员必须显式调用 signal() 或 signalAll()。如果漏写会导致线程死锁。而在 Monitor 中,线程执行 monitor.leave() 时,Monitor 会自动检查是否有其他挂起的 Guard 已满足条件并唤醒对应线程。
  • 防虚假唤醒(Spurious Wakeup Prevention):
    由于 Guard 的状态由框架托管,用户代码无需额外包裹 while(!isSatisfied()) 循环,enterWhen 保证在返回时条件必定成立。

使用建议与最佳实践

  • finally 确保释放锁:
    与 ReentrantLock 相同,任何包含 monitor.enterWhen(...) 的代码块,都必须通过 try-finally 保证 monitor.leave() 被执行。
  • Guard.isSatisfied() 必须无侧边效应:
    isSatisfied() 方法会被 Monitor 内部多次调用以评估条件,因此绝对不能在 isSatisfied() 中修改变量状态或执行耗时 IO,仅允许做布尔逻辑判断。

6、Service / ServiceManager

Guava 中的 Service 与 ServiceManager(位于 com.google.common.util.concurrent 包)用于管理长生命周期后台服务的状态切换与协同监控。

在复杂的 Java 应用(如 RPC 服务端、消息消费节点、后台任务调度器)中,组件通常包含"启动、运行、停止、异常恢复"等生命周期。Service 接口统一了服务的状态机模型,而 ServiceManager 能够将多个服务组合在一起,实现一键批量启动、统一优雅停机、状态监控与故障广播。

6.1、Service 状态机与生命周期

Guava Service 定义了一套严格的状态转换模型:

java 复制代码
	  [NEW]
        │
        │ startAsync()
        ▼
   [STARTING] ──── (发生异常) ────┐
        │                        │
        ▼                        │
   [RUNNING]  ──── (发生异常) ────┼───► [FAILED]
        │                        │
        │ stopAsync()            │
        ▼                        │
   [STOPPING] ──── (发生异常) ────┘
        │
        ▼
   [TERMINATED]
  • NEW:服务刚创建,尚未启动。
  • STARTING:正在初始化或拉起资源。
  • RUNNING:正常运行中(核心业务逻辑正在执行)。
  • STOPPING:正在清理资源、关闭连接、等待队列任务处理完成。
  • TERMINATED:服务已成功优雅停止。
  • FAILED:在启动、运行或停止阶段发生未捕获异常,导致服务非正常终止。

6.2、核心 API

1. Service 核心 API

方法签名 返回类型 说明
startAsync() Service 异步启动服务,状态转换为 STARTING(链式调用)
stopAsync() Service 异步停止服务,状态转换为 STOPPING(链式调用)
awaitRunning() void 阻塞当前线程,直到服务进入 RUNNING 状态或启动失败
awaitTerminated() void 阻塞当前线程,直到服务进入 TERMINATEDFAILED 状态
state() Service.State 获取服务当前的实时状态
addListener(Listener, Executor) void 为服务绑定状态监听器(监听 starting, running, failed 等事件)

Guava 提供了 3 个非常方便的抽象基类供开发者继承实现:

  • AbstractIdleService:适用于无循环任务的服务(如 RPC 客户端连接池、数据库连接池,启动即运行,停止即释放)。
  • AbstractScheduledService:适用于周期性/定时后台任务的服务。
  • AbstractExecutionThreadService:适用于需要独占一个单线程循环执行的服务(如消息队列消费者)。

2. ServiceManager 核心 API

方法签名 返回类型 说明
ServiceManager(Iterable<Service>) 构造函数 创建管理一组 Service 的服务管理器
startAsync() ServiceManager 异步并行启动所有受管服务
stopAsync() ServiceManager 异步并行停止所有受管服务
awaitHealthy() void 阻塞直到所有服务均变为 RUNNING 状态
awaitStopped() void 阻塞直到所有服务均变为终止状态(TERMINATEDFAILED
isHealthy() boolean 查询是否所有服务都处于 RUNNING 状态
servicesByState() ImmutableMultimap 按状态分组查询所有的服务实例
startupTimes() ImmutableMap 获取每个服务的启动耗时统计
addListener(Listener, Executor) void 绑定全局管理器监听器(如处理有任何服务进入 FAILED 状态)

6.3、使用示例

1. 基于 AbstractIdleService 实现数据库连接池服务

java 复制代码
import com.google.common.util.concurrent.AbstractIdleService;

public class DatabasePoolService extends AbstractIdleService {
    
    @Override
    protected void startUp() throws Exception {
        System.out.println("【数据库服务】正在初始化数据库连接池...");
        Thread.sleep(500); // 模拟连接池建连耗时
        System.out.println("【数据库服务】连接池初始化完成!");
    }

    @Override
    protected void shutDown() throws Exception {
        System.out.println("【数据库服务】正在归还连接并关闭数据库连接池...");
        System.out.println("【数据库服务】连接池已安全关闭!");
    }
}

2. 基于 AbstractScheduledService 实现后台定时清理服务

java 复制代码
import com.google.common.util.concurrent.AbstractScheduledService;
import java.util.concurrent.TimeUnit;

public class CacheCleanupService extends AbstractScheduledService {

    @Override
    protected void runOneIteration() throws Exception {
        System.out.println("【缓存清理服务】触发定时任务:清理过期本地缓存中...");
    }

    @Override
    protected Scheduler scheduler() {
        // 设置每隔 2 秒循环执行一次
        return Scheduler.newFixedRateSchedule(0, 2, TimeUnit.SECONDS);
    }

    @Override
    protected void startUp() throws Exception {
        System.out.println("【缓存清理服务】定时清理引擎启动...");
    }

    @Override
    protected void shutDown() throws Exception {
        System.out.println("【缓存清理服务】定时清理引擎已停止。");
    }
}

3. 基于 ServiceManager 协同管理与响应 JVM 优雅停机

通过 ServiceManager 将上述多个服务整合统一管控,并结合 JVM ShutdownHook 实现系统的优雅停机:

java 复制代码
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.ServiceManager;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;

public class ApplicationServer {
    public static void main(String[] args) {
        // 1. 初始化独立服务
        Service dbService = new DatabasePoolService();
        Service cacheService = new CacheCleanupService();

        List<Service> services = Arrays.asList(dbService, cacheService);

        // 2. 统一交由 ServiceManager 管理
        ServiceManager manager = new ServiceManager(services);

        // 3. 为 ServiceManager 绑定全局监听器
        manager.addListener(new ServiceManager.Listener() {
            @Override
            public void healthy() {
                System.out.println("\n>>> [系统通知] 所有后台服务已成功启动,系统进入 HEALTHY 状态!<<<\n");
            }

            @Override
            public void stopped() {
                System.out.println("\n>>> [系统通知] 所有后台服务已全部安全停止!<<<\n");
            }

            @Override
            public void failure(Service service) {
                System.err.println("\n>>> [严重告警] 服务 [" + service.getClass().getSimpleName() 
                        + "] 启动/运行失败,异常原因: " + service.failureCause() + " <<<\n");
            }
        }, MoreExecutors.directExecutor());

        // 4. 注册 JVM ShutdownHook 实现优雅关闭
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("\n收到 JVM 关闭信号,开始执行优雅停机...");
            try {
                // 最多等待 5 秒平滑停止所有受管服务
                manager.stopAsync().awaitStopped(5, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
                System.err.println("部分服务停止超时,强制退出");
            }
        }));

        // 5. 一键异步启动所有服务并等待就绪
        System.out.println("正在一键启动所有应用服务...");
        manager.startAsync();
        manager.awaitHealthy(); // 阻塞直到所有服务变为 RUNNING

        // 打印服务的启动耗时统计
        System.out.println("服务启动耗时汇总: " + manager.startupTimes());

        // 模拟应用持续运行 3 秒
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 模拟手动触发停止
        System.out.println("收到关闭指令,发起系统停止...");
        manager.stopAsync();
        manager.awaitStopped();
    }
}

6.3、注意事项

为什么选择 Service / ServiceManager?

  • 统一生命周期抽象,避免重复造轮子:
    在没有 Service 前,很多组件通过自定义 init() 和 destroy() 方法管理生命周期,逻辑分散且状态控制容易混乱。Guava Service 将状态变化(STARTING -> RUNNING -> STOPPING -> TERMINATED)进行了标准化约束。
  • 告别"死锁式"启动与依赖协同:
    ServiceManager 能够并发并行拉起多个独立服务,显著加快应用启动速度。通过 awaitHealthy() 可以保证在所有底座服务就绪前,不暴露网络流量(如不向注册中心注册暴露接口),避免了冷启动期间流量打入导致崩溃。
  • 强大的故障隔离与状态监控:
    如果有任何一个核心后台服务在运行期间发生崩溃(转换为 FAILED 状态),ServiceManager.Listener.failure(...) 会第一时间感知并广播,方便监控告警或触发全盘保护策略。

最佳实践与建议

  • 选择合适的基类:
    • AbstractIdleService:用于处理连接建立/断开、静态资源装载/释放。
    • AbstractScheduledService:用于替代 ScheduledExecutorService 执行定时轮询任务。
    • AbstractExecutionThreadService:用于单线程死循环逻辑(如 while (isRunning()) { consume(); })。
  • 在 shutDown() 中释放阻塞与响应中断:
    如果服务中有阻塞的线程(如在等待 BlockingQueue.take() 或 Socket 读写),必须在 triggerShutdown() 或 shutDown() 中主动调用中断(thread.interrupt())或关闭底层连接,否则服务无法平滑进入 STOPPING / TERMINATED 状态。
相关推荐
西索斯coding1 小时前
doubao-seed-2.1-turbo 调用一直 401 怎么办?pro 版同样的 Key 却正常——5 分钟排查定位指南
java·服务器·数据库·ai
旧梦95271 小时前
Java SortedMap 接口详解:从入门到实战
java·开发语言
莫陌尛.1 小时前
Java_this构造方法
java·开发语言
2601_962297251 小时前
C# vs Java vs Python:YOLO工业部署性能对比实战
java·python·c·工业视觉·性能对比
计算机毕设定制辅导-无忧学长1 小时前
《基于SpringBoot的马术俱乐部管理系统》
java·spring boot·后端
Dreams°1232 小时前
【Java后端+Vue前后端分离:内网正常、公网访问异常|5个高频经典踩坑完整复盘】
java·开发语言·vue.js
myy-learn2 小时前
31-TCP并发
服务器·网络·tcp/ip
程序员夏洛2 小时前
HTTP 请求包含哪些内容,请求头和请求体有哪些类型?
网络·网络协议·http
啊阿狸不会拉杆2 小时前
《计算机网络-自顶向下方法》3.9 小结 · 课后习题 · Wireshark实验
网络·测试工具·wireshark
Chengbei112 小时前
SRC报告成稿 Skill(SRC/0day 提交稿、分层验证门、Step 式 PoC、截图铁律)
网络·人工智能·安全·web安全·自动化·系统安全