公司自研 SQL 调用树,3 分钟内定位慢 SQL

2025 最新,钻石VIP学习

2025私活联盟整起!

在复杂的业务系统中,一个接口往往会执行多条SQL,如何直观地看到这些SQL的调用关系和执行情况?

本文将使用SpringBoot + MyBatis拦截器构建一个SQL调用树可视化系统。

公司自研 SQL 调用树,3 分钟内定位慢 SQL公司自研 SQL 调用树,3 分钟内定位慢 SQL

项目背景

在日常开发中,我们常常遇到这样的场景:

复杂查询链路 :一个用户详情接口可能涉及用户基本信息、订单列表、订单详情等多个查询

性能问题排查 :系统响应慢,需要快速定位是哪个SQL影响了性能

开发调试需求 :希望能直观地看到SQL的执行顺序和层次关系

基于这些需求,实现了一个基于SpringBoot + MyBatis的SQL调用树可视化系统。

系统功能特性

该系统具有以下核心功能:

核心功能

MyBatis拦截器 :通过拦截器机制捕获SQL执行过程,无需修改业务代码

调用树构建 :自动构建SQL调用的层次关系

可视化展示 :使用D3.js实现树形结构的可视化展示

性能监控 :记录SQL执行时间,自动标识慢SQL

统计分析 :提供SQL执行统计信息和性能分析

数据管理 :支持数据的查询、清理和导出

技术实现

后端技术 :Spring Boot 3.4.5 + MyBatis 3.0.3 + H2数据库

前端技术 :HTML5 + Tailwind CSS + D3.js v7

配置管理 :支持动态配置慢SQL阈值等参数

项目结构

技术栈

后端技术栈

Spring Boot 3.4.5 :应用框架

MyBatis 3.0.3 :数据访问层和拦截器

H2 Database :内存数据库(演示用)

Lombok :简化代码编写

Jackson :JSON序列化

前端技术栈

HTML5 + Tailwind CSS :页面结构和样式

D3.js v7 :数据可视化

Font Awesome :图标库

原生JavaScript :前端交互逻辑

项目目录结构

 springboot-sql-tree/
├── src/main/java/com/example/sqltree/
│ ├── SqlTreeApplication.java # 启动类
│ ├── SqlInterceptor.java # MyBatis拦截器
│ ├── SqlCallTreeContext.java # 调用树上下文管理
│ ├── SqlNode.java # SQL节点数据模型
│ ├── SqlTreeController.java # REST API控制器
│ ├── DemoController.java # 演示API
│ ├── UserService.java # 用户服务(演示用)
│ ├── UserMapper.java # 用户数据访问
│ └── OrderMapper.java # 订单数据访问
├── src/main/resources/
│ ├── application.yml # 应用配置
│ ├── schema.sql # 数据库表结构
│ ├── data.sql # 示例数据
│ └── static/
│ ├── index.html # 前端页面
│ └── sql-tree.js # 前端JavaScript
└── pom.xml # Maven配置

核心实现详解

1. MyBatis拦截器:零侵入的核心

这是整个系统的 核心组件 ,通过MyBatis的插件机制实现SQL执行的无感知拦截:

 @Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
}),
@Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class
})
})
public class SqlInterceptor implements Interceptor {

@Autowired
private SqlCallTreeContext sqlCallTreeContext;

@Override
public Object intercept(Invocation invocation) throws Throwable {
// 检查是否启用追踪
if (!sqlCallTreeContext.isTraceEnabled) {
return invocation.proceed;
}

long startTime = System.currentTimeMillis;
Object args = invocation.getArgs;
MappedStatement mappedStatement = (MappedStatement) args[0];
Object parameter = args[1];

// 获取SQL信息
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String sql = boundSql.getSql;
String sqlType = mappedStatement.getSqlCommandType.name;

// 获取调用栈信息
StackTraceElement stackTrace = Thread.currentThread.getStackTrace;
String serviceName = extractServiceName(stackTrace);
String methodName = extractMethodName(stackTrace);

// 创建SQL节点
SqlNode sqlNode = SqlNode.builder
.nodeId(UUID.randomUUID.toString)
.sql(formatSql(sql))
.sqlType(sqlType)
.threadName(Thread.currentThread.getName)
.serviceName(serviceName)
.methodName(methodName)
.startTime(LocalDateTime.now)
.parameters(extractParameters(boundSql, parameter))
.depth(sqlCallTreeContext.getCurrentDepth + 1)
.build;

// 进入SQL调用
sqlCallTreeContext.enter(sqlNode);

try {
// 执行SQL
Object result = invocation.proceed;

// 记录执行结果
long executionTime = System.currentTimeMillis - startTime;
int affectedRows = calculateAffectedRows(result, sqlType);

sqlCallTreeContext.exit(sqlNode, affectedRows, );

return result;

} catch (Exception e) {
// 记录异常信息
sqlCallTreeContext.exit(sqlNode, 0, e.getMessage);
throw e;
}
}

private String extractServiceName(StackTraceElement[] stackTrace) {
for (StackTraceElement element : stackTrace) {
String className = element.getClassName;
if (className.contains("Service") && !className.contains("$")) {
return className.substring(className.lastIndexOf('.') + 1);
}
}
return "Unknown";
}

private String extractMethodName(StackTraceElement[] stackTrace) {
for (StackTraceElement element : stackTrace) {
if (element.getClassName.contains("Service")) {
return element.getMethodName;
}
}
return "unknown";
}

private int calculateAffectedRows(Object result, String sqlType) {
if ("SELECT".equals(sqlType) && result instanceof List) {
return ((List>) result).size;
} else if (result instanceof Integer) {
return (Integer) result;
}
return 0;
}
}

