0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何一眼定位SQL的代码来源:一款SQL染色标记的简易MyBatis插件

京东云 来源:京东物流 郭忠强 作者:京东物流 郭忠强 2025-03-05 11:36 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

作者:京东物流 郭忠强

导语

本文分析了后端研发和运维在日常工作中所面临的线上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分析治理时,往往会存在类似的痛点。



wKgZO2fHxriABriOAAiJsP2UtWA344.png





思路

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

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

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

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

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



方案



wKgZPGfHxrmAJQosAAD8bcarvFs140.png





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

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

开整

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

wKgZPGfHxrqAOXvFAADZ89DzPWg881.png





SQL的修改核心代码片段:



wKgZO2fHxruAPNtvAAFsbFwDeAY513.png





wKgZPGfHxryAUy9vAAE4DNgdlNI586.png





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



wKgZO2fHxr2AKZ-ZAANaCTIl8u4655.png





用法示例:

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

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

效果



wKgZPGfHxr-AWLJMAAOPUUr_gZQ520.png





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, m.diff_qty diffQty, m.broken_qty brokenQty, m.status, m.create_time as createTime, m.update_time as updateTime, m.update_user as updateUser, m.create_user as createUser, 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 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' 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-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
==> Parameters: 6_975(String), 10(Integer)
2025-02-11 00: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



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,
	m.diff_qty diffQty,
	m.broken_qty brokenQty,
	m.status,
	m.create_time AS createTime,
	m.update_time AS updateTime,
	m.update_user AS updateUser,
	m.create_user AS createUser,
	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
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' 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注释信息中。



wKgZO2fHxsGAOYhIAAQS66e8DcI904.png





wKgZO2fHxsKAIGYAAAE0wT1T5_k281.png







wKgZPGfHxsSASmiIAARnnIHBXXs777.png





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



wKgZO2fHxsaAbIZpAA0O8mLA0X0102.png





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



wKgZO2fHxsiAaaiCAAYPoq0G5wI559.png







性能影响

wKgZPGfHxsmAJZDbAAIJJCOUjuM568.png



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



支持情况

已支持的场景:

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



SELECT SQL效果:

SELECT
 COUNT(DISTINCT ito.transfer_order_no) AS qty
FROM
 inv_transfer_order AS ito
LEFT JOIN inv_transfer_order_detail itd
 ON
 ito.warehouse_no = itd.warehouse_no
  AND ito.transfer_order_no = itd.transfer_no
  AND itd.deleted = 0
WHERE
 ito.deleted = 0
 AND ito.warehouse_no = ?
 AND ito.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效果:

UPDATE
 inv_transfer_task_detail
 SET
 task_status = ?,
 task_user = ?,
 update_user = ?,
 update_time = now(),
 receive_time = now()
WHERE
 warehouse_no = ?
 AND deleted = 0
 AND order_detail_id IN(?)
 AND task_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效果:

INSERT
INTO
	inv_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效果:

