如何一眼定位SQL的代码来源:一款SQL染色标记的简易MyBatis插件
文章介绍了一款基于MyBatis的SQL染色标记插件,通过在SQL中添加注释标记(如statementId、pFinderId等),帮助快速定位和排查线上SQL问题。该插件支持SELECT、INSERT、UPDATE、DELETE等多种语句,并允许用户自定义附加信息,适用于分布式跟踪和慢SQL分析等场景。 2025-7-2 05:59:26 Author: www.freebuf.com(查看原文) 阅读量:17 收藏

导语

本文分析了后端研发和运维在日常工作中所面临的线上SQL定位排查痛点,基于姓名贴的灵感,设计和开发了一款SQL染色标记的MyBatis插件。该插件轻量高效,对业务代码无侵入,接入简单,支持SELECT、INSERT、UPDATE、DELETE等语句,同时也支持无WHERE条件SQL的标记增强。该SQL染色插件并不改变SQL指纹,染色信息内置了statementId、PFinderId,方便分布式跟踪和定位。此外,还提供了附加信息的传递入口,方便用户进行自定义信息染色,例如客户端的执行线程id等。期望在大家面临类似痛点时提供一些实践经验和参考,也欢迎大家合适的场景下接入使用。

痛点

作为后端开发,不可避免地与SQL打交道,一个大型复杂系统中往往会有大量的SQL语句支撑业务,而且单表所涉及的不同SQL可能也多达几十个甚至上百个。

当看到一个SQL时,如何快速识别这个SQL是哪块业务的?具体是哪个方法走到了这个SQL?

这些SQL是凭个人大脑无法全部记住的,而且业务在不断发展,SQL语句本身也在不断地变化,可能明天增多一个表的join,后天增多了几个where条件限制,大后天减少了几个字段……

SQL本身也是支持动态拼接形成,当看到一个SQL时,如何快速定位是来自哪块具体业务?这是个问题,也是个难题。

以下面的报表查询SQL为例:

SELECTCOUNT(*)FROMst_stock m
INNERJOINst_lot_shelf_life slsl
ONm.tenant_code =slsl.tenant_code
	ANDm.sku =slsl.sku
	ANDm.lot_no =slsl.lot_no
	ANDslsl.deleted =0WHEREm.deleted =0ANDm.stock_qty >0ANDm.warehouse_no =?
	ANDm.lot_no !='-1'ANDm.owner_no IN(?)

我经常会面临这种根据SQL定位分析业务来源的问题,尤其是在慢SQL分析治理时,往往会存在类似的痛点。

思路

我们日常看到一些工作人员的制服上会配备姓名贴,这样很有辨识度,通过姓名贴我们可以一看就可以看出来当前的工作人员是哪位同事。

在此启发下,我认为对SQL也可以进行一些染色标记增强,通过这些标记可以一眼看出来这个SQL是哪些业务产生的。

我这里考虑采用MyBatis Plugin机制进行SQL染色增强,可以达到业务零侵入的效果:不改业务代码、不改业务SQL,做到SQL无感增强,自动染色。

用什么来区分SQL的唯一性呢?这个区分的标识区分度越高,越容易达到“一眼就看出来SQL来源”的效果。

对此,我采用SQL statement的id来作为唯一标识。SQL statement是有两部分组成:mapper namespace + SQL id,通过SQL statement的id基本上可以唯一确定程序中的SQL在mapper文件中的位置,顺便可以找到对应的DAO方法,及其追溯到上层调用来源和业务场景。

方案

SQL染色增强,这里是通过将附加信息作为SQL注释,对SQL拼接改写。

因为增加的部分是SQL注释,不影响SQL的执行正确性,也不会改写SQL指纹,对于慢SQL排查定位、死锁日志SQL排查都有帮助。

开整

这里是对SQL执行前进行染色增强,所以拦截StatementHandler的StatementHandler方法即可。

SQL的修改核心代码片段:

插件除了会自动拼接statementId和pFinderId外,还预留了一个ThreadLocal变量,允许使用者执行线程的上线文中向SQL传递附加信息,比如SQL的执行用户ERP、执行线程的id等。

用法示例:

// 其他代码SQLMarkingThreadLocal.put("operator",UserInfoUtil.getUserCode());// 其他代码SQLMarkingThreadLocal.remove();// 其他代码

用户也可以通过自定义切面方式自动赋值这些附加信息。

效果

