跳到主要内容

核心概念:故事 · 现场 · 追踪

这三个概念是 dlz-lens 的灵魂。理解了它们,你就理解了为什么这个工具不只是"另一个 SQL 监控",而是一个全新的调试体验。

三者一览

概念英文一句话解释你可以把它理解成
故事Story一次请求的完整 IO 旅程一部电影、一段录像
现场Scene单次调用的完整上下文一张高清照片、一个犯罪现场
追踪Trace串联故事的线索一根线、一个镜头轨道

故事(Story)

什么是故事?

一次 HTTP 请求 = 一个故事。

从请求进入 Controller,到响应返回给客户端,这中间应用对外做的每一次 IO 调用——查 SQL、写 SQL、查 Redis、调 HTTP 接口、发 MQ 消息——全部按时间顺序排列,形成一条完整的时间线。这就是一个"故事"。

故事包含的信息:

  • 请求元信息:请求方法、URL、开始时间、总耗时、TraceID
  • 调用时间线:所有 IO 调用按执行顺序排列
  • 事务边界:BEGIN / COMMIT / ROLLBACK 在哪里,哪些调用在事务内
  • 统计数据:总调用数、SQL 数、Redis 数、HTTP 数、慢调用数、错误数
  • 诊断标签:这个故事有没有 N+1 问题?有没有事务风险?有没有错误?

故事长什么样

[GET] /api/orders/123 总耗时 245ms 8次调用 1慢 0错
─────────────────────────────────────────────────────────
0ms ┌── BEGIN 事务开始 OrderService.java:42
3ms │ SELECT * FROM orders WHERE id = ? 3ms OrderDao.java:18
7ms │ SELECT * FROM order_items WHERE order_id = ? 4ms ItemDao.java:25
15ms │ ┌── HTTP: GET /api/user/456 120ms UserClient.java:87
135ms │ └── ✅ 200 OK
140ms │ SELECT * FROM user_address WHERE user_id = ? 5ms AddressDao.java:33
158ms │ REDIS: SET order:123:viewed 1 TTL=3600 2ms OrderService.java:78
160ms │ UPDATE orders SET view_count = view_count + 1 WHERE id = ? 3ms
163ms └── COMMIT 事务提交 OrderService.java:42
165ms MQ: send order-viewed topic payload={...} 2ms OrderEvent.java:45

一眼就能看出来:

  • 事务从第 0ms 开始,第 163ms 提交,包含了 5 次调用
  • 中间有一个 HTTP 调用花了 120ms,是最大瓶颈(占总耗时的一半)
  • 事务里居然包含了 HTTP 调用!(这是事务风险)
  • 事务提交后还发了一条 MQ 消息

故事能解决什么问题

故障排查

线上报错了?找到对应请求的故事,一眼看出是哪一步出的问题,前面执行了什么,在不在事务里,参数是什么。不需要猜,不需要加日志重跑。

性能优化

一个接口慢?看故事里的调用时间线——是某条 SQL 特别慢?是 N+1 导致 SQL 太多?是 Redis 慢?还是下游 HTTP 慢?精准定位瓶颈,不瞎优化。

事务分析

事务范围合不合理?事务里有没有不该有的 HTTP 调用?有没有 N+1 导致事务过长?一眼看清。

Code Review

PR 提交前跑一遍,看看有没有意料之外的调用、隐式 N+1、不合理的事务范围。比代码审查靠谱多了。

新人上手

新同事不了解业务?跑几个核心接口的故事,看一遍就懂代码流转路径和数据依赖。


现场(Scene)

什么是现场?

单次 IO 调用的完整上下文 = 一个现场。

一条 SQL 不是孤立存在的,一个 Redis 命令也不是凭空执行的。它是谁调用的?参数是什么?花了多久?成功还是失败?在不在事务里?完整调用栈是什么?这些信息合在一起,就是这次调用的"现场"。

现场包含的信息

