当前位置: 首页 > news >正文

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

不上APM,103行代码搞定慢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监控工具

  • 核心功能:无需APM,SQL执行耗时超100毫秒自动入库

  • 适配场景:政务内网、无APM部署条件的Java+MyBatis项目

  • 核心优势:线程安全、防SQL注入、独立事务、零业务侵入
    /
    @Component
    public 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<>();

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    /**

    • 监控开始——SQL执行前调用,记录基础信息
    • @param methodName Mapper方法全限定名(如:com.xxx.mapper.UserMapper.selectById)
    • @param operType 操作类型(如:SELECT/INSERT/UPDATE/DELETE)
      */
      public void startMonitor(String methodName, String operType) {
      // 记录当前时间作为SQL执行开始时间
      START_TIME_LOCAL.set(System.currentTimeMillis());
      // 记录Mapper方法名,用于后续解析实际执行SQL
      METHOD_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,解析真实SQL
      Configuration 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 实际执行的SQL

    • @param 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();
      }
      }
      }

三、核心设计要点

  1. 阈值可配置,适配不同场景

默认阈值设为100毫秒,适配政务系统OLTP场景(高频、短耗时操作),如果是报表查询等耗时场景,可直接修改SLOW_SQL_THRESHOLD常量,无需修改核心逻辑。相比原代码的硬编码,优化后更灵活,便于后续维护。

  1. 线程安全,支持高并发

原代码使用成员变量存储开始时间、方法名等信息,多线程并发时会出现数据混乱。优化后改用ThreadLocal存储,每个线程独立存储自己的信息,互不干扰,同时在endMonitor方法中手动清理ThreadLocal,避免内存泄漏,适配高并发业务场景。

  1. 独立事务,不干扰业务

慢SQL记录采用独立事务,通过sqlSessionFactory.openSession(false)开启新的会话,手动控制提交和回滚,与业务事务完全隔离——即使业务事务回滚,慢SQL记录也能正常入库;同时避免了原代码中事务递归(审计日志触发慢SQL记录)的问题。

  1. 防SQL注入,提升安全性

原代码采用字符串拼接的方式生成插入SQL,存在SQL注入风险。优化后使用PreparedStatement绑定参数,彻底杜绝注入问题;同时过滤SQL中的单双引号,进一步提升安全性,适配政务系统对数据安全的高要求。

  1. 资源安全,避免泄漏

使用try-with-resources自动关闭SqlSession、Connection、PreparedStatement等资源,无需手动关闭,避免资源泄漏;同时在异常处理中做好兜底,确保监控逻辑不会因为资源问题影响业务正常执行。

  1. 零业务侵入,使用简单

配合框架AOP使用,自定义@Monitoring注解,标注需要监控的Mapper方法,AOP代理会自动在方法执行前后调用startMonitor和endMonitor,业务代码无需任何修改,完全无感知。

  1. 记录信息完整,便于排查

慢SQL信息存入TUNINGEVENT表,字段设计清晰,便于运维排查和统计,具体字段说明如下:

字段名

字段说明

备注

METHODNAME

MyBatis Mapper方法全限定名

快速定位SQL所在位置

SQL

实际执行的SQL语句

过滤引号,截取4000字符,适配数据库限制

OPER

操作类型

SELECT/INSERT/UPDATE/DELETE,便于分类统计

OPERDATE

SQL执行时间

格式:yyyy-MM-dd HH:mm:ss,便于定位时间节点

ELAPSED

SQL执行耗时(毫秒)

用于排序,快速找到耗时最长的SQL

  1. SQL解析优化,适配MyBatis

通过SqlSessionFactory获取MyBatis配置,解析MappedStatement和BoundSql,获取参数绑定后的实际执行SQL,相比原代码的框架依赖,优化后适配主流MyBatis版本,通用性更强;同时增加异常处理,避免SQL解析失败影响整体流程。

四、使用方式(AOP配置,零侵入业务)

