架构设计之慢SQL监控
1、慢SQL
慢SQL指查询执行时间超出合理阈值(如数百毫秒以上)的语句,常因索引缺失、复杂联表、全表扫描或数据量激增导致。其直接影响包括响应延迟飙升、吞吐量骤降、系统资源(CPU/IO)耗尽,甚至引发服务雪崩。
我们可以使用第三方的组件(如:p6spy)来记录sql的执行情况
https://github.com/p6spy/p6spy
为实现技术能力进阶,我们自主开发
了一个组件实现慢 SQL 监控。该组件支持JDBC、MyBatis 及 JPA 的 SQL 执行过程进行无侵入拦截,通过代理技术捕获数据库操作。当 SQL 执行耗时超过预设阈值(如 500ms)时,组件自动触发日志记录,输出结构化监控信息,涵盖 完整 SQL 语句(含参数占位符及实际值)、执行耗时等关键信息进行记录,为性能分析提供全链路数据支撑。
最终效果如下:
mysql
---------------------------
SQL: SELECT id,name,password,sex,age,phone,birthday FROM t_user WHERE id=?
Parameters: [1]
Slow SQL: true
Execution Time: 2ms
---------------------------
mysql
CREATE TABLE `t_user` (
`id` bigint NOT NULL,
`name` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`sex` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`age` int DEFAULT NULL,
`phone` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`birthday` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
2、定义可配置信息
java
package com.xx.datasource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:16
*/
@Component
@ConfigurationProperties(prefix = "xx.sql")
@Data
public class SqlProperties {
/**
* 是否显示sql
*/
private boolean showSql = true ;
/**
* 是否显示执行SQL动态参数
*/
private boolean showParam = false ;
/**
* 是否显示慢SQL
*/
private boolean slowSql = false ;
/**
* 慢SQL所执行时间的阈值 单位 毫秒
*/
private int slowSqlThreshold = 1000 ;
}
3、配置文件
yaml
xx:
sql:
show_sql: true
show_param: true
slow_sql: true
slow_sql_threshold: 1000 # 自己测试的时候可以调小一点 单位毫秒
4、自定义数据源
我们需要拦截DataSource#getConnection方法,因为JDBC中的Connection对象用于创建对应的Statement对象,这是拦截SQL语句执行的关键前提。
java
package com.xx.datasource;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.annotation.Resource;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:18
*/
public class XxDataSource extends HikariDataSource {
@Resource
private SqlProperties sqlProperties ;
@Override
public Connection getConnection() throws SQLException {
Connection connection = super.getConnection();
// 判断只有开启了显示SQL功能,才会进行代理的创建(这里你也可以加入日志级别的判断)。我们使用的JDK动态代理实现,所以这里的核心是InvocationHandler对象。
if (this.sqlProperties.isShowSql()) {
return (Connection) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class<?>[] {Connection.class},
new ConnectionInvocationHandler(connection, sqlProperties)) ;
}
return connection ;
}
}
首先,判断只有开启了显示SQL功能,才会进行代理的创建(这里你也可以加入日志级别的判断)。我们使用的JDK动态代理实现,所以这里的核心是InvocationHandler对象。
java
package com.xx.datasource;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:22
*/
public class ConnectionInvocationHandler implements InvocationHandler {
private final Connection target;
private final SqlProperties sqlProperties ;
public ConnectionInvocationHandler(Connection target,
SqlProperties slowSqlProperties) {
this.target = target;
this.sqlProperties = slowSqlProperties ;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("prepareStatement")) {
PreparedStatement realStatement = (PreparedStatement) method.invoke(target, args) ;
// 原始 SQL(带 ? 占位符)
String rawSql = (String) args[0];
// 返回代理的 PreparedStatement,并绑定原始 SQL
return Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{PreparedStatement.class},
new PreparedStatementInvocationHandler(realStatement, rawSql, sqlProperties)
);
}
return method.invoke(target, args);
}
}
5、拦截PreparedStatement
通过拦截PreparedStatement对象相应的set方法来获取动态设置的执行参数以及拦截execute开头的方法来记录SQL执行的耗时情况。
java
package com.xx.datasource;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:19
*/
@Slf4j
public class PreparedStatementInvocationHandler implements InvocationHandler {
private final PreparedStatement target;
private final String rawSql;
private final SqlProperties sqlProperties ;
private final Map<Integer, Object> paramMap = new HashMap<>();
public PreparedStatementInvocationHandler(PreparedStatement target,
String rawSql, SqlProperties sqlProperties) {
this.target = target;
this.rawSql = rawSql;
this.sqlProperties = sqlProperties ;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 拦截所有 setXXX() 方法,保存参数值
if (method.getName().startsWith("set")) {
int paramIndex = (int) args[0] ;
Object paramValue = args[1] ;
paramMap.put(paramIndex, paramValue) ;
}
// 拦截 execute() 方法,记录完整 SQL 和 参数
if (method.getName().startsWith("execute")) {
String sql = sqlAndParams() ;
if (this.sqlProperties.isSlowSql()) {
long start = System.currentTimeMillis() ;
Object ret = method.invoke(target, args) ;
long duration = System.currentTimeMillis() - start ;
if (duration > this.sqlProperties.getSlowSqlThreshold()) {
log.warn("{}", buildSlowLogMessage(this.rawSql, sqlParamValues(), duration)) ;
} else {
log.info("Executed SQL: {}", sql) ;
}
return ret ;
} else {
log.info("Executed SQL: {}", sql) ;
}
}
return method.invoke(target, args) ;
}
private String buildSlowLogMessage(String sql, List<String> params, long executionTime) {
StringBuilder sb = new StringBuilder();
sb.append("\n---------------------------\n") ;
sb.append("SQL Execution Details\n");
sb.append("---------------------------\n") ;
sb.append("SQL: ").append(sql).append("\n") ;
sb.append("Parameters: ").append(params.toString()).append("\n") ;
if (this.sqlProperties.isSlowSql() && executionTime > this.sqlProperties.getSlowSqlThreshold()) {
sb.append("Slow SQL: true\n") ;
sb.append("Execution Time: ").append(executionTime).append("ms\n");
}
sb.append("---------------------------");
return sb.toString();
}
private String sqlAndParams() {
// 获取键的集合并排序
List<Integer> sortedKeys = new ArrayList<>(paramMap.keySet());
Collections.sort(sortedKeys);
if (this.sqlProperties.isShowParam()) {
return rawSql + ", Params: " + sortedKeys.stream()
.map(paramMap::get)
.map(Object::toString)
.collect(Collectors.toList())
.toString() ;
}
return rawSql ;
}
private List<String> sqlParamValues() {
return new ArrayList<>(paramMap.keySet()).stream()
.map(paramMap::get)
.map(Object::toString)
.collect(Collectors.toList());
}
}
6、配置数据源
java
package com.xx.datasource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:21
*/
@Configuration
public class DataSourceConfig {
@Bean
// 表示从配置文件中读取以 spring.datasource.hikari 开头的属性,自动绑定到 DataSourceBuilder 中。
@ConfigurationProperties(prefix = "spring.datasource.hikari")
XxDataSource dataSource(DataSourceProperties properties) {
return (XxDataSource) DataSourceBuilder.create()
.type(XxDataSource.class)
.driverClassName(properties.getDriverClassName())
.url(properties.getUrl())
.username(properties.getUsername())
.password(properties.getPassword())
.build() ;
}
}
我们还是使用系统提供的默认数据源配置,DataSourceProperties是Spring Boot底层自动配置的属性(spring.datasource下的配置)。
7、测试
1、User
java
package com.xx.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:14
*/
@TableName("t_user")
@Data
public class User implements Serializable {
@TableId
private Long id;
private String name;
private String password;
private String sex;
private Integer age;
private String phone;
private Date birthday;
}
2、UserMapper
java
package com.xx.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xx.entity.User;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:15
*/
public interface UserMapper extends BaseMapper<User> {
@Select(value = "select * from t_user where name = #{name} or age = #{age}")
List<User> queryUser(String name, Integer age) ;
}
3、TestController
java
package com.xx.controller;
import com.xx.entity.User;
import com.xx.mapper.UserMapper;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:03
*/
@RestController
public class TestController {
@Resource
private UserMapper userMapper;
@GetMapping("/testCreate")
public void testCreate() {
User user = new User();
user.setId(1L);
user.setAge(19);
user.setBirthday(new Date());
user.setName("小薛科技");
user.setPassword("123123");
user.setPhone("1111111111");
user.setSex("男");
this.userMapper.insert(user);
}
@GetMapping("/testQuery")
public void testQuery() {
this.userMapper.selectById(1L);
}
@GetMapping("/testQueryByNameAndAage")
public void testQueryByNameAndAage() {
this.userMapper.queryUser("小薛科技", 11);
}
}
8、启动类
java
package com.xx;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @Author: xueqimiao
* @Date: 2025/5/29 14:13
*/
@SpringBootApplication
@MapperScan(basePackages = {"com.xx.mapper"})
public class SpringBootSlowSQLApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootSlowSQLApplication.class, args);
}
}
9、完整application.yml
yaml
spring:
application:
name: slow_sql_record
---
logging:
include-application-name: false
pattern:
dateformat: 21:mm
---
spring:
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xx_db2025?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=UTF-8&rewriteBatchedStatements=true&cachePrepStmts=true
username: root
password: mac_root
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 100
maximumPoolSize: 100
autoCommit: true
idleTimeout: 30000
poolName: PackHikariCP
maxLifetime: 1800000
connectionTimeout: 30000
connectionTestQuery: SELECT 1
---
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
type-aliases-package: com.xx.entity
mapper-locations:
- classpath:mapper/*.xml
---
xx:
sql:
show_sql: true
show_param: true
slow_sql: true
slow_sql_threshold: 1
10、完整pom
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/>
</parent>
<artifactId>xx-spring-boot-slow-sql</artifactId>
<description>慢SQL日志记录</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.10.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>