在企业级应用开发中,数据库性能往往是系统整体性能的关键瓶颈。慢查询不仅会影响用户体验,还可能导致连接池耗尽,进而引发系统雪崩。
因此,对数据库慢查询进行有效监控和及时优化,是保障系统稳定运行的重要环节。
本文将介绍6种在SpringBoot应用中实现慢查询监控的方案。
一、数据库原生慢查询日志
原理概述
几乎所有主流关系型数据库都提供了内置的慢查询日志功能,通过设置阈值,将执行时间超过阈值的SQL记录到专门的日志文件中。
实现方式
以MySQL为例:
- 配置慢查询日志
修改MySQL配置文件(my.cnf):
ini
# 开启慢查询日志
slow_query_log = 1
# 慢查询日志文件位置
slow_query_log_file = /var/log/mysql/mysql-slow.log
# 设置慢查询阈值(秒)
long_query_time = 1
# 记录没有使用索引的查询
log_queries_not_using_indexes = 1
- 在SpringBoot中查看慢查询日志
typescript
@Repository
public class SlowQueryAnalyzer {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> getSlowQueries() {
String sql = "SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 100";
return jdbcTemplate.queryForList(sql);
}
public void analyzeSlowQueries() {
String sql = "SELECT COUNT(*) as count, db, sql_text, AVG(query_time) as avg_time " +
"FROM mysql.slow_log " +
"GROUP BY db, sql_text " +
"ORDER BY avg_time DESC " +
"LIMIT 10";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql);
// 处理结果...
}
}
优缺点分析
优点:
- 零代码侵入,无需修改应用代码
- 数据库原生支持,准确性高
- 可捕获所有慢查询,包括非应用发起的查询
缺点:
- 需要数据库管理员权限配置
- 增加数据库I/O负担,生产环境需谨慎使用
- 日志分析需要额外工具支持
- 无法与应用上下文关联(如调用方法、请求URL等)
适用场景
- 开发和测试环境的问题排查
- 对数据库有完全控制权的场景
- 需要捕获所有数据库操作的场景
- 基础设施层面的监控需求
二、基于AOP的慢查询监控
原理概述
利用Spring AOP机制,在Repository方法执行前后添加切面,计算执行时间并记录超过阈值的方法调用。
实现方式
- 添加AOP依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 创建慢查询监控切面
less
@Aspect
@Component
@Slf4j
public class SlowQueryAspect {
@Value("${slow.query.threshold:500}")
private long slowQueryThreshold; // 默认阈值500毫秒
@Around("execution(* com.example.repository.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
if (executionTime > slowQueryThreshold) {
String args = Arrays.toString(joinPoint.getArgs());
log.warn("Slow Query detected: {} with args {}, execution time: {} ms",
methodName, args, executionTime);
// 可以将慢查询信息保存到数据库或发送告警
saveSlowQueryInfo(methodName, args, executionTime);
}
return result;
}
private void saveSlowQueryInfo(String methodName, String args, long executionTime) {
// 保存慢查询信息到数据库或发送到监控系统
}
}
- 创建慢查询事件监听器(可选)
java
@Component
@Slf4j
public class SlowQueryEventListener {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void onSlowQuery(String methodName, String args, long executionTime) {
SlowQueryEvent event = new SlowQueryEvent(this, methodName, args, executionTime);
eventPublisher.publishEvent(event);
}
@EventListener
public void handleSlowQueryEvent(SlowQueryEvent event) {
// 处理慢查询事件,如发送告警邮件、存储到时序数据库等
log.warn("Handling slow query event: {}", event);
}
}
@Getter
public class SlowQueryEvent extends ApplicationEvent {
private final String methodName;
private final String args;
private final long executionTime;
public SlowQueryEvent(Object source, String methodName, String args, long executionTime) {
super(source);
this.methodName = methodName;
this.args = args;
this.executionTime = executionTime;
}
}
优缺点分析
优点:
- 实现简单,代码侵入性低
- 可以捕获完整的方法调用上下文
- 灵活可定制,可以根据需求调整监控策略
- 可以与应用现有的监控系统集成
缺点:
- 只能监控应用代码中的查询,无法监控原生SQL
- 性能开销较大,特别是在高并发场景
- 可能出现AOP失效的场景(如内部方法调用)
- 无法获取到实际执行的SQL语句
适用场景
- 小型应用或并发量不大的系统
- 需要监控特定Repository方法性能的场景
- 开发或测试环境的性能调优
- 已经广泛使用Spring AOP的项目
三、Spring Boot Actuator + Micrometer
原理概述
利用Spring Boot Actuator和Micrometer提供的指标收集功能,监控数据库操作性能,并将数据导出到监控系统。
实现方式
- 添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
- 配置Actuator和数据源监控
在application.properties
中添加:
ini
# 开启所有Actuator端点
management.endpoints.web.exposure.include=*
# 启用数据库指标收集
management.metrics.enable.jdbc=true
# 配置Prometheus端点
management.metrics.export.prometheus.enabled=true
- 自定义数据源代理,添加指标收集
less
@Configuration
public class DataSourceProxyConfig {
@Bean
@Primary
public DataSource dataSource(DataSource originalDataSource, MeterRegistry meterRegistry) {
return ProxyDataSourceBuilder
.create(originalDataSource)
.name("metrics-ds")
.listener(new QueryExecutionListener() {
@Override
public void beforeQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
// 查询执行前的操作
}
@Override
public void afterQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
long elapsedTime = execInfo.getElapsedTime();
// 记录查询时间指标
Timer.builder("datasource.query.time")
.tag("success", String.valueOf(execInfo.isSuccess()))
.tag("query", getSafeQueryName(queryInfoList))
.register(meterRegistry)
.record(elapsedTime, TimeUnit.MILLISECONDS);
// 检测慢查询并记录
if (elapsedTime > 500) { // 500ms阈值
Counter.builder("datasource.slow.queries")
.tag("query", getSafeQueryName(queryInfoList))
.register(meterRegistry)
.increment();
// 可以记录慢查询日志
logSlowQuery(execInfo, queryInfoList);
}
}
private String getSafeQueryName(List<QueryInfo> queryInfoList) {
if (queryInfoList.isEmpty()) {
return "unknown";
}
String sql = queryInfoList.get(0).getQuery();
// 简化SQL以避免过多的唯一标签
return DigestUtils.md5DigestAsHex(sql.getBytes()).substring(0, 8);
}
private void logSlowQuery(ExecutionInfo execInfo, List<QueryInfo> queryInfoList) {
// 记录慢查询详情
}
})
.build();
}
}
ProxyDataSourceBuilder 来自开源库
datasource-proxy
,datasource-proxy
可以用于 JDBC 数据源的代理,可以用来拦截和监控 SQL 查询执行,实现 SQL 日志记录、性能监控、查询统计等功能。
xml
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<version>1.9</version>
</dependency>
- 创建自定义端点查看慢查询
scss
@Component
@Endpoint(id = "slowqueries")
public class SlowQueryEndpoint {
@Autowired
private MeterRegistry meterRegistry;
@ReadOperation
public Map<String, Object> slowQueries() {
Map<String, Object> result = new HashMap<>();
// 获取慢查询计数器
List<Meter> meters = meterRegistry.getMeters().stream()
.filter(m -> m.getId().getName().equals("datasource.slow.queries"))
.collect(Collectors.toList());
Map<String, Double> queryCounts = new HashMap<>();
for (Meter meter : meters) {
String query = meter.getId().getTag("query");
double count = ((Counter) meter).count();
queryCounts.put(query, count);
}
result.put("counts", queryCounts);
// 获取慢查询时间分布
List<Meter> timers = meterRegistry.getMeters().stream()
.filter(m -> m.getId().getName().equals("datasource.query.time"))
.collect(Collectors.toList());
Map<String, Map<String, Object>> queryTimes = new HashMap<>();
for (Meter meter : timers) {
String query = meter.getId().getTag("query");
Timer timer = (Timer) meter;
Map<String, Object> stats = new HashMap<>();
stats.put("count", timer.count());
stats.put("max", timer.max(TimeUnit.MILLISECONDS));
stats.put("mean", timer.mean(TimeUnit.MILLISECONDS));
stats.put("percentile95", timer.takeSnapshot().percentileValues()[0].value(TimeUnit.MILLISECONDS));
queryTimes.put(query, stats);
}
result.put("times", queryTimes);
return result;
}
}
优缺点分析
优点:
- 与Spring Boot生态紧密集成
- 支持多种监控系统,如Prometheus、Grafana等
- 提供丰富的指标和可视化能力
- 运行时监控,影响生产代码较小
缺点:
- 配置相对复杂
- 资源消耗较大,特别是在大量指标收集的情况下
- 需要额外的监控系统支持
- 学习成本较高
适用场景
- 中大型微服务架构
- 已经使用Prometheus + Grafana等监控系统的团队
- 需要全面监控系统性能的场景
- 对指标和可视化有较高要求的项目
四、使用P6Spy进行SQL性能监控
原理概述
P6Spy是一个开源的JDBC代理框架,能够拦截JDBC操作并记录SQL语句的执行情况,包括执行时间、参数等信息。
实现方式
- 添加依赖
xml
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>3.9.1</version>
</dependency>
- 配置数据源
修改数据源配置,将驱动类替换为P6Spy的代理驱动:
ini
# 原始配置
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.datasource.url=jdbc:mysql://localhost:3306/test
# P6Spy配置
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/test
- 创建P6Spy配置文件
在resources
目录下创建spy.properties
文件:
ini
# 指定日志输出模块
appender=com.p6spy.engine.spy.appender.Slf4JLogger
# 日志格式
logMessageFormat=com.example.config.CustomP6SpyLogFormat
# 是否开启慢SQL记录
outagedetection=true
# 慢SQL阈值(毫秒)
outagedetectioninterval=2000
# 设置 p6spy driver 代理
deregisterdrivers=true
# 日期格式
dateformat=yyyy-MM-dd HH:mm:ss
# 实际驱动
driverlist=com.mysql.cj.jdbc.Driver
- 自定义日志格式化器
java
package com.example.config;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CustomP6SpyLogFormat implements MessageFormattingStrategy {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public String formatMessage(int connectionId, String now, long elapsed, String category,
String prepared, String sql, String url) {
return StringUtils.hasText(sql) ?
LocalDateTime.now().format(formatter) +
" | " + elapsed + "ms | " + category +
" | connection " + connectionId +
" | " + sql : "";
}
}
- 创建P6Spy慢查询监听器
java
package com.example.config;
import com.p6spy.engine.common.ConnectionInformation;
import com.p6spy.engine.event.JdbcEventListener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class SlowQueryListener extends JdbcEventListener {
@Value("${sql.slow.threshold:500}")
private long slowThreshold; // 默认500毫秒
@Override
public void onAfterAnyExecute(ConnectionInformation connectionInformation, long timeElapsedNanos, SQLException e) {
long timeElapsedMillis = TimeUnit.NANOSECONDS.toMillis(timeElapsedNanos);
if (timeElapsedMillis > slowThreshold) {
String query = connectionInformation.getSqlWithValues();
log.warn("Slow SQL detected: {} ms, SQL: {}", timeElapsedMillis, query);
// 可以记录到数据库或发送告警
saveSlowQuery(query, timeElapsedMillis);
}
}
private void saveSlowQuery(String query, long timeElapsed) {
// 保存慢查询记录到数据库或告警系统
}
}
优缺点分析
优点:
- 能够获取完整的SQL语句和参数值
- 配置简单,几乎零代码侵入
- 可以监控所有JDBC操作,包括非ORM框架的查询
- 提供丰富的自定义选项
缺点:
- 对性能有一定影响,不建议在高负载生产环境长期开启
- 日志量较大,需要合理配置
- 可能与某些特定数据库驱动不兼容
- 不提供内置的图形化监控界面
适用场景
- 开发和测试环境的SQL调优
- 需要详细了解SQL执行情况的场景
- 排查特定SQL问题的临时监控
- 对SQL执行参数有监控需求的场景
五、基于APM工具的慢查询监控
原理概述
应用性能监控(APM)工具如SkyWalking、Pinpoint、Elastic APM等通过Java Agent技术在字节码级别插桩,实现对数据库操作的全方位监控。
实现方式
以SkyWalking为例:
- 下载SkyWalking Agent
从SkyWalking官网下载Agent包。
- 配置Java Agent
在启动命令中添加:
ini
java -javaagent:/path/to/skywalking-agent.jar -Dskywalking.agent.service_name=your-service-name -jar your-application.jar
或在Spring Boot应用中通过环境变量配置:
yaml
# application.yml
spring:
application:
name: your-service-name
- 配置SkyWalking Agent
修改agent.config
文件:
ini
# 设置后端服务地址
collector.backend_service=localhost:11800
# 启用SQL跟踪
plugin.jdbc.trace_sql=true
# 设置慢SQL阈值
plugin.jdbc.slow_sql_threshold=1000
- 集成SkyWalking API(可选)
xml
<dependency>
<groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId>
<version>8.7.0</version>
</dependency>
kotlin
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User findUserById(Long id) {
// 添加自定义Tag
ActiveSpan.tag("userId", id.toString());
// 获取traceId,可用于日志关联
String traceId = TraceContext.traceId();
log.info("Processing user query with traceId: {}", traceId);
return userRepository.findById(id).orElse(null);
}
}
- 使用SkyWalking UI查看慢查询
优缺点分析
优点:
- 全方位监控,包括HTTP请求、数据库操作、远程调用等
- 分布式追踪能力,可以跟踪完整调用链
- 零代码侵入(基础功能)
- 提供丰富的可视化界面和告警功能
- 支持多种存储后端(ElasticSearch、MySQL等)
缺点:
- 部署复杂,需要额外维护监控服务器
- 资源消耗较大,增加应用内存占用
- 学习成本较高
- 可能与某些安全策略冲突(如禁止Java Agent)
适用场景
- 中大型分布式系统
- 微服务架构应用
- 需要完整分布式追踪的场景
- 生产环境监控
- 需要同时监控多种性能指标的场景
六、基于Druid连接池的慢查询监控
原理概述
阿里巴巴开源的Druid连接池内置了强大的监控功能,包括慢查询统计、SQL防火墙等。
实现方式
- 添加依赖
xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
- 配置Druid
在application.properties
中添加:
ini
# 数据源类型
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql://localhost:3306/test
spring.datasource.druid.username=root
spring.datasource.druid.password=password
# 连接池配置
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
spring.datasource.druid.max-wait=60000
# 慢SQL监控配置
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=1000
spring.datasource.druid.filter.stat.merge-sql=true
# 开启监控页面
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
spring.datasource.druid.stat-view-servlet.deny=
# 开启Web应用监控
spring.datasource.druid.web-stat-filter.enabled=true
spring.datasource.druid.web-stat-filter.url-pattern=/*
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
# 开启Spring监控
spring.datasource.druid.aop-patterns=com.example.service.*,com.example.repository.*
- 配置Druid监控(Java Config方式)
typescript
@Configuration
public class DruidConfig {
@Bean
public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername", "admin");
initParams.put("loginPassword", "admin");
initParams.put("allow", "127.0.0.1");
bean.setInitParameters(initParams);
return bean;
}
@Bean
public FilterRegistrationBean<WebStatFilter> druidWebStatFilter() {
FilterRegistrationBean<WebStatFilter> bean = new FilterRegistrationBean<>(new WebStatFilter());
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions", "*.js,*.css,/druid/*");
bean.setInitParameters(initParams);
bean.setUrlPatterns(Collections.singletonList("/*"));
return bean;
}
@Bean
@ConfigurationProperties("spring.datasource.druid.filter.stat")
public StatFilter statFilter() {
StatFilter filter = new StatFilter();
filter.setSlowSqlMillis(1000);
filter.setLogSlowSql(true);
filter.setMergeSql(true);
return filter;
}
@Bean
public DruidStatInterceptor druidStatInterceptor() {
return new DruidStatInterceptor();
}
@Bean
public BeanNameAutoProxyCreator druidStatProxyCreator() {
BeanNameAutoProxyCreator creator = new BeanNameAutoProxyCreator();
creator.setProxyTargetClass(true);
creator.setBeanNames("*Service", "*ServiceImpl", "*Repository");
creator.setInterceptorNames("druidStatInterceptor");
return creator;
}
}
- 自定义慢查询监听器(可选)
java
@Component
public class DruidSlowSqlListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private DruidDataSource dataSource;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
dataSource.setConnectionProperties("druid.stat.slowSqlMillis=1000");
StatFilter statFilter = new StatFilter();
statFilter.setLogSlowSql(true);
statFilter.setSlowSqlMillis(1000);
statFilter.setMergeSql(true);
statFilter.setSlowSqlLoggerName("SLOW_SQL_LOGGER");
dataSource.getProxyFilters().add(statFilter);
}
}
- 自定义慢查询Controller(可选)
typescript
@RestController
@RequestMapping("/api/monitor")
public class DruidMonitorController {
@Autowired
private DruidDataSource dataSource;
@GetMapping("/slow-sql")
public List<Map<String, Object>> getSlowSql() {
List<Map<String, Object>> result = new ArrayList<>();
try {
JdbcStatManager statManager = JdbcStatManager.getInstance();
for (Object item : statManager.getDataSourceList().values()) {
JdbcDataSourceStat dataSourceStat = (JdbcDataSourceStat) item;
Map<String, JdbcSqlStat> sqlStatMap = dataSourceStat.getSqlStatMap();
for (Map.Entry<String, JdbcSqlStat> entry : sqlStatMap.entrySet()) {
JdbcSqlStat sqlStat = entry.getValue();
if (sqlStat.getExecuteMillisMax() > 1000) {
Map<String, Object> slowSql = new HashMap<>();
slowSql.put("sql", sqlStat.getSql());
slowSql.put("executionCount", sqlStat.getExecuteCount());
slowSql.put("maxTime", sqlStat.getExecuteMillisMax());
slowSql.put("avgTime", sqlStat.getExecuteMillisTotal() / sqlStat.getExecuteCount());
result.add(slowSql);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
优缺点分析
优点:
- 集成度高,开箱即用
- 自带可视化监控界面
- 功能全面,除慢查询外还有连接池监控、SQL防火墙等
缺点:
- 仅适用于使用Druid连接池的场景
- 与其他监控系统集成需要额外开发
- 默认监控页面功能固定,不易扩展
- 安全配置较为重要,否则可能泄露敏感信息
适用场景
- 对数据库性能有全面监控需求的场景
- 需要开箱即用监控功能的项目
- 小型到中型规模的应用
- 对监控数据安全性有要求的场景
七、方案对比
方案对比
方案 | 实现复杂度 | 代码侵入性 | 性能影响 | 监控全面性 | 可视化能力 |
---|---|---|---|---|---|
数据库原生慢查询日志 | 低 | 无 | 中 | 高 | 低 |
基于AOP的监控 | 低 | 低 | 中 | 中 | 低 |
Spring Boot Actuator + Micrometer | 中 | 低 | 中 | 高 | 高(需外部系统) |
P6Spy | 低 | 低 | 中高 | 高 | 低 |
APM工具(SkyWalking等) | 高 | 低 | 中高 | 极高 | 高 |
Druid连接池 | 低 | 低 | 低 | 高 | 中 |
总结
慢查询监控是数据库性能优化的重要环节,选择合适的监控方案对于提升应用性能至关重要。
在实际应用中,可以根据项目规模、技术栈和团队能力选择合适的方案,也可以组合使用多种方案,实现更全面的监控覆盖。随着应用的发展,监控策略也应该不断演进和优化,以适应不断变化的性能需求。