核心概念:故事 · 现场 · 追踪
这三个概念是 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 最大的护城河——
不是看日志,是看代码运行的回放。
不是一条条孤立的调用记录,是一个完整的、可以回放的、有上下文的执行故事。