起因
前几天看到一篇关于SQL监控的文章,功能挺实用:能追踪API到SQL的调用链,能看到完整的可执行SQL,还能检测N+1查询。我觉得不错,就点了个赞。
结果今天再去看,发现代码库没了。
这让我想起一个问题:开源到底意味着什么?
这些年学习和成长的过程中,我一直在学习各种开源项目的代码,从Spring Boot到MyBatis,从Dubbo到Seata。每次看到优秀的开源项目,都有一种"原来可以这样做"的豁然开朗。更重要的是,开源让我们能够和同行一起交流、一起进步。
所以我一直乐于分享自己的技术积累,最近的JobFlow任务调度项目也是这个想法的延续。帮助别人的过程中,也在充实自己。
既然别人的库没了,那我就自己实现一个,完全开源。
这就是SQLInsight项目的由来。
技术方案
在动手之前,我梳理了几种常见的实现思路。
为什么不用这些方案
方案一:注解 + AOP
这是最常见的想法:
java
@SqlMonitor
public class OrderService {
// ...
}
不采用的原因:
- 需要在每个类/方法上加注解,侵入性强
- 开发者容易忘记加
- 维护成本高
方案二:JavaAgent字节码增强
这个方案技术上很强大,但现在的应用已经有太多Agent了:Jacoco做代码覆盖率、SkyWalking做链路追踪、Arthas做诊断。再加一个Agent,启动参数越来越长,维护越来越复杂。不想给大家增加负担。
方案三:IDEA插件
IDEA插件方案有几个问题:
- IDEA本来就慢,装的插件越多,越容易卡顿
- 现在AI插件已经很多了,IDE已经够拥挤
- 只能开发阶段用,生产环境出了问题无法排查
这个思路不适合生产环境的监控工具。
采用的方案
经过思考,我决定采用:动态代理 + StackTrace
- 动态代理:在最底层拦截
思路很简单:不管你用JPA、MyBatis、Hibernate还是JdbcTemplate,最终都要通过JDBC的Connection执行SQL。
所以我们直接在最底层做代理:
markdown
应用启动
↓
自动拦截DataSource创建
↓
动态代理:DataSource → Connection → PreparedStatement
↓
SQL执行时,自动记录
这个思路参考了Seata的实现。Seata也是通过动态代理DataSource来实现分布式事务的。
好处:
- 一行依赖即可接入
- 不需要改业务代码
- 支持所有ORM框架
- 支持所有DataSource(Druid/HikariCP/C3P0)
- StackTrace:自动追踪调用链
传统的API追踪需要在Controller层加AOP,但我们用StackTrace来实现:
java
// SQL执行时
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
// 向上查找,找到Controller
for (StackTraceElement element : stack) {
Class<?> clazz = Class.forName(element.getClassName());
if (clazz.isAnnotationPresent(RestController.class)) {
// 找到了,解析API路径
return parseApiInfo(clazz, element.getMethodName());
}
}
这样就能自动知道:
scss
GET /api/orders/123
↓
OrderController.getOrder()
↓
OrderService.getById()
↓
OrderMapper.selectById()
↓
SELECT * FROM orders WHERE id = 123 (15ms)
完整的调用链,自动构建,不需要写任何额外代码。
- SQL解析:JSQLParser
SQL解析用JSQLParser 4.9版本,这个库很成熟,PageHelper也在用它。可以用来:
- 识别SQL类型(SELECT/INSERT/UPDATE/DELETE)
- 提取表名,统计访问频率
- SQL格式化
- 接入方式
动态代理 + StackTrace的组合,可以让接入变得非常简单。
开发者只需要:
java
// 1. 添加依赖
<dependency>
<groupId>com.github</groupId>
<artifactId>sqlinsight-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
// 2. 启动类加个注解(如果需要的话)
@SpringBootApplication
@EnableSqlInsight // 这一行
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
如果采用Spring Boot自动配置,甚至连注解都可以省略。
关于监控系统集成
很多人可能会问:为什么不直接集成Prometheus?
我的想法是:建议业务系统自己接入Prometheus,但SQLInsight作为一个工具,不应该渗透到业务系统的监控体系中。
原因:
- 每个公司的监控体系不一样,有的用Prometheus,有的用其他方案
- 作为一个通用工具,不应该强制用户使用某种监控方案
- 保持工具的纯粹性和简洁性
不过,SQLInsight会提供标准的输出接口:
- 日志输出(可以对接ELK)
- 数据库持久化(可以自己做可视化)
- 事件回调(可以自己集成任何监控系统)
用户可以根据自己的需求,灵活接入自己的监控体系。
数据持久化
SQLInsight支持可选的持久化功能:
yaml
sqlinsight:
persistence:
enabled: true
type: mysql # 或者 postgresql
持久化内容:
- API路径和方法
- SQL语句和参数
- 执行时间
- 调用链路
- 时间戳
持久化的好处:
- 可以做历史查询
- 可以做趋势分析
- 可以做慢SQL统计
- 后续可以做Web页面查看
核心功能
1. API到SQL的完整追踪
sql
输入:/api/orders/123
输出:
└─ OrderController.getOrder()
└─ OrderService.getById()
└─ SQL: SELECT * FROM orders WHERE id = 123 (15ms)
2. SQL执行统计
每个API执行了多少条SQL:
sql
GET /api/orders/123
├─ SELECT * FROM orders WHERE id = ? (15ms)
├─ SELECT * FROM order_items WHERE order_id = ? (8ms)
└─ SELECT * FROM users WHERE id = ? (5ms)
总计: 3条SQL,28ms
3. SQL执行时间
sql
慢SQL检测:
[警告] SELECT * FROM orders WHERE status = 'PAID' ORDER BY created_at DESC
执行时间: 3500ms
API: GET /api/orders/list
建议: 添加索引 (status, created_at)
4. 完整的可执行SQL
不再是带问号的SQL,而是可以直接复制到数据库执行的完整SQL:
sql
原始: SELECT * FROM orders WHERE id = ? AND user_id = ?
完整: SELECT * FROM orders WHERE id = 123 AND user_id = 456
这对于排查问题非常有用,可以直接拿到数据库执行,看看结果是否符合预期。
5. N+1查询检测
自动检测循环查询问题:
sql
[警告] 检测到N+1查询
API: GET /api/orders/list
SQL数量: 23条SELECT
建议: 使用JOIN或批量查询优化
开发计划
第一阶段:核心功能
- 动态代理DataSource
- 动态代理Connection
- 动态代理PreparedStatement
- StackTrace解析
- API信息提取
- SQL参数绑定
- 控制台输出
- 日志输出
第二阶段:增强功能
- 数据库持久化
- 慢SQL检测
- N+1查询检测
- 性能优化(缓存)
- 配置项支持
性能考虑
很多人会担心:这样监控,会不会影响性能?
影响可以忽略不计。
性能优化策略:
- 缓存Controller信息
java
// 解析一次后缓存
Map<String, ControllerInfo> cache = new ConcurrentHashMap<>();
首次解析:约50微秒 缓存命中:<1微秒
- 异步记录
java
// SQL信息异步写入
executor.submit(() -> {
persistence.save(sqlExecution);
});
- 限制栈深度
java
// 只扫描前50个栈帧
for (int i = 0; i < Math.min(stack.length, 50); i++) {
// ...
}
预期性能影响:微秒级,可忽略。
使用示例
控制台输出
bash
2026-01-05 10:30:45.123 [SQL] GET /api/orders/123
└─ OrderController.getOrder()
└─ SELECT * FROM orders WHERE id = 123 (15ms)
2026-01-05 10:30:45.145 [SQL] GET /api/orders/123
└─ OrderController.getOrder()
└─ SELECT * FROM order_items WHERE order_id = 123 (8ms)
2026-01-05 10:30:45.850 [SLOW-SQL] GET /api/reports/summary
└─ ReportController.getSummary()
└─ SELECT COUNT(*), SUM(amount) FROM orders
WHERE created_at > '2025-01-01' GROUP BY user_id (3500ms)
日志输出(JSON格式,ELK友好)
json
{
"timestamp": "2026-01-05T10:30:45.123Z",
"api": {
"path": "/api/orders/123",
"method": "GET",
"controller": "OrderController.getOrder()"
},
"sql": {
"statement": "SELECT * FROM orders WHERE id = ?",
"parameters": [123],
"executableSql": "SELECT * FROM orders WHERE id = 123",
"duration": 15,
"type": "SELECT"
},
"callStack": "OrderController.getOrder() → OrderService.getById() → OrderMapper.selectById()"
}
Web页面查询(计划中)
vbnet
┌─────────────────────────────────────────────────────┐
│ SQL监控查询 │
├─────────────────────────────────────────────────────┤
│ 查询条件: │
│ API路径: /api/orders/* │
│ 时间范围: 最近1小时 │
│ 执行时间: > 1000ms │
│ [查询] │
├─────────────────────────────────────────────────────┤
│ 结果: │
│ │
│ GET /api/orders/list 3500ms │
│ SELECT * FROM orders WHERE status = 'PAID'... │
│ 执行时间: 3500ms │
│ 调用链: OrderController → OrderService │
│ │
│ GET /api/orders/export 2800ms │
│ SELECT * FROM orders WHERE... │
│ 执行时间: 2800ms │
│ 调用链: OrderController → ExportService │
└─────────────────────────────────────────────────────┘
写在最后
做SQLInsight这个项目,不是为了跟谁竞争,也不是为了证明什么。只是觉得技术应该开放共享,而不是闭门造车。
如果这个工具能帮助到一些同行,让大家少踩一些坑,少加一些班,那就值了。
项目采用MIT协议,可以自由使用、修改、商用和分发。