SELECT NOW()
/* [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分析

wKgZO2fHxsuAKu0GAAiTsBs9RgI089.png





会话管理

wKgZPGfHxs2AAUXRAASKXjftv_8950.png





wKgZO2fHxs-ABRjLAAWD9Gd2k6A207.png





PFinder SQL分析

wKgZO2fHxtGAeiNNAAgej1hJHw0465.png





如何接入?

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

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

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

1、引入Maven坐标:

< dependency >
    < groupId >com.jd.sword< /groupId >
    < artifactId >sword-mybatis-plugins< /artifactId >
    < version >1.0.2-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 >


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

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

< !-- SQLMarking Plugin -- >
< plugin interceptor="com.jd.sword.mybatis.plugin.sql.SQLMarkingInterceptor" >
    < !-- 是否开启SQL染色标记插件 -- >
    < property name="enabled" value="true"/ >
< /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的信息。

审核编辑 黄宇

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • SQL
    SQL
    +关注

    关注

    1

    文章

    788

    浏览量

    45547
  • mybatis
    +关注

    关注

    0

    文章

    64

    浏览量

    6986
收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    SQL 通用数据类型

    data type. SQL 开发人员必须在创建 SQL 表时决定表中的每个列将要存储的数据的类型。数据类型是个标签,是便于 SQL 了解每个列期望存储什么类型的数据的指南,它也标
    的头像 发表于 08-18 09:46 161次阅读

    数据库数据恢复—SQL Server数据库被加密如何恢复数据?

    SQL Server数据库故障: SQL Server数据库被加密,无法使用。 数据库MDF、LDF、log日志文件名字被篡改。
    的头像 发表于 06-25 13:54 278次阅读
    数据库数据恢复—<b class='flag-5'>SQL</b> Server数据库被加密如何恢复数据?

    达梦数据库常用管理SQL命令详解

    达梦数据库常用管理SQL命令详解
    的头像 发表于 06-17 15:12 1110次阅读
    达梦数据库常用管理<b class='flag-5'>SQL</b>命令详解

    大促数据库压力激增,如何一眼定位 SQL 执行来源

    语句成为了性能瓶颈。面对这样的困境,本篇文章提出了对 SQL 进行 “染色” 的方法来帮助大家 一眼定位问题 SQL,而无需再在多处逻辑中
    的头像 发表于 06-10 11:32 210次阅读
    大促数据库压力激增,如何<b class='flag-5'>一眼</b><b class='flag-5'>定位</b> <b class='flag-5'>SQL</b> 执行<b class='flag-5'>来源</b>?

    Devart: dbForge Compare Bundle for SQL Server—比较SQL数据库最简单、最准确的方法

      dbForge Compare Bundle For SQL Server:包含两个工具,可帮助您节省用于手动数据库比较的 70% 的时间 dbForge数据比较 帮助检测和分析实时SQL数据库
    的头像 发表于 01-17 11:35 635次阅读

    dbForge Studio For SQL Server:用于有效开发的最佳SQL Server集成开发环境

    管理 单元测试 数据库文档 测试数据生成 数据导出和导入 为什么dbForge Studio For SQL Server是个好的选择 更快编码 编写更清晰的代码,具有上下文感知的代码
    的头像 发表于 01-16 10:36 843次阅读

    Devart::dbForge SQL Complete让生产力上个台阶

    SQL编码助手,适用于SSMS 和VS 该工具提供上下文感知的代码补全,使SQL开发人员和数据库管理员能够更快地编写代码SQL Comp
    的头像 发表于 01-14 11:09 759次阅读
    Devart::dbForge <b class='flag-5'>SQL</b> Complete让生产力上<b class='flag-5'>一</b>个台阶

    创建唯索引的SQL命令和技巧

    在创建唯索引时,以下是SQL命令和技巧,可以帮助优化性能: 使用合适的索引类型:对于需要保证唯性的列,使用UNIQUE索引来避免重复数据的插入。 这可以确保列中的值是唯
    的头像 发表于 01-09 15:21 591次阅读

    通过Skyvia Connect SQL终端节点访问任何数据

    通过 Skyvia Connect SQL 终端节点访问任何数据   通过 Skyvia Connect SQL 终端节点访问任何数据ADO.NET 数据网关 使用 Skyvia Connect
    的头像 发表于 01-02 09:31 438次阅读
    通过Skyvia Connect <b class='flag-5'>SQL</b>终端节点访问任何数据

    浅谈SQL优化小技巧

    作者:京东零售 王军 回顾:MySQL的执行过程回顾 MySQL的执行过程,帮助 介绍 如何进行sql优化。 (1)客户端发送条查询语句到服务器; (2)服务器先查询缓存,如果命中缓存,则立即返回
    的头像 发表于 12-25 09:59 974次阅读

    SQL错误代码及解决方案

    SQL数据库开发和管理中,常见的错误代码及其解决方案可以归纳如下: 、语法错误(Syntax Errors) 错误代码 :无特定代码,但
    的头像 发表于 11-19 10:21 7146次阅读

    常用SQL函数及其用法

    SQL(Structured Query Language)是种用于管理和操作关系数据库的编程语言。SQL 提供了丰富的函数库,用于数据检索、数据更新、数据删除以及数据聚合等操作。以下是
    的头像 发表于 11-19 10:18 1773次阅读

    SQL与NoSQL的区别

    景。 SQL数据库 SQL数据库,也称为关系型数据库管理系统(RDBMS),是种基于关系模型的数据库。它使用表格、行和列来组织数据,并通过SQL语言进行数据的查询和管理。 特点 结构
    的头像 发表于 11-19 10:15 742次阅读

    大数据从业者必知必会的Hive SQL调优技巧

    不尽人意。本文针对Hive SQL的性能优化进行深入研究,提出了系列可行的调优方案,并给出了相应的优化案例和优化前后的SQL代码。通过合理的优化策略和技巧,能够显著提升Hive
    的头像 发表于 09-24 13:30 809次阅读

    数据库数据恢复—SQL Server数据库出现823错误的数据恢复案例

    SQL Server数据库故障: SQL Server附加数据库出现错误823,附加数据库失败。数据库没有备份,无法通过备份恢复数据库。 SQL Server数据库出现823错误的可能原因有:数据库物理页面损坏、数据库物理页
    的头像 发表于 09-20 11:46 779次阅读
    数据库数据恢复—<b class='flag-5'>SQL</b> Server数据库出现823错误的数据恢复案例