关键特性

精准拦截 :同时拦截查询和更新操作

异常安全 :确保业务逻辑不受监控影响

丰富信息 :自动提取Service调用信息和执行统计

2. 调用树上下文管理器:线程安全的数据管理

SqlCallTreeContext 负责管理SQL调用树的构建和存储,采用线程安全的设计:

 @Component
public class SqlCallTreeContext {

// 线程本地存储
private final ThreadLocal
@Override
protected Stack
return new Stack;
}
};

private final ThreadLocal
@Override
protected List
return new ArrayList;
}
};

// 全局会话存储
private final Map

// 统计信息
private final AtomicLong totalSqlCount = new AtomicLong(0);
private final AtomicLong slowSqlCount = new AtomicLong(0);
private final AtomicLong errorSqlCount = new AtomicLong(0);
private final AtomicLong totalExecutionTime = new AtomicLong(0);

// 配置参数
private volatile long slowSqlThreshold = 1000; // 慢SQL阈值(毫秒)
private volatile boolean traceEnabled = true; // 追踪开关

/**
* 进入SQL调用
*/
public SqlNode enter(SqlNode sqlNode) {
if (!traceEnabled) {
return sqlNode;
}

Stack

// 设置深度
sqlNode.setDepth(stack.size + 1);

// 建立父子关系
if (!stack.isEmpty) {
SqlNode parent = stack.peek;
parent.addChild(sqlNode);
sqlNode.setParentId(parent.getNodeId);
} else {
// 根节点
rootNodes.get.add(sqlNode);
}

// 压入栈
stack.push(sqlNode);

return sqlNode;
}

/**
* 退出SQL调用
*/
public void exit(SqlNode sqlNode, int affectedRows, String errorMessage) {
if (!traceEnabled) {
return;
}

// 设置结束时间和结果
sqlNode.setEndTime(LocalDateTime.now);
sqlNode.setAffectedRows(affectedRows);
sqlNode.setErrorMessage(errorMessage);

// 计算执行时间
long executionTime = Duration.between(sqlNode.getStartTime, sqlNode.getEndTime).toMillis;
sqlNode.setExecutionTime(executionTime);

// 标记慢SQL
if (executionTime > slowSqlThreshold) {
sqlNode.setSlowSql(true);
slowSqlCount.incrementAndGet;
}

// 标记错误SQL
if (errorMessage != ) {
errorSqlCount.incrementAndGet;
}

// 更新统计
totalSqlCount.incrementAndGet;
totalExecutionTime.addAndGet(executionTime);

// 弹出栈
Stack
if (!stack.isEmpty) {
stack.pop;

// 如果栈为空,说明调用树完成,保存到全局会话
if (stack.isEmpty) {
String sessionKey = generateSessionKey;
globalSessions.put(sessionKey, new ArrayList(rootNodes.get));
rootNodes.get.clear;
}
}
}

/**
* 获取当前调用深度
*/
public int getCurrentDepth {
return callStack.get.size;
}

/**
* 获取当前线程的根节点
*/
public List
return new ArrayList(rootNodes.get);
}

/**
* 获取所有会话
*/
public Map
return new HashMap(globalSessions);
}

/**
* 清理会话数据
*/
public void clearSessions {
globalSessions.clear;
rootNodes.get.clear;
callStack.get.clear;
}

/**
* 生成会话键
*/
private String generateSessionKey {
return Thread.currentThread.getName + "_" + System.currentTimeMillis;
}

/**
* 获取统计信息
*/
public SqlStatistics getStatistics {
return SqlStatistics.builder
.totalSqlCount(totalSqlCount.get)
.slowSqlCount(slowSqlCount.get)
.errorSqlCount(errorSqlCount.get)
.averageExecutionTime(totalSqlCount.get > 0 ?
totalExecutionTime.get / totalSqlCount.get : 0)
.build;
}

