SpringBoot 实现QPS监控:别等系统“咳血”了才想起装“心电图”!

大家好,我是小悟。

一、什么是QPS?------ 系统的"心跳频率" 💗

想象一下你的系统就像一个忙碌的外卖小哥,QPS(Query Per Second)就是他每秒能送多少份外卖!如果小哥每秒只能送1单,那估计顾客早就饿晕在厕所了;要是每秒能送100单,那他绝对是"闪电侠"附体!

正常系统的QPS就像人的心跳:

  • 60-100 QPS:健康小伙子,心跳平稳
  • 100-1000 QPS:健身达人,有点小激动
  • 1000+ QPS:跑马拉松呢!快喘口气!
  • 10000+ QPS:这货是打了鸡血吧?

二、方案大比拼------给系统装上"智能手环"

方案1:简易版手环(AOP拦截器)

适合小项目,就像给系统戴个手环

java 复制代码
@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)

适合大项目,就像给系统做全面体检

typescript 复制代码
@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)

给老板看的,必须高大上!

yaml 复制代码
# 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
swift 复制代码
@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实施步骤(简易版):

  1. 添加依赖 :给你的pom.xml来点"维生素"
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 启用AOP:在主类上贴个"创可贴"
less 复制代码
@SpringBootApplication
@EnableAspectJAutoProxy  // 启用AOP魔法
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        System.out.println("QPS监控小鸡破壳而出!");
    }
}
  1. 创建切面类 :如上文的QpsMonitorAspect
  2. 测试一下:疯狂刷新接口,看看控制台输出

方案2实施步骤(专业版):

  1. 添加全家桶依赖
xml 复制代码
<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>
  1. 配置application.yml
yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}
  endpoint:
    metrics:
      enabled: true
  1. 访问监控数据
bash 复制代码
http://localhost:8080/actuator/metrics  # 查看所有指标
http://localhost:8080/actuator/prometheus  # Prometheus格式

方案3实施步骤(豪华版):

  1. 搭建Spring Boot Admin Server
less 复制代码
@SpringBootApplication
@EnableAdminServer
public class AdminServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AdminServerApplication.class, args);
        System.out.println("监控大屏已就位,陛下请检阅!");
    }
}
  1. 客户端配置:如上文yml配置
  2. 访问Admin UIhttp://localhost:9090

四、QPS统计的进阶技巧

1. 滑动窗口统计(最近N秒的QPS)

arduino 复制代码
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响应时间)

scss 复制代码
@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的自动熔断

java 复制代码
@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. 最佳实践提醒

arduino 复制代码
// 记住这些黄金法则:
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不规范,运维两行泪!

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海

相关推荐
无敌最俊朗@5 小时前
STL-vector面试剖析(面试复习4)
java·面试·职场和发展
追逐时光者5 小时前
一款开源、现代化的 WinForm UI 控件库
后端·.net
PPPPickup5 小时前
easychat项目复盘---获取联系人列表,联系人详细,删除拉黑联系人
java·前端·javascript
LiamTuc5 小时前
Java构造函数
java·开发语言
长安er6 小时前
LeetCode 206/92/25 链表翻转问题-“盒子-标签-纸条模型”
java·数据结构·算法·leetcode·链表·链表翻转
菜鸟plus+6 小时前
N+1查询
java·服务器·数据库
我要添砖java6 小时前
《JAVAEE》网络编程-什么是网络?
java·网络·java-ee
CoderYanger6 小时前
动态规划算法-01背包问题:50.分割等和子集
java·算法·leetcode·动态规划·1024程序员节
花月C6 小时前
个性化推荐:基于用户的协同过滤算法
开发语言·后端·算法·近邻算法
cci7 小时前
还在用conda?,试试uv,提高包的安装速度
后端