2025-02-1100:27:19.982[http-nio-8082-exec-7]DEBUG [pfinderId:4630283.56667.17392048399060130]org.apache.ibatis.logging.jdbc.BaseJdbcLogger-debug:137-c.j.w.s.i.j.r.d.S.selectStockShelfLifeReport
==>Preparing: SELECTm.id,m.sku,m.location_no locationNo,m.container_level_1 containerLevel1,m.container_level_2 containerLevel2,m.lot_no lotNo,m.sku_level skuLevel,m.owner_no ownerNo,m.pack_code packCode,m.stock_qty stockQty,m.prepicked_qty prePickedQty,m.premoved_qty preMovedQty,m.frozen_qty frozenQty,m.diff_qty diffQty,m.broken_qty brokenQty,m.status,m.create_time ascreateTime,m.update_time asupdateTime,m.update_user asupdateUser,m.create_user ascreateUser,stock_qty -(prepicked_qty +premoved_qty +frozen_qty +diff_qty +broken_qty)availableQty,(prepicked_qty +premoved_qty +frozen_qty +diff_qty +broken_qty)noAvailableQty,m.zone_no zoneNo,m.zone_type zoneType,slsl.shelf_life_status shelfLifeStatus,slsl.left_days leftDays,slsl.production_date productionDate,slsl.expiration_date expirationDate,slsl.shelf_life_days shelfLifeDays,slsl.warning_days warningDays,slsl.regular_advent_days regularAdventDays,slsl.urgent_advent_days urgentAdventDays,slsl.advent_days adventDays,slsl.extend_content extendContent FROMst_stock m INNERJOINst_lot_shelf_life slsl ONm.tenant_code =slsl.tenant_code ANDm.sku =slsl.sku ANDm.lot_no =slsl.lot_no ANDslsl.deleted =0WHEREm.deleted =0ANDm.stock_qty >0ANDm.warehouse_no =? ANDm.lot_no !='-1'LIMIT? /* [SQLMarking] statementId: com.jdwl.wms.stock.infrastructure.jdbc.report.dao.StockShelfLifeReportDao.selectStockShelfLifeReport, pFinderId: 4630283.56667.17392048399060130, operator: guozhongqiang5, traceId: 59f48d4d-5346-4ffe-9837-693a090090fc */2025-02-1100:27:19.982[http-nio-8082-exec-7]DEBUG [pfinderId:4630283.56667.17392048399060130]org.apache.ibatis.logging.jdbc.BaseJdbcLogger-debug:137-c.j.w.s.i.j.r.d.S.selectStockShelfLifeReport
==>Parameters: 6_975(String),10(Integer)2025-02-1100:27:19.988[http-nio-8082-exec-7]DEBUG [pfinderId:4630283.56667.17392048399060130]org.apache.ibatis.logging.jdbc.BaseJdbcLogger-debug:137-c.j.w.s.i.j.r.d.S.selectStockShelfLifeReport
<==Total: 10
SELECTm.id,m.sku,m.location_no locationNo,m.container_level_1 containerLevel1,m.container_level_2 containerLevel2,m.lot_no lotNo,m.sku_level skuLevel,m.owner_no ownerNo,m.pack_code packCode,m.stock_qty stockQty,m.prepicked_qty prePickedQty,m.premoved_qty preMovedQty,m.frozen_qty frozenQty,m.diff_qty diffQty,m.broken_qty brokenQty,m.status,m.create_time AScreateTime,m.update_time ASupdateTime,m.update_user ASupdateUser,m.create_user AScreateUser,stock_qty -(prepicked_qty +premoved_qty +frozen_qty +diff_qty +broken_qty)availableQty,(prepicked_qty +premoved_qty +frozen_qty +diff_qty +broken_qty)noAvailableQty,m.zone_no zoneNo,m.zone_type zoneType,slsl.shelf_life_status shelfLifeStatus,slsl.left_days leftDays,slsl.production_date productionDate,slsl.expiration_date expirationDate,slsl.shelf_life_days shelfLifeDays,slsl.warning_days warningDays,slsl.regular_advent_days regularAdventDays,slsl.urgent_advent_days urgentAdventDays,slsl.advent_days adventDays,slsl.extend_content extendContent
FROMst_stock m
INNERJOINst_lot_shelf_life slsl
ONm.tenant_code =slsl.tenant_code
	ANDm.sku =slsl.sku
	ANDm.lot_no =slsl.lot_no
	ANDslsl.deleted =0WHEREm.deleted =0ANDm.stock_qty >0ANDm.warehouse_no =?
	ANDm.lot_no !='-1'LIMIT?
	/* [SQLMarking] statementId: com.jdwl.wms.stock.infrastructure.jdbc.report.dao.StockShelfLifeReportDao.selectStockShelfLifeReport, pFinderId: 4630283.56667.17392048399060130, operator: xxx, traceId: 59f48d4d-5346-4ffe-9837-693a090090fc */