只需两步,即可完成部署,无需修改业务代码:

  1. 自定义@Monitoring注解

import java.lang.annotation.*;

/**

  • 慢SQL监控注解,标注在需要监控的Mapper方法上
    */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Monitoring {
    // 操作类型,默认SELECT
    String operType() default “SELECT”;
    }
  1. 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切面,自动调用监控方法
    */
    @Aspect
    @Component
    public class MonitoringAspect {

    @Resource
    private 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 {
    // 无论业务方法是否异常,都结束监控并记录慢SQL
    slowSqlMonitor.endMonitor();
    }
    }
    }

  1. 表结构创建(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;

小结一下核心优势:

  • 轻量无依赖:无需APM,103行核心代码,部署简单

  • 安全可靠:线程安全、防SQL注入、资源自动释放

  • 零侵入:AOP+注解,不修改业务代码

  • 易维护:阈值可配置、记录信息完整、排查便捷

轻量级SQL监控大家都是怎么做的?有没有更好的优化思路?欢迎评论区交流探讨~

标签:#Java #慢SQL #性能监控 #MyBatis #政务信息化 #自研框架 #轻量级监控

http://www.jsqmd.com/news/638544/

相关文章:

  • 现代化C++开发环境配置:vcpkg、CMake与CLion
  • C语言基础知识点汇总
  • 亲测机电一体化系统维保品牌实践分享
  • 解放双手的智慧:BetterGI原神自动化助手全攻略 [特殊字符]
  • FUTURE POLICE常见问题排查:音频格式不支持、时间轴错位怎么办
  • 2026合肥网站建设公司实测盘点:优质网站制作公司、网站设计公司盘点严选 - 企业推荐官【官方】
  • AI知识库集问答
  • 零基础玩转忍者像素绘卷:手把手教你生成火影风格像素艺术
  • Kandinsky-5.0-I2V-Lite-5s入门必看:PyCharm中调试模型调用代码详解
  • stm32 freertos 学习尚硅谷 第 2 章FreeRTOS基础知识
  • 从数据到视觉:用UGUI RawImage在Unity里做个交互式温度监控面板(支持动态更新)
  • 解决Spring Boot应用启动超慢问题:深入类加载与Bean初始化
  • 【奇点2026权威发布】:AIAgent任务调度必须绕开的7个LLM原生缺陷(附可验证的调度补偿算法伪代码)
  • 西安特产大秦酥饼:百年非遗技艺,一口酥香品长安 - 企业推荐官【官方】
  • Meta:AIRA2系统突破AI科研Agent瓶颈
  • 《机电安装行业数字化转型样板:陕西高信项目管理系统试运行报告》
  • 前端国际化多语言方案
  • K8s StatefulSet 存储卷持久化机制
  • 上海研倍新材料攻克镁合金SLM 3D打印技术难关,轻量化精密构件性能优于铸件 - 企业推荐官【官方】
  • biliTickerBuy:高效智能的B站会员购抢票神器,告别演唱会门票秒杀烦恼
  • 负载箱的选型方法论与系统集成:从需求分析到全生命周期决策
  • Llama-3.2-3B新手入门:用Ollama一键搭建你的本地AI助手
  • 14讲——最短路问题
  • Redis限流算法全解析与实战优化
  • BKIN 完整链路评估
  • 运维系列虚拟化系列OpenStack系列【仅供参考】:将 instance 连接到 vlan100- 每天5分钟玩转 OpenS(95)创建第二个 vlan network “vlan101“
  • 2026年4月AI智能体培训指南:技术实力与口碑俱佳的机构如何选? - 企业推荐官【官方】
  • 2026万商卡线上变现指南:平台操作教程与避坑技巧 - 团团收购物卡回收
  • Cortex-A7 MPCore 架构
  • 用MediaPipe Objectron和Python做个AR小玩具:实时把桌上的杯子“抓”到屏幕里