// Getter和Setter方法
public boolean isTraceEnabled {
return traceEnabled;
}

public void setTraceEnabled(boolean traceEnabled) {
this.traceEnabled = traceEnabled;
}

public long getSlowSqlThreshold {
return slowSqlThreshold;
}

public void setSlowSqlThreshold(long slowSqlThreshold) {
this.slowSqlThreshold = slowSqlThreshold;
}
}

线程安全 :使用ThreadLocal确保多线程环境下的数据隔离

智能建树 :自动识别父子关系,构建完整调用树

实时统计 :同步更新性能统计信息

3. 数据模型:完整的SQL节点信息

 @Data
public class SqlNode {
private String nodeId; // 节点唯一标识
private String sql; // SQL语句
private String formattedSql; // 格式化后的SQL
private String sqlType; // SQL类型
private int depth; // 调用深度
private String threadName; // 线程名称
private String serviceName; // Service类名
private String methodName; // Service方法名
private LocalDateTime startTime; // 开始时间
private LocalDateTime endTime; // 结束时间
private long executionTime; // 执行耗时
private boolean slowSql; // 是否为慢SQL
private int affectedRows; // 影响行数
private String errorMessage; // 错误信息
private List
private List

// 智能分析方法
public boolean isSlowSql(long threshold) {
return executionTime > threshold;
}

public int getTotalNodeCount {
return 1 + children.stream.mapToInt(SqlNode::getTotalNodeCount).sum;
}

public int getMaxDepth {
return children.isEmpty ? depth :
children.stream.mapToInt(SqlNode::getMaxDepth).max.orElse(depth);
}
}

4. RESTful API:完整的数据接口

SqlTreeController 提供完整的REST API接口,支持数据查询、配置管理和系统监控:

 @RestController
@RequestMapping("/api/sql-tree")
public class SqlTreeController {

@Autowired
private SqlCallTreeContext sqlCallTreeContext;

/**
* 获取当前线程的SQL调用树
*/
@GetMapping("/current")
public ResponseEntity
List
return ResponseEntity.ok(rootNodes);
}

/**
* 获取所有会话的SQL调用树
*/
@GetMapping("/sessions")
public ResponseEntity
Map
return ResponseEntity.ok(sessions);
}

/**
* 获取指定会话的SQL调用树
*/
@GetMapping("/session/{sessionKey}")
public ResponseEntity
Map
List
if (sessionTree != ) {
return ResponseEntity.ok(sessionTree);
} else {
return ResponseEntity.notFound.build;
}
}

/**
* 清理所有调用树数据
*/
@DeleteMapping("/clear")
public ResponseEntity
sqlCallTreeContext.clearSessions;
Map
response.put("success", true);
response.put("message", "All SQL trees cleared successfully");
response.put("timestamp", LocalDateTime.now);
return ResponseEntity.ok(response);
}

/**
* 获取统计信息
*/
@GetMapping("/statistics")
public ResponseEntity
SqlStatistics stats = sqlCallTreeContext.getStatistics;
Map
response.put("totalSqlCount", stats.getTotalSqlCount);
response.put("slowSqlCount", stats.getSlowSqlCount);
response.put("errorSqlCount", stats.getErrorSqlCount);
response.put("averageExecutionTime", stats.getAverageExecutionTime);
response.put("slowSqlThreshold", sqlCallTreeContext.getSlowSqlThreshold);
response.put("traceEnabled", sqlCallTreeContext.isTraceEnabled);
return ResponseEntity.ok(response);
}

/**
* 配置追踪参数
*/
@PostMapping("/config")
public ResponseEntity
Map

if (config.containsKey("slowSqlThreshold")) {
long threshold = ((Number) config.get("slowSqlThreshold")).longValue;
sqlCallTreeContext.setSlowSqlThreshold(threshold);
response.put("slowSqlThreshold", threshold);
}

if (config.containsKey("traceEnabled")) {
boolean enabled = (Boolean) config.get("traceEnabled");
sqlCallTreeContext.setTraceEnabled(enabled);
response.put("traceEnabled", enabled);
}

response.put("success", true);
response.put("message", "Configuration updated successfully");
return ResponseEntity.ok(response);
}

/**
* 分析慢SQL
*/
@GetMapping("/analysis/slow-sql")
public ResponseEntity
Map
List

for (List
collectSlowSqlNodes(sessionNodes, slowSqlNodes);
}

// 按执行时间降序排序
slowSqlNodes.sort((a, b) -> Long.compare(b.getExecutionTime, a.getExecutionTime));

return ResponseEntity.ok(slowSqlNodes);
}

/**
* 导出数据
*/
@GetMapping("/export")
public ResponseEntity
Map
exportData.put("sessions", sqlCallTreeContext.getAllSessions);
exportData.put("statistics", sqlCallTreeContext.getStatistics);
exportData.put("exportTime", LocalDateTime.now);
exportData.put("version", "1.0");

