Trace Sql:打通全链路日志最后一里路
背景介绍
曾经,我遭遇了一个荒诞的 Bug。那是一个看似平凡的字段,由我和同事共同维护着,如同两个陌生人在同一片土地上耕作。最终,我们发现这个字段背叛了现实,它所记录的与真实情况南辕北辙。
生产环境是沉默的,它从不打印 SQL 语句,就像西西弗斯推石的山坡,永远不会告诉你石头为何滚落。我们各自负责的模块如同两座孤岛,彼此的业务逻辑互不相通,因此很难判断问题的根源在哪里。
后来,我借助阿里云的 SQL 洞察,如同考古学家挖掘古迹一般,获取到了那条记录的更新历史,这才为我洗刷了冤屈。原来是同事后续的操作将字段更新错误了。幸运的是,两个操作之间存在时间差,这成了唯一的线索,让真相得以浮出水面。
那一刻我想,如果 SQL 语句也能携带 TraceId,就像每个人都有自己的身份证一样,那么回溯问题将变得简单而确凿,铁证如山。这就是我们今天 #技术分享要探讨的——在数据库的荒原中,为每一条 SQL 语句刻上它的身份标记。
在微服务的迷宫中,全链路追踪本应是我们的阿里阿德涅之线。我们在应用层、网关层、服务间调用等环节都小心翼翼地添加着 TraceId,却往往忽略了数据库这个沉默的见证者。为了实现真正意义上的全链路追踪,我们必须将 TraceId 的印记延伸到 SQL 语句中,让每一次数据库的低语都能被准确地追溯到它的源头。
解决思路
恒等条件
笔者最开始想到的办法是使用 WHERE 子句 ,比如查询的时候我在 WHERE 的最后加一个恒等表达式,形如:#{traceId} = #{traceId}
SELECT
id, age, name
FROM
tbl_user
WHERE
id = 1
AND
'004b0307-2d71-466e-aedf-cb8009893881' = '004b0307-2d71-466e-aedf-cb8009893881';
这样我们就可以把链路中的 TraceId 带入 SQL,可是这么搞局限性非常大:
- SELECT UPDATE DELET都可以带WHERE字句,但是INSERT不可以
- 需要处理的case复杂,比如子句前需不需要AND,比如有的SQL是ORDER或者LIMIT子句结尾,那你还要找到WHERE子句的位置然后添加恒等式,等等
- 即使各种case全部都能覆盖,但是为了加上trace浪费挺多资源
综上所述,笔者最终放弃了这个方案。
SQL SQL注释
我们必须要找到一种足够简单,不需要应对各种复杂 case,代码好写好维护的方案。于是我想到可以把 SQL 都带上注释,然后注释里带上 Trace 信息。
SELECT
id, age, name
FROM
tbl_user
WHERE
id = 1;
这个方案有很多好处:
- 支持所有类型的SQL
- 足够简单,不需要分析原SQL,仅仅只需要给原来的语句头部加上注释信息
- 代码好写,基本上也没啥资源消耗
具体实现
使用 MyBatis 的 Interceptor 拦截 StatementHandler ,修改 SQL。MyBatis 允许开发者通过实现
org.apache.ibatis.plugin.Interceptor 接口,拦截四大对象之一:
- Executor
- ParameterHandler
- ResultSetHandler
- StatementHandler ← 我们重点要用这个!
我们要拦截的是: StatementHandler.prepare() 方法 ,在这个方法执行之前或之后,可以拿到 即将发送到数据库的原始 SQL ,然后我们 在 SQL 的头部或尾部拼接上 /* traceId = xxx */ 这种注释 (笔者最终选择头部,因为一眼就能看到)。
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import java.lang.reflect.Field; import java.sql.Connection;
@Intercepts({ @Signature( type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class} ) }) public class TraceSqlInterceptor implements Interceptor {
private static final String TRACE_NAME = "X-Id:";
private static final String SQL = "sql";
@Override public Object intercept(Invocation invocation) throws Throwable { Object target = invocation.getTarget(); if (target instanceof StatementHandler) { String sqlMark = buildMark(); if (StringUtils.isNotBlank(sqlMark)) { StatementHandler stat = (StatementHandler) target; BoundSql boundSql = stat.getBoundSql(); String sql = boundSql.getSql(); setField(boundSql, sqlMark + sql); } } return invocation.proceed(); }
private void setField(BoundSql boundSql, String newSql) { try { Field field = BoundSql.class.getDeclaredField(SQL); field.setAccessible(true); field.set(boundSql, newSql); } catch (Exception e) { } }
private static String buildMark() String traceId = TraceContext.getTraceId(); if (StringUtils.isBlank(traceId)) { return StringUtils.EMPTY; }
return StringPool.SLASH + StringPool.ASTERISK + StringPool.SPACE + TRACE_NAME + traceId + StringPool.SPACE + StringPool.ASTERISK + StringPool.SLASH; }
}
同时不要忘了往 Spring 容器中注入拦截器实例
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false) public class TraceSqlConfiguration {
@Bean public TraceSqlInterceptor traceSqlInterceptor() { return new TraceSqlInterceptor(); }
}
看看成果
可以看到阿里云 ARMS 采集到的 SQL 已经全部带上了业务系统的 TraceId,目的达成......
--- 后记 有朋友反馈我的文字功底不行,因此开头的背景介绍是用 AI 改写的,有那味儿了......
相关文章
- MyBatis如何实现分页查询?_mybatis collection分页查询
- 通过Mybatis Plus实现代码生成器,常见接口实现讲解
- MyBatis-Plus 日常使用指南_mybatis-plus用法
- 聊聊:Mybatis-Plus 新增获取自增列id,这一次帮你总结好
- MyBatis-Plus码之重器 lambda 表达式使用指南,开发效率瞬间提升80%
- Spring Boot整合MybatisPlus和Druid
- mybatis 代码生成插件free-idea-mybatis、mybatisX
- mybatis-plus 团队新作 mybatis-mate 轻松搞定企业级数据处理
- Maven 依赖范围(scope) 和 可选依赖(optional)
- Trace Sql:打通全链路日志最后一里路