大家好,我是小悟。
一、什么是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实施步骤(简易版):
- 添加依赖 :给你的
pom.xml来点"维生素"
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 启用AOP:在主类上贴个"创可贴"
less
@SpringBootApplication
@EnableAspectJAutoProxy // 启用AOP魔法
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
System.out.println("QPS监控小鸡破壳而出!");
}
}
- 创建切面类 :如上文的
QpsMonitorAspect - 测试一下:疯狂刷新接口,看看控制台输出
方案2实施步骤(专业版):
- 添加全家桶依赖:
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>
- 配置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
- 访问监控数据:
bash
http://localhost:8080/actuator/metrics # 查看所有指标
http://localhost:8080/actuator/prometheus # Prometheus格式
方案3实施步骤(豪华版):
- 搭建Spring Boot Admin Server:
less
@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)
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不规范,运维两行泪!

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海