导语
本文分析了后端研发和运维在日常工作中所面临的线上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为例:
我经常会面临这种根据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等。

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

通过这个染色标记后的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效果:
UPDATE SQL效果:
INSERT SQL效果:
无WHERE条件的SQL效果:
该插件暂不支持的场景如下:
•ORM非MyBatis的SQL,例如通过 connection statement execute 操作的SQL,通过JdbcTemplate 操作的SQL等。
线上SQL的排查定位使用案例
慢SQL分析

会话管理


PFinder SQL分析

如何接入?
如果小伙伴也有类似痛点和使用诉求,可以接入这个简易的SQL染色标记插件。
目前该组件已在多个大型复杂系统的生产环境中接入使用,大家可以先在测试、UAT环境接入试用,然后再逐步推广线上生产环境。
接入方法也非常简单,如下。
1、引入Maven坐标:
对于其中的间接依赖,例如lombok等,大家可以使用自己工程中的已有依赖,在这里可以通过exclusion排掉,如果自己工程中没有这些依赖,可以不exclusion。
2、在mybatis config xml中引入SQLMarking插件:
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
答:缺少依赖,添加以下依赖:
5、染色信息中如何添加一些个性化的附加信息?
答:可以用下这个
SQL 执行完 remove 掉。一个方法同时执行多个SQL时,如果 SQLMarkingThreadLocal 可共享,也可以在方法维度上 put 和 remove,就不用每个SQL put remove一下。主要是看线程上下文是否应该传递SQLMarkingThreadLocal的信息。
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 或之上版本时,需要新引入下面的依赖:
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之上,例如:
SQL染色标记MyBatis插件助力定位
777

被折叠的 条评论
为什么被折叠?