return ResponseEntity.ok(exportData);
}

/**
* 系统状态检查
*/
@GetMapping("/health")
public ResponseEntity
Map
health.put("status", "UP");
health.put("traceEnabled", sqlCallTreeContext.isTraceEnabled);
health.put("slowSqlThreshold", sqlCallTreeContext.getSlowSqlThreshold);
health.put("timestamp", LocalDateTime.now);

return ResponseEntity.ok(health);
}

/**
* 递归收集慢SQL节点
*/
private void collectSlowSqlNodes(List
for (SqlNode node : nodes) {
if (node.isSlowSql) {
slowSqlNodes.add(node);
}
if (node.getChildren != && !node.getChildren.isEmpty) {
collectSlowSqlNodes(node.getChildren, slowSqlNodes);
}
}
}
}

5. 前端可视化实现

前端使用D3.js实现交互式的SQL调用树可视化,主要包含以下功能:

 // sql-tree.js - 主要的可视化逻辑
class SqlTreeVisualizer {
constructor {
this.width = 1200;
this.height = 800;
this.margin = { transform: translateY( 50, right: 150, bottom: 50, left: 150 };

// 初始化SVG容器
this.svg = d3.select('#tree-container')
.append('svg')
.attr('width', this.width)
.attr('height', this.height);

this.g = this.svg.append('g')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);

// 配置树布局
this.tree = d3.tree
.size([this.height - this.margin.top - this.margin.bottom,
this.width - this.margin.left - this.margin.right]);

// 初始化工具提示
this.tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
}

/**
* 渲染SQL调用树
*/
render(sessions) {
this.g.selectAll('*').rem)ove;

if (!sessions || Object.keys(sessions).length === 0) {
this.showEmptyState;
return;
}

// 选择第一个会话进行展示
const sessionKey = Object.keys(sessions)[0];
const rootNodes = sessions[sessionKey];

if (rootNodes && rootNodes.length > 0) {
this.renderTree(rootNodes[0]);
}
}

/**
* 渲染单个调用树
*/
renderTree(rootNode) {
// 构建D3层次结构
const root = d3.hierarchy(rootNode, d => d.children);

// 计算节点位置
this.tree(root);

// 绘制连接线
const links = this.g.selectAll('.link')
.data(root.links)
.enter.append('path')
.attr('class', 'link')
.attr('d', d3.linkHorizontal
.x(d => d.y)
.y(d => d.x))
.style('fill', 'none')
.style('stroke', '#94a3b8')
.style('stroke-width', '2px')
.style('stroke-opacity', 0.6);

// 绘制节点组
const nodes = this.g.selectAll('.node')
.data(root.descendants)
.enter.append('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.y},${d.x})`);

// 绘制节点圆圈
nodes.append('circle')
.attr('r', 10)
.style('fill', d => this.getNodeColor(d.data))
.style('stroke', '#1e293b')
.style('stroke-width', '2px')
.style('cursor', 'pointer');

// 添加节点文本
nodes.append('text')
.attr('dy', '.35em')
.attr('x', d => d.children ? -15 : 15)
.style('text-anchor', d => d.children ? 'end' : 'start')
.style('font-size', '12px')
.style('font-weight', '500')
.style('fill', '#1e293b')
.text(d => this.getNodeLabel(d.data));

// 添加交互事件
nodes
.on('mouseover', (event, d) => this.showTooltip(event, d.data))
.on('mouseout', => this.hideTooltip)
.on('click', (event, d) => this.showNodeDetails(d.data));
}

/**
* 获取节点颜色
*/
getNodeColor(data) {
if (data.errorMessage) {
return '#ef4444'; // 错误:红色
}
if (data.slowSql) {
return '#f59e0b'; // 慢SQL:橙色
}
switch (data.sqlType) {
case 'SELECT':
return '#10b981'; // 查询:绿色
case 'INSERT':
return '#3b82f6'; // 插入:蓝色
case 'UPDATE':
return '#8b5cf6'; // 更新:紫色
case 'DELETE':
return '#ef4444'; // 删除:红色
default:
return '#6b7280'; // 默认:灰色
}
}

/**
* 获取节点标签
*/
getNodeLabel(data) {
const time = data.executionTime || 0;
return `${data.sqlType} (${time}ms)`;
}

/**
* 显示工具提示
*/
showTooltip(event, data) {
const tooltipContent = `

${data.sqlType} 操作



执行时间: ${data.executionTime || 0}ms


影响行数: ${data.affectedRows || 0}


服务: ${data.serviceName || 'Unknown'}


方法: ${data.methodName || 'unknown'}


${data.errorMessage ? `

错误: ${data.errorMessage}

` : ''}
© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...