通过这个染色标记后的SQL我们可以一眼看出来,这个SQL是来自StockShelfLifeReportDao中的selectStockShelfLifeReport方法,其中StockShelfLifeReportDao对应于mapper文件中的namespace,selectStockShelfLifeReport 对应于 SQL id。

除了statementId和pFinderId外,还允许用户在线程上下文中自定义传输一些附加信息到SQL中,并体现在SQL注释信息中。

借助IDE的reference功能,我们可以很快找到调用入口:

继续向上追溯,流量源头是来自一张报表查询:

性能影响

既然是代理增强,多少会有一些性能开销,目前根据我这边使用的情况来看,单个SQL基本上是0-1ms左右,个别在3-4ms,正常情况下,不会影响业务响应时长。

支持情况

已支持的场景:

•使用MyBatis的SQL,包含select、insert、update、delete,同时也支持无where条件的SQL。支持MyBatis-Plus。

SELECT SQL效果:

SELECTCOUNT(DISTINCTito.transfer_order_no)ASqty
FROMinv_transfer_order ASito
LEFTJOINinv_transfer_order_detail itd
 ONito.warehouse_no =itd.warehouse_no
  ANDito.transfer_order_no =itd.transfer_no
  ANDitd.deleted =0WHEREito.deleted =0ANDito.warehouse_no =?
 ANDito.transfer_status IN(?,?,?,?,?,?,?,?)/* [SQLMarking] statementId: com.jdwl.wms.inventory.xxx.infrastructure.jdbc.dao.TransferOrderDao.selectOverstockOrderQty, pFinderId: 4900300.56689.17397685906403801, traceId: abc53cd3-e814-451e-a771-5d8caae861a7, operator: xxx */

UPDATE SQL效果:

UPDATEinv_transfer_task_detail
 SETtask_status =?,task_user =?,update_user =?,update_time =now(),receive_time =now()WHEREwarehouse_no =?
 ANDdeleted =0ANDorder_detail_id IN(?)ANDtask_status IN(?,?,?)/* [SQLMarking] statementId: com.jdwl.wms.inventory.xxx.infrastructure.jdbc.dao.TransferTaskDetailDao.updateStatusAndTaskUserByOrderDetailAndStatus, pFinderId: 4900300.56689.17397685881342999, traceId: 41366c16-2e10-4c45-a10c-c84326e201b4, operator: xxx */

INSERT SQL效果:

INSERTINTOinv_transfer_task_result
	(id,result_no,transfer_type,task_type,location_no,container_level_1,container_level_2,container_full,extend_content,warehouse_no,create_user,create_time,update_user,update_time,task_no,tenant_code
	)VALUES(?,?,?,?,?,?,?,?,?,?,?,now(),?,now(),?,'TC26473419')/* [SQLMarking] statementId: com.jdwl.wms.inventory.xxx.infrastructure.jdbc.dao.TransferTaskResultDao.insert, pFinderId: 4900300.56689.17397685845562352, traceId: 7cc0eebf-c4c5-4fc1-b5de-ae1f14ba29ba, operator: xxx */

无WHERE条件的SQL效果:

SELECTNOW()/* [SQLMarking] statementId: com.jdwl.wms.stock.xxx.jdbc.main.dao.StockQueryDao.dbTime, pFinderId: 2033056.56579.17392526509236705 */

该插件暂不支持的场景如下:

ORM非MyBatis的SQL,例如通过 connection statement execute 操作的SQL,通过JdbcTemplate 操作的SQL等。

线上SQL的排查定位使用案例

慢SQL分析

会话管理

PFinder SQL分析

如何接入?

如果小伙伴也有类似痛点和使用诉求,可以接入这个简易的SQL染色标记插件。

目前该组件已在多个大型复杂系统的生产环境中接入使用,大家可以先在测试、UAT环境接入试用,然后再逐步推广线上生产环境。

接入方法也非常简单,如下。

注:目前发布的版本是京东内部版本,京东外部伙伴若有引入需要,可联系 [email protected] 发布一个外部版本,或者按照文中的解决方案和思路定制开发一个自己需要的SQL染色插件。 

1、引入Maven坐标:

<!-- http://sd.jd.com/article/42942?shareId=105168&isHideShareButton=1 -->
<dependency>
    <groupId>com.jd.sword</groupId>
    <artifactId>sword-mybatis-plugins</artifactId>
    <version>1.0.7-SNAPSHOT</version>
    <exclusions>
        <exclusion>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.jd.sword</groupId>
    <artifactId>sword-utils-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>