类别内容
基本信息调用类型(SQL/Redis/HTTP/MQ)、操作名、开始时间、耗时、状态
调用详情SQL 语句 + 参数 / Redis 命令 + 参数 / HTTP URL + 请求体 + 响应
调用者触发这次调用的业务类名、方法名、行号、文件路径
完整调用栈40 层完整栈帧,每帧都可以点击跳 IDE
上下文TraceID、事务 ID、连接 ID、数据源名称
错误信息异常类型、异常消息、异常堆栈(失败时)
诊断标签慢?N+1?事务风险?错误?

现场 vs 日志

很多人以为"我打了日志就够了"。但日志和现场有本质区别:

日志里的信息dlz-lens 的现场
参数值❌ 通常不打,怕泄露/嫌麻烦✅ 结构化参数列表
精确耗时❌ 日志时间戳精度低,且受缓冲影响✅ 纳秒级包裹调用
调用者❌ 日志行 ≠ 调用行,且通常只打类名✅ 精准业务代码行号
完整调用栈❌ 只有异常才有栈,正常调用没有✅ 每次调用都有 40 层栈
事务上下文❌ 日志里不会打事务状态✅ 在不在事务里、事务ID
TraceID 关联❌ 需要手动打 traceId✅ 自动绑定,开箱即用
IDE 跳转❌ 手动搜类搜方法✅ 点击直达行号

为什么"现场"这个概念很重要

调试的本质是还原现场。你看到一条慢 SQL,想知道的不是"这条 SQL 很慢",而是"谁在什么情况下执行了这条慢 SQL,为什么会执行它"。

现场就是答案——它把调用放回了上下文中。你不仅看到了调用本身,还看到了它的来龙去脉。


追踪(Trace)

什么是追踪?

Trace 是一种机制,用来把同一个请求中的所有 IO 调用串联起来。

它的核心是 TraceID——一个贯穿整个请求生命周期的唯一标识。请求进入时生成,绑定到 ThreadLocal;所有 IO 调用执行时自动带上这个 TraceID;请求结束时解绑。

这样,散落的调用记录就被 TraceID 这根线串成了一个完整的故事。

Trace 的工作原理

HTTP 请求进入

WebTraceFilter
├─ 生成 TraceID
├─ 创建 TraceContext
└─ 绑定到 ThreadLocal

Controller → Service → DAO → RPC → MQ...

每次 IO 调用时:
├─ 从 ThreadLocal 取 TraceContext
├─ 读取 TraceID、事务ID、连接ID
├─ 生成调用序号(seq)
└─ 写入调用记录

更多调用... 都带同一个 TraceID

请求结束
├─ TraceContext 从 ThreadLocal 解绑
└─ 汇总所有调用 → 形成完整 Story

Trace 带来的能力

能力说明
故事回放按 TraceID 聚合,时间线重放完整执行路径
N+1 检测同 Trace 内相同调用模式出现超过阈值,自动标记
事务分析同 Trace 内的事务边界清晰可见
错误定位报错时,不仅知道哪步错了,还知道前面执行了什么
跨类型关联SQL、Redis、HTTP、MQ 都在同一个时间线上
SaaS 扩展TraceID + appName 可以跨应用追踪(需网关透传)

Trace 的传递

目前版本支持:

  • 同线程:ThreadLocal 自动传递
  • 请求内:WebTraceFilter 自动绑定/解绑

规划中:

  • 异步线程:支持线程池场景的 Trace 传递(TTL / TransmittableThreadLocal)
  • 跨服务:HTTP Header 透传 TraceID,支持微服务链路
  • MQ 场景:消息头透传 TraceID,消费端自动恢复

三者关系

Trace(线索)

├─ 串联起所有调用 → 形成 Story(故事)

└─ 每次调用都有完整上下文 → 就是 Scene(现场)

Trace 是线,把散落的珠子(调用)串起来;

现场是每颗珠子的细节,让你看清这颗珠子是什么样子;

故事是串好的整条项链,让你看到完整的全貌和结构。

三者结合,就是 dlz-lens 最大的护城河——

不是看日志,是看代码运行的回放。

不是一条条孤立的调用记录,是一个完整的、可以回放的、有上下文的执行故事。