不上APM,103行代码搞定慢SQL检测:超100毫秒自动入库

张开发
2026/6/1 5:12:42 15 分钟阅读
不上APM,103行代码搞定慢SQL检测:超100毫秒自动入库
不上APM103行代码搞定慢SQL检测超100毫秒自动入库原创于 2026-04-14 06:40:21 发布·更新于 2026-04-14 10:30:00非科班野生程序员深耕政务信息化20年。从VC到PB再到Java自研框架browise也打磨了十几年。最近整理框架代码发现不少有趣的决策——政务内网环境部署APM麻烦重重于是琢磨出一个轻量级慢SQL检测方案103行核心代码无需任何外部监控组件超100毫秒的慢SQL自动入库运维直接查表就能定位优化今天整理优化后版本分享给有同样需求的同行。最后感谢豆包、智谱、OpenCode决策是我做的代码是我搓的文字是他们协助总结优化的。文章标签#sql #数据库 #java #后端 #慢SQL监控 #MyBatis #政务信息化一、场景痛点政务内网无APM慢SQL难定位做政务系统的同行都懂系统上线后偶尔会收到用户反馈“页面卡”“操作响应慢”但排查起来却一头雾水——到底是SQL执行慢、网络延迟还是前端渲染卡顿常规解决方案是上APM应用性能监控但政务内网环境特殊部署APM需要申请权限、配置网络、协调运维整个流程繁琐且耗时往往小问题拖成大麻烦。基于此我想到一个轻量级方案在自研框架层埋点计时捕捉每一条SQL的执行耗时超过设定阈值默认100毫秒的SQL自动记录到数据库专用表中运维人员定期查询该表就能快速定位需要优化的慢SQL无需复杂部署零侵入业务代码。二、优化后核心代码可直接生产使用原代码存在线程不安全、SQL注入风险、资源泄漏、硬编码等问题优化后保留核心逻辑解决所有隐患兼容MyBatis适配政务系统OLTP场景核心代码103行不含注释如下import org.apache.ibatis.mapping.BoundSql;import org.apache.ibatis.mapping.MappedStatement;import org.apache.ibatis.session.Configuration;import org.apache.ibatis.session.SqlSession;import org.apache.ibatis.session.SqlSessionFactory;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.SQLException;import java.text.SimpleDateFormat;import java.util.Date;import java.util.Objects;/**轻量级慢SQL监控工具核心功能无需APMSQL执行耗时超100毫秒自动入库适配场景政务内网、无APM部署条件的JavaMyBatis项目核心优势线程安全、防SQL注入、独立事务、零业务侵入/Componentpublic class SlowSqlMonitor {/*慢SQL阈值毫秒可根据业务调整避免硬编码*/private static final long SLOW_SQL_THRESHOLD 100;/**线程安全存储SQL执行开始时间解决多线程并发问题*/private static final ThreadLocal START_TIME_LOCAL new ThreadLocal();/**线程安全存储MyBatis Mapper方法名关联SQL来源*/private static final ThreadLocal METHOD_NAME_LOCAL new ThreadLocal();/**线程安全存储操作类型可选用于区分查询/新增/修改/删除*/private static final ThreadLocal OPER_TYPE_LOCAL new ThreadLocal();Resourceprivate SqlSessionFactory sqlSessionFactory;/**监控开始——SQL执行前调用记录基础信息param methodName Mapper方法全限定名如com.xxx.mapper.UserMapper.selectByIdparam operType 操作类型如SELECT/INSERT/UPDATE/DELETE*/public void startMonitor(String methodName, String operType) {// 记录当前时间作为SQL执行开始时间START_TIME_LOCAL.set(System.currentTimeMillis());// 记录Mapper方法名用于后续解析实际执行SQLMETHOD_NAME_LOCAL.set(methodName);// 记录操作类型便于后续统计分析OPER_TYPE_LOCAL.set(operType);}/**监控结束——SQL执行后调用计算耗时并记录慢SQL*/public void endMonitor() {// 异常捕获确保监控逻辑不影响业务流程try {// 获取线程中存储的基础信息避免空指针Long startTime START_TIME_LOCAL.get();String methodName METHOD_NAME_LOCAL.get();String operType OPER_TYPE_LOCAL.get();if (Objects.isNull(startTime) || Objects.isNull(methodName) || Objects.isNull(operType)) {return;}// 计算SQL执行耗时毫秒 long costTime System.currentTimeMillis() - startTime; // 仅记录超过阈值的慢SQL if (costTime SLOW_SQL_THRESHOLD) { // 解析MyBatis实际执行的SQL包含参数绑定后内容 String executeSql getExecuteSql(methodName); // 独立事务保存慢SQL信息不干扰业务事务 saveSlowSql(methodName, executeSql, operType, costTime); }} catch (Exception e) {// 监控异常不抛出避免影响业务正常执行e.printStackTrace();} finally {// 清理ThreadLocal防止内存泄漏关键优化点START_TIME_LOCAL.remove();METHOD_NAME_LOCAL.remove();OPER_TYPE_LOCAL.remove();}}/**解析MyBatis实际执行的SQL语句param methodName Mapper方法全限定名return 实际执行的SQL过滤特殊字符截取长度适配数据库字段*/private String getExecuteSql(String methodName) {try {// 从MyBatis配置中获取MappedStatement解析真实SQLConfiguration config sqlSessionFactory.getConfiguration();MappedStatement mappedStatement config.getMappedStatement(methodName);BoundSql boundSql mappedStatement.getBoundSql(null);String sql boundSql.getSql();// 过滤单双引号避免拼接SQL时出现注入风险优化点 sql sql.replace(\, ).replace(\, ); // 截取4000字符适配Oracle VARCHAR2字段长度限制 return sql.length() 4000 ? sql.substring(0, 4000) : sql;} catch (Exception e) {// 异常时返回提示不影响整体流程return “SQL解析失败” e.getMessage().substring(0, 100);}}/**独立事务保存慢SQL信息到数据库避免污染业务事务param methodName Mapper方法名param sql 实际执行的SQLparam operType 操作类型param costTime 执行耗时毫秒*/private void saveSlowSql(String methodName, String sql, String operType, long costTime) {// try-with-resources自动关闭资源避免资源泄漏关键优化点try (SqlSession sqlSession sqlSessionFactory.openSession(false);Connection connection sqlSession.getConnection()) {// 使用PreparedStatement彻底避免SQL注入核心优化点 String insertSql INSERT INTO TUNINGEVENT(METHODNAME, SQL, OPER, OPERDATE, ELAPSED) VALUES(?, ?, ?, ?, ?); try (PreparedStatement preparedStatement connection.prepareStatement(insertSql)) { // 绑定参数规范SQL执行 preparedStatement.setString(1, methodName); preparedStatement.setString(2, sql); preparedStatement.setString(3, operType); preparedStatement.setString(4, new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(new Date())); preparedStatement.setLong(5, costTime); // 执行插入 preparedStatement.executeUpdate(); // 手动提交事务独立事务不依赖业务事务 connection.commit(); } catch (SQLException e) { // 插入失败回滚不影响业务 connection.rollback(); throw e; }} catch (Exception e) {e.printStackTrace();}}}三、核心设计要点阈值可配置适配不同场景默认阈值设为100毫秒适配政务系统OLTP场景高频、短耗时操作如果是报表查询等耗时场景可直接修改SLOW_SQL_THRESHOLD常量无需修改核心逻辑。相比原代码的硬编码优化后更灵活便于后续维护。线程安全支持高并发原代码使用成员变量存储开始时间、方法名等信息多线程并发时会出现数据混乱。优化后改用ThreadLocal存储每个线程独立存储自己的信息互不干扰同时在endMonitor方法中手动清理ThreadLocal避免内存泄漏适配高并发业务场景。独立事务不干扰业务慢SQL记录采用独立事务通过sqlSessionFactory.openSession(false)开启新的会话手动控制提交和回滚与业务事务完全隔离——即使业务事务回滚慢SQL记录也能正常入库同时避免了原代码中事务递归审计日志触发慢SQL记录的问题。防SQL注入提升安全性原代码采用字符串拼接的方式生成插入SQL存在SQL注入风险。优化后使用PreparedStatement绑定参数彻底杜绝注入问题同时过滤SQL中的单双引号进一步提升安全性适配政务系统对数据安全的高要求。资源安全避免泄漏使用try-with-resources自动关闭SqlSession、Connection、PreparedStatement等资源无需手动关闭避免资源泄漏同时在异常处理中做好兜底确保监控逻辑不会因为资源问题影响业务正常执行。零业务侵入使用简单配合框架AOP使用自定义Monitoring注解标注需要监控的Mapper方法AOP代理会自动在方法执行前后调用startMonitor和endMonitor业务代码无需任何修改完全无感知。记录信息完整便于排查慢SQL信息存入TUNINGEVENT表字段设计清晰便于运维排查和统计具体字段说明如下字段名字段说明备注METHODNAMEMyBatis Mapper方法全限定名快速定位SQL所在位置SQL实际执行的SQL语句过滤引号截取4000字符适配数据库限制OPER操作类型SELECT/INSERT/UPDATE/DELETE便于分类统计OPERDATESQL执行时间格式yyyy-MM-dd HH:mm:ss便于定位时间节点ELAPSEDSQL执行耗时毫秒用于排序快速找到耗时最长的SQLSQL解析优化适配MyBatis通过SqlSessionFactory获取MyBatis配置解析MappedStatement和BoundSql获取参数绑定后的实际执行SQL相比原代码的框架依赖优化后适配主流MyBatis版本通用性更强同时增加异常处理避免SQL解析失败影响整体流程。四、使用方式AOP配置零侵入业务只需两步即可完成部署无需修改业务代码自定义Monitoring注解import java.lang.annotation.*;/**慢SQL监控注解标注在需要监控的Mapper方法上*/Target({ElementType.METHOD})Retention(RetentionPolicy.RUNTIME)Documentedpublic interface Monitoring {// 操作类型默认SELECTString operType() default “SELECT”;}AOP切面配置import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.stereotype.Component;import javax.annotation.Resource;/**慢SQL监控AOP切面自动调用监控方法*/AspectComponentpublic class MonitoringAspect {Resourceprivate SlowSqlMonitor slowSqlMonitor;// 环绕通知拦截所有标注Monitoring注解的方法Around(“annotation(monitoring)”)public Object around(ProceedingJoinPoint joinPoint, Monitoring monitoring) throws Throwable {// 获取Mapper方法全限定名包名类名方法名String methodName joinPoint.getSignature().getDeclaringTypeName() “.” joinPoint.getSignature().getName();// 获取注解上的操作类型String operType monitoring.operType();// 开始监控slowSqlMonitor.startMonitor(methodName, operType);try {// 执行业务方法return joinPoint.proceed();} finally {// 无论业务方法是否异常都结束监控并记录慢SQLslowSqlMonitor.endMonitor();}}}表结构创建Oracle示例创建TUNINGEVENT表用于存储慢SQL信息适配代码中的字段设计CREATE TABLE TUNINGEVENT (ID NUMBER(19) PRIMARY KEY AUTO_INCREMENT, – 主键自增METHODNAME VARCHAR2(255) NOT NULL, – Mapper方法名SQL VARCHAR2(4000) NOT NULL, – 执行的SQL语句OPER VARCHAR2(20) NOT NULL, – 操作类型OPERDATE DATE NOT NULL, – 执行时间ELAPSED NUMBER(10) NOT NULL – 执行耗时毫秒);– 索引优化提升查询效率根据ELAPSED排序查询CREATE INDEX IDX_TUNINGEVENT_ELAPSED ON TUNINGEVENT(ELAPSED DESC);五、实际效果与小结这个优化版的慢SQL监控工具核心代码仅103行无需引入任何外部监控组件部署简单、零业务侵入完美适配政务内网环境。我在自己负责的政务系统中上线后第一次查询TUNINGEVENT表就发现了一个跑了8秒的报表查询SQL——由于未加索引导致全表扫描添加索引后执行耗时直接降到200毫秒以内用户反馈的“页面卡”问题彻底解决。相比APM的复杂部署这个轻量级方案更适合中小项目、内网项目运维人员只需定期执行如下查询语句就能快速定位需要优化的慢SQL– 查询耗时前10的慢SQL按耗时降序排列SELECT * FROM TUNINGEVENT ORDER BY ELAPSED DESC LIMIT 10;小结一下核心优势轻量无依赖无需APM103行核心代码部署简单安全可靠线程安全、防SQL注入、资源自动释放零侵入AOP注解不修改业务代码易维护阈值可配置、记录信息完整、排查便捷轻量级SQL监控大家都是怎么做的有没有更好的优化思路欢迎评论区交流探讨~标签#Java #慢SQL #性能监控 #MyBatis #政务信息化 #自研框架 #轻量级监控

更多文章