业务链路追踪日志设计模式 — 从原理到实践

业务链路追踪日志设计模式 --- 从原理到实践

一、什么是业务链路追踪日志

业务链路追踪日志是一种将复杂业务流程中的每个关键步骤以"节点"形式记录下来的技术方案。它不同于普通的 log 日志,核心区别在于:

对比项 普通日志(log.info 链路追踪日志
存储位置 文件/ELK 独立数据库(MySQL)
查询方式 按关键字搜索 按业务单号聚合,按节点排序展示
展示形式 纯文本 结构化(文本+表格+JSON+图标)
使用场景 开发排查 业务人员+开发人员排查
关联性 散落各处 同一业务号串联所有节点

一句话理解:把一个业务流程拆成若干步骤,每个步骤执行完后发一条结构化消息到 MQ,消费端存入数据库,前端页面按业务单号查询并按步骤顺序展示。


二、整体架构

复制代码
┌─────────────────────────────────────────────────────────┐
│                    业务服务(生产者)                      │
│                                                         │
│  业务方法() {                                            │
│    步骤1 → 发送节点1日志                                  │
│    步骤2 → 发送节点2日志                                  │
│    步骤3 → 发送节点3日志                                  │
│    ...                                                  │
│    return → 发送最终结果节点                               │
│  }                                                      │
└────────────────────┬────────────────────────────────────┘
                     │ sendAsync(异步,不阻塞业务)
                     ▼
┌─────────────────────────────────────────────────────────┐
│                    RocketMQ                              │
└────────────────────┬────────────────────────────────────┘
                     │ 消费
                     ▼
┌─────────────────────────────────────────────────────────┐
│              链路追踪服务(消费者)                         │
│                                                         │
│  1. 反序列化消息                                         │
│  2. 从"寻源入参"节点提取查询字段写入主表                    │
│  3. 将节点内容写入明细表                                  │
│  4. 从"返回最终结果"节点提取耗时更新主表                    │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│                    MySQL                                 │
│                                                         │
│  主表:sourcing_trace(业务号、状态、耗时、查询字段...)     │
│  明细表:sourcing_trace_node(节点名、顺序、内容JSON...)   │
└────────────────────┬────────────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────────────┐
│                    前端查询页面                            │
│                                                         │
│  输入业务号 → 查主表 → 查明细表 → 按 nodeOrder 排序展示     │
│  每个节点展示:节点名、状态标签、内容(文本/表格/JSON)       │
└─────────────────────────────────────────────────────────┘

三、核心概念

3.1 业务号(sourcingBizNo)

同一次业务流程的所有节点共用一个业务号,用于聚合查询。类似于分布式追踪中的 traceId。

3.2 节点(Node)

业务流程中的一个关键步骤。每个节点有:

  • nodeName:节点名称(如"入参校验"、"库存查询")
  • nodeOrder:排序号(前端按此排序展示)
  • status:SUCCESS / FAIL
  • content:节点内容(结构化数据)
  • nodeTag:标签文字(简短的结果摘要,如"通过"、"失败")
  • nodeTagColor:标签背景色

3.3 内容项(ContentItem)

节点内容支持三种类型混合:

类型 用途 前端渲染
TEXT 键值对文本 label: value(可带颜色)
TABLE 表格数据 带表头的表格
JSON JSON 数据 代码块 + 复制按钮

3.4 标签(nodeTag + nodeTagColor)

标签是节点的"一眼结论",在前端列表中直接展示,不需要展开节点详情就能看到每个步骤的结果。


四、简单示例:订单下单流程

假设有一个下单接口,流程是:校验参数 → 检查库存 → 扣减库存 → 创建订单。

4.1 定义节点

nodeOrder nodeName 说明
1 下单入参 记录原始请求
2 参数校验 校验商品、数量、地址
3 库存检查 检查库存是否充足
4 创建订单 生成订单号
99 最终结果 总耗时、成功/失败

4.2 示例代码

java 复制代码
@Service
public class OrderService {

    @Resource
    private SourcingTraceProducer traceProducer;

    public OrderResult createOrder(OrderRequest request) {
        long startTime = System.currentTimeMillis();
        String bizNo = generateBizNo(); // 生成业务号

        try {
            // ========== 节点1:下单入参 ==========
            traceProducer.sendAsync(SourcingTraceMessage.builder()
                .sourcingBizNo(bizNo)
                .sourcingType("ORDER")
                .systemSource("S001")
                .scenarioName("普通下单")
                .nodeName("下单入参").nodeOrder(1)
                .nodeTime("2026-05-21 10:00:01")
                .status("SUCCESS")
                .nodeTag("通过")
                .nodeTagColor("#E6FFEC")
                .content(Arrays.asList(
                    ContentItem.text("商品编码", request.getProductCode()),
                    ContentItem.text("下单数量", String.valueOf(request.getQty())),
                    ContentItem.text("收货地址", request.getAddress()),
                    ContentItem.jsonObj("完整入参", request)
                )).build());

            // ========== 节点2:参数校验 ==========
            validateParams(request); // 可能抛异常
            traceProducer.sendAsync(SourcingTraceMessage.builder()
                .sourcingBizNo(bizNo)
                .sourcingType("ORDER").systemSource("S001")
                .scenarioName("普通下单")
                .nodeName("参数校验").nodeOrder(2)
                .nodeTime("2026-05-21 10:00:01")
                .status("SUCCESS")
                .nodeTag("校验通过")
                .nodeTagColor("#E6FFEC")
                .content(Arrays.asList()).build());

            // ========== 节点3:库存检查 ==========
            int stockQty = checkStock(request.getProductCode());
            boolean enough = stockQty >= request.getQty();
            traceProducer.sendAsync(SourcingTraceMessage.builder()
                .sourcingBizNo(bizNo)
                .sourcingType("ORDER").systemSource("S001")
                .scenarioName("普通下单")
                .nodeName("库存检查").nodeOrder(3)
                .nodeTime("2026-05-21 10:00:02")
                .status(enough ? "SUCCESS" : "FAIL")
                .nodeTag(enough ? "库存充足" : "库存不足")
                .nodeTagColor(enough ? "#E6FFEC" : "#FFECE8")
                .content(Arrays.asList(
                    ContentItem.text("当前库存", String.valueOf(stockQty)),
                    ContentItem.text("需求数量", String.valueOf(request.getQty()))
                )).build());

            if (!enough) {
                throw new RuntimeException("库存不足");
            }

            // ========== 节点4:创建订单 ==========
            String orderNo = doCreateOrder(request);
            traceProducer.sendAsync(SourcingTraceMessage.builder()
                .sourcingBizNo(bizNo)
                .sourcingType("ORDER").systemSource("S001")
                .scenarioName("普通下单")
                .nodeName("创建订单").nodeOrder(4)
                .nodeTime("2026-05-21 10:00:03")
                .status("SUCCESS")
                .nodeTag("创建成功")
                .nodeTagColor("#E6FFEC")
                .content(Arrays.asList(
                    ContentItem.text("订单号", orderNo)
                )).build());

            // ========== 节点99:最终结果 ==========
            long costMs = System.currentTimeMillis() - startTime;
            traceProducer.sendAsync(SourcingTraceMessage.builder()
                .sourcingBizNo(bizNo)
                .sourcingType("ORDER").systemSource("S001")
                .scenarioName("普通下单")
                .nodeName("最终结果").nodeOrder(99)
                .nodeTime("2026-05-21 10:00:03")
                .status("SUCCESS")
                .nodeTag("下单成功")
                .nodeTagColor("#E6FFEC")
                .content(Arrays.asList(
                    ContentItem.text("总耗时", costMs + "ms"),
                    ContentItem.jsonObj("返回结果", result)
                )).build());

            return result;

        } catch (Exception e) {
            // ========== 节点99:最终结果(失败)==========
            long costMs = System.currentTimeMillis() - startTime;
            traceProducer.sendAsync(SourcingTraceMessage.builder()
                .sourcingBizNo(bizNo)
                .sourcingType("ORDER").systemSource("S001")
                .scenarioName("普通下单")
                .nodeName("最终结果").nodeOrder(99)
                .nodeTime("2026-05-21 10:00:03")
                .status("FAIL")
                .nodeTag("下单失败")
                .nodeTagColor("#FFECE8")
                .content(Arrays.asList(
                    ContentItem.text("总耗时", costMs + "ms"),
                    ContentItem.text("失败原因", e.getMessage())
                )).build());
            throw e;
        }
    }
}

4.3 前端展示效果

查询业务号后,页面展示类似:

复制代码
业务号:ORD260521100001
场景:普通下单
状态:成功
耗时:120ms

节点列表:
┌────┬──────────┬──────────┬─────────────────────────────┐
│ 序号 │ 节点名称   │ 标签      │ 内容摘要                     │
├────┼──────────┼──────────┼─────────────────────────────┤
│ 1  │ 下单入参   │ [通过]    │ 商品:ABC001, 数量:10         │
│ 2  │ 参数校验   │ [校验通过] │                             │
│ 3  │ 库存检查   │ [库存充足] │ 当前库存:50, 需求:10          │
│ 4  │ 创建订单   │ [创建成功] │ 订单号:SO260521001           │
│ 99 │ 最终结果   │ [下单成功] │ 总耗时:120ms                 │
└────┴──────────┴──────────┴─────────────────────────────┘

五、设计要点

5.1 异步发送,不影响业务

java 复制代码
// 用 sendAsync,MQ 发送失败只打日志,不影响业务主流程
traceProducer.sendAsync(message);

5.2 业务号贯穿全流程

同一次请求的所有节点必须使用同一个 bizNo,这是聚合查询的唯一依据。

5.3 节点可乱序到达

消息通过 MQ 异步发送,到达顺序不保证。前端按 nodeOrder 排序展示,不依赖消息到达顺序。

5.4 标签是"一眼结论"

标签(nodeTag)的作用是让用户不展开节点详情就能快速判断每个步骤的结果。设计原则:

  • 简短(2-4个字)
  • 有明确的成功/失败语义
  • 颜色区分(绿色底=成功,红色底=失败)

5.5 特殊节点约定

服务端会从特定节点中提取字段:

  • 第一个节点(入参节点):提取查询字段(如客户编码、商品编码等)写入主表,用于列表页搜索
  • 最后一个节点(最终结果):提取"总耗时"写入主表

5.6 双层 try-catch 模式

java 复制代码
try {
    // 步骤1
    try {
        doStep1();
        sendNode1(SUCCESS);
    } catch (Exception e) {
        sendNode1(FAIL);
        throw e;
    }

    // 步骤2
    try {
        doStep2();
        sendNode2(SUCCESS);
    } catch (Exception e) {
        sendNode2(FAIL);
        throw e;
    }

    // 最终结果(成功)
    sendFinalNode(SUCCESS);
    return result;

} catch (Exception e) {
    // 最终结果(失败)
    sendFinalNode(FAIL);
    throw e;
}

这种模式确保:

  • 每个步骤失败时,对应节点记录 FAIL
  • 无论哪个步骤失败,最终结果节点都会记录 FAIL
  • 失败后的步骤不再发送节点日志

5.7 工具方法抽取

当节点多时,每次构建 Message 的重复代码很多。建议抽取工具方法:

java 复制代码
private void sendTraceLog(String bizNo, String nodeName, int nodeOrder,
                          String status, List<ContentItem> content,
                          String nodeTag, String nodeTagColor) {
    SourcingTraceMessage message = SourcingTraceMessage.builder()
        .sourcingBizNo(bizNo)
        .sourcingType("ORDER")
        .systemSource("S001")
        .scenarioName("普通下单")
        .nodeName(nodeName)
        .nodeOrder(nodeOrder)
        .nodeTime(LocalDateTime.now().format(
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
        .status(status)
        .nodeTag(nodeTag)
        .nodeTagColor(nodeTagColor)
        .content(content)
        .build();
    traceProducer.sendAsync(message);
}

六、常量管理

将重复使用的字符串抽取为常量,避免硬编码:

java 复制代码
public final class TraceConstants {
    // 状态
    public static final String STATUS_SUCCESS = "SUCCESS";
    public static final String STATUS_FAIL = "FAIL";

    // 内容文字颜色
    public static final String COLOR_SUCCESS = "#52c41a";  // 绿色文字
    public static final String COLOR_FAIL = "#ff4d4f";     // 红色文字

    // 标签背景色
    public static final String TAG_COLOR_SUCCESS = "#E6FFEC"; // 绿色底
    public static final String TAG_COLOR_FAIL = "#FFECE8";    // 红色底
}

注意区分:

  • COLOR_SUCCESS/FAIL:用于 ContentItem 中文字的颜色(深绿/深红)
  • TAG_COLOR_SUCCESS/FAIL:用于标签的背景色(浅绿/浅红)

七、适用场景

这种设计模式适合以下场景:

  • 业务流程步骤多,排查问题时需要知道"卡在哪一步"
  • 需要业务人员(非开发)也能看懂流程执行情况
  • 需要按业务单号快速定位问题
  • 流程涉及多个外部系统调用,需要记录每次调用的入参和出参

不适合的场景:

  • 简单的 CRUD 操作
  • 对性能极度敏感的高频接口(虽然异步发送影响很小)
  • 不需要业务人员参与排查的纯技术问题

八、总结

概念 类比 作用
业务号 快递单号 串联整个流程
节点 快递物流节点 记录每个关键步骤
nodeOrder 时间线顺序 前端排序展示
nodeTag 物流状态标签 一眼看到结果
content 物流详情 展开查看具体信息
sendAsync 异步通知 不阻塞主流程

整个设计的核心思想:用结构化的方式记录业务流程的每个关键步骤,让任何人都能通过一个业务号快速了解"发生了什么、在哪一步、为什么失败"。

相关推荐
nnsix1 天前
设计模式 - 建造者模式 笔记
笔记·设计模式·建造者模式
cui17875681 天前
矩阵拼团 + 复购拼团:新零售最稳的复购模式,规则简单
大数据·人工智能·设计模式·零售
百珏1 天前
[灰度发布]:全链路透传组件:APM、自研方案与 Java Agent 的实现取舍
后端·设计模式·架构
likerhood1 天前
设计模式 · 享元模式(Flyweight Pattern)java
java·设计模式·享元模式
AI大法师1 天前
从 Adobe 焕新看品牌系统升级:Logo、主色、字体与产品体验如何重新对齐
大数据·人工智能·adobe·设计模式
贵慜_Derek1 天前
《从零实现 Agent 系统》连载 03|控制循环:感知—决策—行动—反思
人工智能·设计模式·架构
nnsix1 天前
设计模式 - 原型模式 笔记
笔记·设计模式·原型模式
nnsix1 天前
设计模式 - 适配器模式 笔记
笔记·设计模式·适配器模式
asdfg12589631 天前
一文理解软件开发中的“设计模式”
java·设计模式·软件开发