大家好,我是小悟。
一、什么是QPS?—— 系统的”心跳频率” 💗
想象一下你的系统就像一个忙碌的外卖小哥,QPS(Query Per Second)就是他每秒能送多少份外卖!如果小哥每秒只能送1单,那估计顾客早就饿晕在厕所了;要是每秒能送100单,那他绝对是”闪电侠”附体!
正常系统的QPS就像人的心跳:
- 60-100 QPS:健康小伙子,心跳平稳
- 100-1000 QPS:健身达人,有点小激动
- 1000+ QPS:跑马拉松呢!快喘口气!
- 10000+ QPS:这货是打了鸡血吧?
二、方案大比拼——给系统装上”智能手环”
方案1:简易版手环(AOP拦截器)
适合小项目,就像给系统戴个手环
@Slf4j @Aspect @Component public class QpsMonitorAspect { // 用ConcurrentHashMap存计数器,线程安全! private final ConcurrentHashMap<String, AtomicLong> counterMap = new ConcurrentHashMap<>(); private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); @PostConstruct public void init() { log.info("QPS监控龟龟已启动,开始慢慢爬..."); // 每秒统计一次,像乌龟一样稳定 scheduler.scheduleAtFixedRate(this::printQps, 0, 1, TimeUnit.SECONDS); } @Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " + "@annotation(org.springframework.web.bind.annotation.PostMapping)") public Object countQps(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); // 计数器自增,像小松鼠囤松果一样积极 counterMap.computeIfAbsent(methodName, k -> new AtomicLong(0)) .incrementAndGet(); long start = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long cost = System.currentTimeMillis() - start; // 顺便记录一下响应时间,看看系统是不是"老了腿脚慢" if (cost > 1000) { log.warn("方法 {} 执行了 {}ms,比蜗牛还慢!", methodName, cost); } } } private void printQps() { if (counterMap.isEmpty()) { log.info("系统在睡大觉,没有请求..."); return; } StringBuilder sb = new StringBuilder("\n========== QPS报告 ==========\n"); counterMap.forEach((method, counter) -> { long qps = counter.getAndSet(0); // 重置计数器 String status = ""; if (qps > 1000) status = ""; if (qps > 5000) status = ""; sb.append(String.format("%s %-40s : %d QPS%n", status, method, qps)); }); sb.append("================================"); log.info(sb.toString()); } }
方案2:专业版体检仪(Micrometer + Prometheus)
适合大项目,就像给系统做全面体检
@Configuration public class MetricsConfig { @Bean public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() { return registry -> { registry.config().commonTags("application", "my-awesome-app"); log.info("系统体检中心开业啦!欢迎随时来检查身体~"); }; } } @Service public class OrderService { private final Counter orderCounter; private final Timer orderTimer; private final MeterRegistry meterRegistry; public OrderService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; // 创建订单计数器,像收银机一样"叮叮叮" this.orderCounter = Counter.builder("order.count") .description("订单数量统计") .tag("type", "create") .register(meterRegistry); // 创建订单耗时计时器 this.orderTimer = Timer.builder("order.process.time") .description("订单处理时间") .register(meterRegistry); } public Order createOrder(OrderDTO dto) { // 记录方法执行时间 return orderTimer.record(() -> { log.debug("正在打包订单,请稍候..."); // 业务逻辑... Order order = doCreateOrder(dto); // 订单创建成功,计数器+1 orderCounter.increment(); // 动态QPS统计(最近1分钟) double qps = meterRegistry.get("order.count") .counter() .measure() .stream() .findFirst() .map(Measurement::getValue) .orElse(0.0) / 60.0; if (qps > 100) { log.warn("订单处理太快了!当前QPS: {}/s,考虑加点运费?", qps); } return order; }); } // 动态查看QPS的API @GetMapping("/metrics/qps") public Map<String, Object> getRealTimeQps() { Map<String, Object> metrics = new HashMap<>(); // 收集所有接口的QPS meterRegistry.getMeters().forEach(meter -> { String meterName = meter.getId().getName(); if (meterName.contains(".count")) { double qps = meter.counter().count() / 60.0; // 转换为每秒 metrics.put(meterName, String.format("%.2f QPS", qps)); // 添加表情包增强可视化效果 String emoji = ""; if (qps > 100) emoji = ""; if (qps > 500) emoji = ""; metrics.put(meterName + "_emoji", emoji); } }); metrics.put("report_time", LocalDateTime.now()); metrics.put("message", "系统当前状态良好,吃嘛嘛香!"); return metrics; } }
方案3:豪华版监控大屏(Spring Boot Admin)
给老板看的,必须高大上!
# application.yml spring: boot: admin: client: url: http://localhost:9090 # Admin Server地址 instance: name: "青龙系统" metadata: owner: "码农小张" department: "爆肝事业部" management: endpoints: web: exposure: include: "*" # 暴露所有端点,不穿"隐身衣" metrics: export: prometheus: enabled: true endpoint: health: show-details: ALWAYS @RestController @Slf4j public class QpsDashboardController { @GetMapping("/dashboard/qps") public String qpsDashboard() { // 模拟从各个服务收集QPS数据 Map<String, Double> serviceQps = getClusterQps(); // 生成ASCII艺术报表 StringBuilder dashboard = new StringBuilder(); dashboard.append("\n"); dashboard.append("╔══════════════════════════════════════════╗\n"); dashboard.append("║ 系统QPS监控大屏 ║\n"); dashboard.append("╠══════════════════════════════════════════╣\n"); serviceQps.forEach((service, qps) -> { // 生成进度条 int bars = (int) Math.min(qps / 10, 50); String progressBar = "█".repeat(bars) + "░".repeat(50 - bars); String status = "正常"; if (qps > 500) status = "警告"; if (qps > 1000) status = "紧急"; dashboard.append(String.format("║ %-15s : %-30s ║\n", service, progressBar)); dashboard.append(String.format("║ %6.1f QPS %-20s ║\n", qps, status)); }); dashboard.append("╚══════════════════════════════════════════╝\n"); // 添加系统健康建议 dashboard.append("\n 系统建议:\n"); double maxQps = serviceQps.values().stream().max(Double::compare).orElse(0.0); if (maxQps < 50) { dashboard.append(" 系统有点闲,可以考虑接点私活~ \n"); } else if (maxQps > 1000) { dashboard.append(" 系统快冒烟了!快加机器!\n"); } else { dashboard.append(" 状态完美,继续保持!\n"); } return dashboard.toString(); } // 定时推送QPS警告 @Scheduled(fixedRate = 60000) public void checkQpsAlert() { Map<String, Double> currentQps = getClusterQps(); currentQps.forEach((service, qps) -> { if (qps > 1000) { log.error("救命!{}服务QPS爆表了:{},快看看是不是被爬了!", service, qps); // 这里可以接入钉钉/企业微信告警 sendAlertToDingTalk(service, qps); } }); } private void sendAlertToDingTalk(String service, double qps) { String message = String.format( "{\"msgtype\": \"text\", \"text\": {\"content\": \"%s服务QPS异常:%.1f,快去看看吧!\"}}", service, qps ); // 调用钉钉webhook log.warn("已发送钉钉告警:{}", message); } }
三、方案详细实施步骤
方案1实施步骤(简易版):
- 添加依赖:给你的
pom.xml来点”维生素”
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 启用AOP:在主类上贴个”创可贴”
@SpringBootApplication @EnableAspectJAutoProxy // 启用AOP魔法 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); System.out.println("QPS监控小鸡破壳而出!"); } }
- 创建切面类:如上文的
QpsMonitorAspect - 测试一下:疯狂刷新接口,看看控制台输出
方案2实施步骤(专业版):
- 添加全家桶依赖:
<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
- 配置application.yml:
management: endpoints: web: exposure: include: health,info,metrics,prometheus metrics: export: prometheus: enabled: true tags: application: ${spring.application.name} endpoint: metrics: enabled: true
- 访问监控数据:
http://localhost:8080/actuator/metrics # 查看所有指标 http://localhost:8080/actuator/prometheus # Prometheus格式
方案3实施步骤(豪华版):
- 搭建Spring Boot Admin Server:
@SpringBootApplication @EnableAdminServer public class AdminServerApplication { public static void main(String[] args) { SpringApplication.run(AdminServerApplication.class, args); System.out.println("监控大屏已就位,陛下请检阅!"); } }
- 客户端配置:如上文yml配置
- 访问Admin UI:
http://localhost:9090
四、QPS统计的进阶技巧
1. 滑动窗口统计(最近N秒的QPS)
public class SlidingWindowQpsCounter { // 用环形队列实现滑动窗口 private final LinkedList<Long> timestamps = new LinkedList<>(); private final int windowSeconds; public SlidingWindowQpsCounter(int windowSeconds) { this.windowSeconds = windowSeconds; log.info("创建滑动窗口监控,窗口大小:{}秒", windowSeconds); } public synchronized void hit() { long now = System.currentTimeMillis(); timestamps.add(now); // 移除窗口外的记录 while (!timestamps.isEmpty() && now - timestamps.getFirst() > windowSeconds * 1000) { timestamps.removeFirst(); } } public double getQps() { return timestamps.size() / (double) windowSeconds; } }
2. 分位数统计(P90/P95/P99响应时间)
@Bean public MeterRegistryCustomizer<MeterRegistry> addQuantiles() { return registry -> { DistributionStatisticConfig config = DistributionStatisticConfig.builder() .percentiles(0.5, 0.9, 0.95, 0.99) // 50%, 90%, 95%, 99% .percentilePrecision(2) .build(); registry.config().meterFilter( new MeterFilter() { @Override public DistributionStatisticConfig configure( Meter.Id id, DistributionStatisticConfig config) { if (id.getName().contains(".timer")) { return config.merge(DistributionStatisticConfig.builder() .percentiles(0.5, 0.9, 0.95, 0.99) .build()); } return config; } } ); log.info("分位数统计已启用,准备精准打击慢查询!"); }; }
3. 基于QPS的自动熔断
@Component public class AdaptiveCircuitBreaker { private volatile boolean circuitOpen = false; private double currentQps = 0; @Scheduled(fixedRate = 1000) public void monitorAndAdjust() { // 获取当前QPS currentQps = calculateCurrentQps(); if (circuitOpen && currentQps < 100) { circuitOpen = false; log.info("熔断器关闭,系统恢复供电!当前QPS: {}", currentQps); } else if (!circuitOpen && currentQps > 1000) { circuitOpen = true; log.error("熔断器触发!QPS过高: {},系统进入保护模式", currentQps); } // 动态调整线程池大小 adjustThreadPool(currentQps); } private void adjustThreadPool(double qps) { int suggestedSize = (int) (qps * 0.5); // 经验公式 log.debug("建议线程池大小调整为: {} (基于QPS: {})", suggestedSize, qps); } }
五、总结:给系统做QPS监控就像…
1. 为什么要监控QPS?
- 对系统:就像给汽车装时速表,超速了会报警
- 对开发:就像给程序员装”健康手环”,代码跑太快会冒烟
- 对老板:就像给公司装”业绩大屏”,数字好看心情好
2. 各方案选择建议:
- 初创公司/小项目:用方案1,简单粗暴见效快,就像”创可贴”
- 中型项目/微服务:用方案2,全面体检不遗漏,就像”年度体检”
- 大型分布式系统:用方案3,全景监控无死角,就像”卫星监控”
3. 最佳实践提醒:
// 记住这些黄金法则: public class QpsGoldenRules { // 法则1:监控不是为了监控而监控 public static final String RULE_1 = "别让监控把系统压垮了!"; // 法则2:告警要有意义 public static final String RULE_2 = "狼来了喊多了,就没人信了!"; // 法则3:数据要可视化 public static final String RULE_3 = "老板看不懂的图表都是废纸!"; // 法则4:要有应对方案 public static final String RULE_4 = "光报警不解决,要你有何用?"; }
4. 最后总结:
给你的系统加QPS监控,就像是:
- 给外卖小哥配了计步器 —— 知道他每天跑多少
- 给程序员装了键盘计数器 —— 知道他有多卷
- 给系统装了”心电图机” —— 随时掌握生命体征
记住,一个健康的系统应该:
- 平时 心跳平稳(QPS稳定)
- 大促时 适当兴奋(弹性扩容)
- 故障时 自动降压(熔断降级)
现在就去给你的SpringBoot系统装上”智能手环”吧!让它在代码的海洋里,游得更快、更稳、更健康!
最后的最后:监控千万条,稳定第一条;QPS不规范,运维两行泪!
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海