如何一眼定位SQL的代码来源:一款SQL染色标记的简易MyBatis插件
导语
本文分析了后端研发和运维在日常工作中所面临的线上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为例:
SELECT
COUNT( *)
FROM
st_stock m
INNER JOIN st_lot_shelf_life slsl
ON
m.tenant_code = slsl.tenant_code
AND m.sku = slsl.sku
AND m.lot_no = slsl.lot_no
AND slsl.deleted = 0
WHERE
m.deleted = 0
AND m.stock_qty > 0
AND m.warehouse_no = ?
AND m.lot_no != '-1'
AND m.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-11 00: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: SELECT m.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,