对于其中的间接依赖,例如lombok等,大家可以使用自己工程中的已有依赖,在这里可以通过exclusion排掉,如果自己工程中没有这些依赖,可以不exclusion。

2、在mybatis config xml中引入SQLMarking插件:

<!-- http://sd.jd.com/article/42942?shareId=105168&isHideShareButton=1 -->
<!-- SQLMarking Plugin,放在第一个Plugin的位置,不影响其他组件,但不强要求位置,也可以灵活调整顺序位置 -->
<plugin interceptor="com.jd.sword.mybatis.plugin.sql.SQLMarkingInterceptor">
    <!-- 是否开启SQL染色标记插件 -->
    <property name="enabled" value="true"/>
    <!-- 是否开启方法调用栈跟踪 -->
    <property name="stackTraceEnabled" value="true"/>
    <!-- 指定需要方法调用栈跟踪的package,减少信息量,value配置为自己工程的package路径,多个路径用英文逗号分割 -->
    <property name="specifiedStackTracePackages" value="com.jdwl.wms.stock"/>
    <!-- 结合CPU利用率和性能考虑,方法调用栈跟踪采集率配置采集率,配置示例: m/n,表示n个里面抽m个进行采集跟踪 -->
    <!-- 预发环境和测试环境可以配置全采集,例如配置1/1,生产环境可以结合CPU利用率和性能考虑按需配置采集率 -->
    <property name="stackTraceSamplingRate" value="1/2"/>
</plugin>

FAQ

1、支持 Mybatis-Plus 吗?

答:支持,Mybatis-Plus是在MyBatis基础上的增强,MyBatis插件可以得到执行。

2、SQLMarking Plugin 在 plugins中的位置有严格要求吗,比如必须第一个位置?

答:没有严格要求,理论上放上放下都可以。有的小伙伴工程里依赖了多种 MyBatis Plugin,多种Plugin之间可能会有冲突,比如有些 Plugin 会对SQL的开头INSERT/SELECT/UPDATE/DELETE关键词进行前缀判断,大家如果遇到报错可以灵活调整 SQLMarking Plugin 的位置,向上或向下调整,不一定非得放在第一个位置。

3、报错信息:There is no getter for property named 'delegate' in 'class com.sun.proxy.$Proxy211'

答:这种是多个插件之间有先后顺序依赖,别的插件先行执行,影响了delegate的获取,调整 SQLMarking Plugin 的位置,向上或向下调整,可解决冲突。

4、报错信息关键词:NoClassDefFoundError RoutingStatementHandlerUtils

答:缺少依赖,添加以下依赖:

<dependency>
	<groupId>mybatis-plugins</groupId>
	<artifactId>mybatis-plugins</artifactId>
	<version>2.2.3</version>
</dependency>

5、染色信息中如何添加一些个性化的附加信息?

答:可以用下这个

SQLMarkingThreadLocal.put(key, value)

SQL 执行完 remove 掉。一个方法同时执行多个SQL时,如果 SQLMarkingThreadLocal 可共享,也可以在方法维度上 put 和 remove,就不用每个SQL put remove一下。主要是看线程上下文是否应该传递SQLMarkingThreadLocal的信息。

SQLMarkingThreadLocal.remove()

6、报错信息 java.lang.AbstractMethodError: com.jd.sword.mybatis.plugin.sql.SQLMarkingInterceptor.plugin(Ljava/lang/Object;)Ljava/lang/Object;

答:org.mybatis:mybatis 版本较低。可以将sword-mybatis-plugins 升级至 1.0.5-SNAPSHOT 或之上版本,对低版本 mybatis 做了兼容。

同时需要注意,sword-mybatis-plugins 在 1.0.4-SNAPSHOT 或之上版本时,需要新引入下面的依赖:

<dependency>
    <groupId>com.jd.sword</groupId>
    <artifactId>sword-utils-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

7、sword-mybatis-plugins包down不下来

答:如果是京东科技的小伙伴,请注意检查私服配置,该包deploy在京东公共的私服上,没在京东科技私有的私服上。

8、报错 Caused by: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.statement.StatementHandler named prepare. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.executor.statement.StatementHandler.prepare(java.sql.Connection, java.lang.Integer)

答:mybatis 版本过低,低于 3.4.0,mybatis 3.4.0 之后 StatementHandler 中的 prepare 变成2个参数的方法,不再是只有1个入参的方法,如下图:

解决方法就是将mybatis版本升级至3.4.0之上,例如:

<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>

文章来源: https://www.freebuf.com/news/437452.html
如有侵权请联系:admin#unsafe.sh