SpringBoot + 事务链路可视化 + 跨服务调用图:一笔订单涉及哪些服务?一图看清

前言

在微服务架构中,一个业务操作往往涉及多个服务的协作。例如,创建一笔订单可能需要调用订单服务、库存服务、支付服务、物流服务等多个服务。当系统出现问题时,如何快速定位问题所在?如何了解整个事务链路的执行情况?这些问题对于微服务的运维和故障排查至关重要。

想象一下这样的场景:用户下单后,订单状态一直显示为"处理中",但不知道具体卡在了哪个服务。此时,如果你能看到整个事务的执行链路,以及各个服务之间的调用关系,就能快速定位问题所在。

事务链路可视化和跨服务调用图正是为了解决这个问题而设计的。通过可视化的方式展示事务的执行过程和服务间的调用关系,可以帮助开发者和运维人员快速理解系统的运行状态,定位问题所在。本文将详细介绍如何在 Spring Boot 中实现事务链路可视化和跨服务调用图。

一、核心概念

1.1 事务链路

事务链路是指一个业务操作从开始到结束的完整执行过程,包括所有参与的服务和操作。在微服务架构中,一个事务链路可能跨越多个服务,涉及多个数据库操作和网络调用。

1.2 分布式追踪

分布式追踪是一种用于监控和观察分布式系统的技术,通过在请求中添加唯一的追踪标识符,记录请求在各个服务之间的传递过程。分布式追踪系统可以帮助我们了解请求的执行路径、耗时和状态。

1.3 跨服务调用图

跨服务调用图是一种可视化表示,展示了服务之间的调用关系和依赖关系。通过调用图,我们可以清晰地看到一个业务操作涉及哪些服务,以及这些服务之间的调用顺序和依赖关系。

1.4 追踪上下文

追踪上下文是指在分布式系统中,用于传递追踪信息的上下文对象。它包含了追踪标识符、跨度信息等,用于将不同服务的操作关联到同一个事务链路中。

1.5 跨度(Span)

跨度是分布式追踪中的基本概念,表示一个操作的执行过程。一个跨度可以包含多个子跨度,形成一个树状结构,代表了操作的执行层次。

二、技术方案

2.1 架构设计

事务链路可视化和跨服务调用图的架构设计主要包括以下几个部分:

  1. 追踪数据采集:在各个服务中采集追踪数据,包括请求的开始、结束时间,以及服务间的调用关系
  2. 追踪数据存储:将采集到的追踪数据存储到数据库或消息队列中
  3. 追踪数据分析:分析追踪数据,构建事务链路和调用关系图
  4. 可视化展示:通过前端界面展示事务链路和调用关系图

2.2 技术选型

  • Spring Boot:作为基础框架,提供依赖注入、配置管理等功能
  • Spring Cloud Sleuth:用于分布式追踪,生成追踪标识符和跨度信息
  • Zipkin:用于存储和可视化追踪数据
  • Spring Cloud OpenFeign:用于服务间调用,自动传递追踪上下文
  • Neo4j:用于存储和查询服务间的调用关系
  • React + D3.js:用于前端可视化展示
  • Spring Boot Actuator:用于暴露监控端点

2.3 核心流程

  1. 请求开始:当一个请求进入系统时,Spring Cloud Sleuth 会生成一个唯一的追踪标识符(Trace ID)
  2. 服务调用:当服务调用其他服务时,Spring Cloud Sleuth 会生成一个跨度(Span),并将追踪标识符传递给被调用的服务
  3. 数据采集:各个服务将追踪数据发送到 Zipkin
  4. 数据存储:Zipkin 将追踪数据存储到数据库中
  5. 数据分析:分析追踪数据,构建事务链路和调用关系图
  6. 可视化展示:通过前端界面展示事务链路和调用关系图

三、Spring Boot 事务链路可视化实现

3.1 依赖配置

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Cloud Sleuth -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>

    <!-- Zipkin Client -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>

    <!-- Spring Cloud OpenFeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

    <!-- Spring Cloud Netflix Eureka Client -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <!-- Neo4j -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-neo4j</artifactId>
    </dependency>

    <!-- Spring Boot Actuator -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!-- Micrometer -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>

    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3.2 配置文件

server:
  port: 8080

spring:
  application:
    name: order-service
  cloud:
    sleuth:
      sampler:
        probability: 1.0  # 采样率,1.0表示全采样
    zipkin:
      base-url: http://localhost:9411  # Zipkin 服务器地址
  neo4j:
    uri: bolt://localhost:7687
    authentication:
      username: neo4j
      password: password

# 监控配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

# Eureka 配置
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

3.3 服务间调用

3.3.1 Feign 客户端

@FeignClient(name = "inventory-service")
public interface InventoryFeignClient {

    @PostMapping("/api/inventory/deduct")
    void deductInventory(@RequestBody InventoryRequest request);

}

@FeignClient(name = "payment-service")
public interface PaymentFeignClient {

    @PostMapping("/api/payment/process")
    void processPayment(@RequestBody PaymentRequest request);

}

3.3.2 服务实现

@Service
@Slf4j
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private InventoryFeignClient inventoryFeignClient;

    @Autowired
    private PaymentFeignClient paymentFeignClient;

    @Autowired
    private TraceService traceService;

    /**
     * 创建订单
     */
    public Order createOrder(OrderRequest request) {
        log.info("开始创建订单");

        // 记录开始时间
        long startTime = System.currentTimeMillis();

        // 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setAmount(request.getAmount());
        order.setStatus("PENDING");
        order.setCreatedAt(LocalDateTime.now());
        order = orderRepository.save(order);

        try {
            // 扣减库存
            inventoryFeignClient.deductInventory(new InventoryRequest(request.getProductId(), request.getQuantity()));

            // 处理支付
            paymentFeignClient.processPayment(new PaymentRequest(request.getUserId(), request.getAmount()));

            // 更新订单状态
            order.setStatus("SUCCESS");
            order = orderRepository.save(order);
        } catch (Exception e) {
            log.error("创建订单失败", e);
            order.setStatus("FAILED");
            order = orderRepository.save(order);
            throw new RuntimeException("创建订单失败", e);
        } finally {
            // 记录结束时间
            long endTime = System.currentTimeMillis();
            // 记录追踪信息
            traceService.recordTrace("createOrder", startTime, endTime, order.getId());
        }

        log.info("订单创建完成");
        return order;
    }

}

3.4 追踪服务

@Service
@Slf4j
public class TraceService {

    @Autowired
    private Tracer tracer;

    @Autowired
    private Neo4jTemplate neo4jTemplate;

    /**
     * 记录追踪信息
     */
    public void recordTrace(String operation, long startTime, long endTime, Long businessId) {
        // 获取当前追踪信息
        Span currentSpan = tracer.currentSpan();
        if (currentSpan != null) {
            // 获取追踪ID
            String traceId = currentSpan.context().traceId();
            // 获取跨度ID
            String spanId = currentSpan.context().spanId();
            // 获取父跨度ID
            String parentSpanId = currentSpan.context().parentId();
            // 获取服务名称
            String serviceName = currentSpan.context().serviceName();

            // 计算执行时间
            long duration = endTime - startTime;

            // 保存到Neo4j
            saveTraceToNeo4j(traceId, spanId, parentSpanId, serviceName, operation, startTime, endTime, duration, businessId);

            log.info("记录追踪信息: traceId={}, spanId={}, serviceName={}, operation={}, duration={}ms",
                    traceId, spanId, serviceName, operation, duration);
        }
    }

    /**
     * 保存追踪信息到Neo4j
     */
    private void saveTraceToNeo4j(String traceId, String spanId, String parentSpanId, 
                                String serviceName, String operation, 
                                long startTime, long endTime, long duration, 
                                Long businessId) {
        // 创建节点
        String cypher = "MERGE (s:Service {name: $serviceName}) " +
                        "MERGE (o:Operation {name: $operation}) " +
                        "MERGE (t:Trace {id: $traceId}) " +
                        "MERGE (span:Span {id: $spanId}) " +
                        "SET span.startTime = $startTime, span.endTime = $endTime, span.duration = $duration, span.businessId = $businessId " +
                        "MERGE (s)-[:PROVIDES]->(o) " +
                        "MERGE (t)-[:CONTAINS]->(span) " +
                        "MERGE (span)-[:EXECUTES]->(o) ";

        // 如果有父跨度,创建关系
        if (parentSpanId != null) {
            cypher += "MERGE (parentSpan:Span {id: $parentSpanId}) " +
                     "MERGE (parentSpan)-[:CHILD]->(span) ";
        }

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("serviceName", serviceName);
        parameters.put("operation", operation);
        parameters.put("traceId", traceId);
        parameters.put("spanId", spanId);
        parameters.put("parentSpanId", parentSpanId);
        parameters.put("startTime", startTime);
        parameters.put("endTime", endTime);
        parameters.put("duration", duration);
        parameters.put("businessId", businessId);

        neo4jTemplate.execute(cypher, parameters);
    }

    /**
     * 根据业务ID获取事务链路
     */
    public List<SpanInfo> getTraceByBusinessId(Long businessId) {
        String cypher = "MATCH (span:Span {businessId: $businessId}) " +
                        "MATCH (span)-[:EXECUTES]->(o:Operation) " +
                        "MATCH (s:Service)-[:PROVIDES]->(o) " +
                        "RETURN span.id as spanId, span.startTime as startTime, span.endTime as endTime, span.duration as duration, " +
                        "s.name as serviceName, o.name as operation " +
                        "ORDER BY span.startTime";

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("businessId", businessId);

        return neo4jTemplate.query(cypher, parameters)
                .stream()
                .map(map -> {
                    SpanInfo spanInfo = new SpanInfo();
                    spanInfo.setSpanId((String) map.get("spanId"));
                    spanInfo.setStartTime((Long) map.get("startTime"));
                    spanInfo.setEndTime((Long) map.get("endTime"));
                    spanInfo.setDuration((Long) map.get("duration"));
                    spanInfo.setServiceName((String) map.get("serviceName"));
                    spanInfo.setOperation((String) map.get("operation"));
                    return spanInfo;
                })
                .collect(Collectors.toList());
    }

    /**
     * 获取服务调用图
     */
    public List<ServiceCallInfo> getServiceCallGraph() {
        String cypher = "MATCH (s1:Service)-[:PROVIDES]->(o1:Operation) " +
                        "MATCH (s2:Service)-[:PROVIDES]->(o2:Operation) " +
                        "MATCH (span1:Span)-[:EXECUTES]->(o1) " +
                        "MATCH (span2:Span)-[:EXECUTES]->(o2) " +
                        "WHERE span1.id <> span2.id AND span1.startTime < span2.startTime " +
                        "RETURN s1.name as sourceService, s2.name as targetService, " +
                        "count(*) as callCount " +
                        "GROUP BY s1.name, s2.name";

        return neo4jTemplate.query(cypher, Collections.emptyMap())
                .stream()
                .map(map -> {
                    ServiceCallInfo callInfo = new ServiceCallInfo();
                    callInfo.setSourceService((String) map.get("sourceService"));
                    callInfo.setTargetService((String) map.get("targetService"));
                    callInfo.setCallCount(((Long) map.get("callCount")).intValue());
                    return callInfo;
                })
                .collect(Collectors.toList());
    }

}

3.5 控制器

@RestController
@RequestMapping("/api/trace")
public class TraceController {

    @Autowired
    private TraceService traceService;

    /**
     * 根据业务ID获取事务链路
     */
    @GetMapping("/business/{id}")
    public ResponseEntity<List<SpanInfo>> getTraceByBusinessId(@PathVariable Long id) {
        List<SpanInfo> trace = traceService.getTraceByBusinessId(id);
        return ResponseEntity.ok(trace);
    }

    /**
     * 获取服务调用图
     */
    @GetMapping("/service-graph")
    public ResponseEntity<List<ServiceCallInfo>> getServiceCallGraph() {
        List<ServiceCallInfo> callGraph = traceService.getServiceCallGraph();
        return ResponseEntity.ok(callGraph);
    }

}

四、跨服务调用图实现

4.1 前端可视化

4.1.1 依赖配置

{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "d3": "^7.8.5",
    "axios": "^1.4.0"
  }
}

4.1.2 事务链路可视化组件

import React, { useEffect, useState } from 'react';
import * as d3 from 'd3';
import axios from 'axios';

const TraceVisualization = ({ businessId }) => {
  const [traceData, setTraceData] = useState([]);

  useEffect(() => {
    // 获取事务链路数据
    axios.get(`/api/trace/business/${businessId}`)
      .then(response => {
        setTraceData(response.data);
      })
      .catch(error => {
        console.error('获取事务链路数据失败', error);
      });
  }, [businessId]);

  useEffect(() => {
    if (traceData.length === 0) return;

    // 清除之前的图表
    d3.select('#trace-chart').selectAll('*').remove();

    // 设置图表尺寸
    const width = 800;
    const height = 400;
    const margin = { top: 20, right: 20, bottom: 30, left: 50 };

    // 创建SVG
    const svg = d3.select('#trace-chart')
      .append('svg')
      .attr('width', width)
      .attr('height', height);

    // 计算时间范围
    const startTime = d3.min(traceData, d => d.startTime);
    const endTime = d3.max(traceData, d => d.endTime);
    const timeRange = endTime - startTime;

    // 创建时间比例尺
    const x = d3.scaleLinear()
      .domain([0, timeRange])
      .range([margin.left, width - margin.right]);

    // 创建服务比例尺
    const services = [...new Set(traceData.map(d => d.serviceName))];
    const y = d3.scaleBand()
      .domain(services)
      .range([margin.top, height - margin.bottom])
      .padding(0.4);

    // 添加X轴
    svg.append('g')
      .attr('transform', `translate(0,${height - margin.bottom})`)
      .call(d3.axisBottom(x)
        .tickFormat(d => `${(d / 1000).toFixed(2)}s`));

    // 添加Y轴
    svg.append('g')
      .attr('transform', `translate(${margin.left},0)`)
      .call(d3.axisLeft(y));

    // 添加矩形表示跨度
    svg.selectAll('rect')
      .data(traceData)
      .enter()
      .append('rect')
      .attr('x', d => x(d.startTime - startTime))
      .attr('y', d => y(d.serviceName))
      .attr('width', d => x(d.duration))
      .attr('height', y.bandwidth())
      .attr('fill', 'steelblue')
      .append('title')
      .text(d => `${d.serviceName}: ${d.operation} (${d.duration}ms)`);

    // 添加标签
    svg.selectAll('text')
      .data(traceData)
      .enter()
      .append('text')
      .attr('x', d => x(d.startTime - startTime) + 5)
      .attr('y', d => y(d.serviceName) + y.bandwidth() / 2 + 5)
      .text(d => d.operation)
      .attr('font-size', '12px')
      .attr('fill', 'white');

  }, [traceData]);

  return (
    <div>
      <h3>事务链路可视化</h3>
      <div id="trace-chart" style={{ width: '100%', height: '400px' }}></div>
    </div>
  );
};

export default TraceVisualization;

4.1.3 服务调用图组件

import React, { useEffect, useState, useRef } from 'react';
import * as d3 from 'd3';
import axios from 'axios';

const ServiceCallGraph = () => {
  const [graphData, setGraphData] = useState([]);
  const svgRef = useRef(null);

  useEffect(() => {
    // 获取服务调用图数据
    axios.get('/api/trace/service-graph')
      .then(response => {
        setGraphData(response.data);
      })
      .catch(error => {
        console.error('获取服务调用图数据失败', error);
      });
  }, []);

  useEffect(() => {
    if (graphData.length === 0) return;

    // 清除之前的图表
    d3.select(svgRef.current).selectAll('*').remove();

    // 设置图表尺寸
    const width = 800;
    const height = 600;

    // 创建SVG
    const svg = d3.select(svgRef.current)
      .attr('width', width)
      .attr('height', height);

    // 提取节点和边
    const nodes = [...new Set([...graphData.map(d => d.sourceService), ...graphData.map(d => d.targetService)])];
    const links = graphData.map(d => ({
      source: d.sourceService,
      target: d.targetService,
      value: d.callCount
    }));

    // 创建力导向图
    const simulation = d3.forceSimulation(nodes)
      .force('link', d3.forceLink(links).id(d => d).distance(150))
      .force('charge', d3.forceManyBody().strength(-300))
      .force('center', d3.forceCenter(width / 2, height / 2));

    // 添加边
    const link = svg.append('g')
      .selectAll('line')
      .data(links)
      .enter()
      .append('line')
      .attr('stroke', '#999')
      .attr('stroke-opacity', 0.6)
      .attr('stroke-width', d => Math.sqrt(d.value));

    // 添加边标签
    const linkText = svg.append('g')
      .selectAll('text')
      .data(links)
      .enter()
      .append('text')
      .text(d => d.value)
      .attr('font-size', '12px')
      .attr('fill', '#666');

    // 添加节点
    const node = svg.append('g')
      .selectAll('circle')
      .data(nodes)
      .enter()
      .append('circle')
      .attr('r', 20)
      .attr('fill', 'steelblue')
      .call(d3.drag()
        .on('start', dragstarted)
        .on('drag', dragged)
        .on('end', dragended));

    // 添加节点标签
    const nodeText = svg.append('g')
      .selectAll('text')
      .data(nodes)
      .enter()
      .append('text')
      .text(d => d)
      .attr('font-size', '12px')
      .attr('text-anchor', 'middle')
      .attr('dy', 4)
      .attr('fill', 'white');

    // 更新力导向图
    simulation.on('tick', () => {
      link
        .attr('x1', d => d.source.x)
        .attr('y1', d => d.source.y)
        .attr('x2', d => d.target.x)
        .attr('y2', d => d.target.y);

      linkText
        .attr('x', d => (d.source.x + d.target.x) / 2)
        .attr('y', d => (d.source.y + d.target.y) / 2);

      node
        .attr('cx', d => d.x)
        .attr('cy', d => d.y);

      nodeText
        .attr('x', d => d.x)
        .attr('y', d => d.y);
    });

    // 拖拽函数
    function dragstarted(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

    function dragged(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    }

    function dragended(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }

  }, [graphData]);

  return (
    <div>
      <h3>服务调用图</h3>
      <svg ref={svgRef} style={{ width: '100%', height: '600px' }}></svg>
    </div>
  );
};

export default ServiceCallGraph;

五、Spring Boot 完整实现

5.1 项目结构

transaction-trace-demo/
├── order-service/              # 订单服务
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/order/  # 源代码
│   │   │   └── resources/             # 配置文件
│   │   └── test/                      # 测试代码
│   └── pom.xml                        # Maven 依赖
├── inventory-service/          # 库存服务
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/inventory/  # 源代码
│   │   │   └── resources/                 # 配置文件
│   │   └── test/                          # 测试代码
│   └── pom.xml                            # Maven 依赖
├── payment-service/            # 支付服务
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/payment/  # 源代码
│   │   │   └── resources/                # 配置文件
│   │   └── test/                         # 测试代码
│   └── pom.xml                           # Maven 依赖
├── trace-service/              # 追踪服务
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/com/example/trace/   # 源代码
│   │   │   └── resources/             # 配置文件
│   │   └── test/                      # 测试代码
│   └── pom.xml                        # Maven 依赖
└── frontend/                   # 前端应用
    ├── src/
    │   ├── components/        # 组件
    │   ├── App.js             # 应用入口
    │   └── index.js            # 主文件
    └── package.json           # 依赖配置

5.2 核心配置

5.2.1 订单服务配置

server:
  port: 8080

spring:
  application:
    name: order-service
  cloud:
    sleuth:
      sampler:
        probability: 1.0
    zipkin:
      base-url: http://localhost:9411
  datasource:
    url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

# 监控配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

# Eureka 配置
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

5.2.2 追踪服务配置

server:
  port: 8083

spring:
  application:
    name: trace-service
  cloud:
    sleuth:
      sampler:
        probability: 1.0
    zipkin:
      base-url: http://localhost:9411
  neo4j:
    uri: bolt://localhost:7687
    authentication:
      username: neo4j
      password: password

# 监控配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus

# Eureka 配置
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

5.3 核心代码

5.3.1 订单服务

@Service
@Slf4j
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private InventoryFeignClient inventoryFeignClient;

    @Autowired
    private PaymentFeignClient paymentFeignClient;

    @Autowired
    private TraceService traceService;

    /**
     * 创建订单
     */
    public Order createOrder(OrderRequest request) {
        log.info("开始创建订单");

        // 记录开始时间
        long startTime = System.currentTimeMillis();

        // 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        order.setAmount(request.getAmount());
        order.setStatus("PENDING");
        order.setCreatedAt(LocalDateTime.now());
        order = orderRepository.save(order);

        try {
            // 扣减库存
            inventoryFeignClient.deductInventory(new InventoryRequest(request.getProductId(), request.getQuantity()));

            // 处理支付
            paymentFeignClient.processPayment(new PaymentRequest(request.getUserId(), request.getAmount()));

            // 更新订单状态
            order.setStatus("SUCCESS");
            order = orderRepository.save(order);
        } catch (Exception e) {
            log.error("创建订单失败", e);
            order.setStatus("FAILED");
            order = orderRepository.save(order);
            throw new RuntimeException("创建订单失败", e);
        } finally {
            // 记录结束时间
            long endTime = System.currentTimeMillis();
            // 记录追踪信息
            traceService.recordTrace("createOrder", startTime, endTime, order.getId());
        }

        log.info("订单创建完成");
        return order;
    }

}

5.3.2 追踪服务

@Service
@Slf4j
public class TraceService {

    @Autowired
    private Tracer tracer;

    @Autowired
    private Neo4jTemplate neo4jTemplate;

    /**
     * 记录追踪信息
     */
    public void recordTrace(String operation, long startTime, long endTime, Long businessId) {
        // 获取当前追踪信息
        Span currentSpan = tracer.currentSpan();
        if (currentSpan != null) {
            // 获取追踪ID
            String traceId = currentSpan.context().traceId();
            // 获取跨度ID
            String spanId = currentSpan.context().spanId();
            // 获取父跨度ID
            String parentSpanId = currentSpan.context().parentId();
            // 获取服务名称
            String serviceName = currentSpan.context().serviceName();

            // 计算执行时间
            long duration = endTime - startTime;

            // 保存到Neo4j
            saveTraceToNeo4j(traceId, spanId, parentSpanId, serviceName, operation, startTime, endTime, duration, businessId);

            log.info("记录追踪信息: traceId={}, spanId={}, serviceName={}, operation={}, duration={}ms",
                    traceId, spanId, serviceName, operation, duration);
        }
    }

    /**
     * 保存追踪信息到Neo4j
     */
    private void saveTraceToNeo4j(String traceId, String spanId, String parentSpanId, 
                                String serviceName, String operation, 
                                long startTime, long endTime, long duration, 
                                Long businessId) {
        // 创建节点
        String cypher = "MERGE (s:Service {name: $serviceName}) " +
                        "MERGE (o:Operation {name: $operation}) " +
                        "MERGE (t:Trace {id: $traceId}) " +
                        "MERGE (span:Span {id: $spanId}) " +
                        "SET span.startTime = $startTime, span.endTime = $endTime, span.duration = $duration, span.businessId = $businessId " +
                        "MERGE (s)-[:PROVIDES]->(o) " +
                        "MERGE (t)-[:CONTAINS]->(span) " +
                        "MERGE (span)-[:EXECUTES]->(o) ";

        // 如果有父跨度,创建关系
        if (parentSpanId != null) {
            cypher += "MERGE (parentSpan:Span {id: $parentSpanId}) " +
                     "MERGE (parentSpan)-[:CHILD]->(span) ";
        }

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("serviceName", serviceName);
        parameters.put("operation", operation);
        parameters.put("traceId", traceId);
        parameters.put("spanId", spanId);
        parameters.put("parentSpanId", parentSpanId);
        parameters.put("startTime", startTime);
        parameters.put("endTime", endTime);
        parameters.put("duration", duration);
        parameters.put("businessId", businessId);

        neo4jTemplate.execute(cypher, parameters);
    }

    /**
     * 根据业务ID获取事务链路
     */
    public List<SpanInfo> getTraceByBusinessId(Long businessId) {
        String cypher = "MATCH (span:Span {businessId: $businessId}) " +
                        "MATCH (span)-[:EXECUTES]->(o:Operation) " +
                        "MATCH (s:Service)-[:PROVIDES]->(o) " +
                        "RETURN span.id as spanId, span.startTime as startTime, span.endTime as endTime, span.duration as duration, " +
                        "s.name as serviceName, o.name as operation " +
                        "ORDER BY span.startTime";

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("businessId", businessId);

        return neo4jTemplate.query(cypher, parameters)
                .stream()
                .map(map -> {
                    SpanInfo spanInfo = new SpanInfo();
                    spanInfo.setSpanId((String) map.get("spanId"));
                    spanInfo.setStartTime((Long) map.get("startTime"));
                    spanInfo.setEndTime((Long) map.get("endTime"));
                    spanInfo.setDuration((Long) map.get("duration"));
                    spanInfo.setServiceName((String) map.get("serviceName"));
                    spanInfo.setOperation((String) map.get("operation"));
                    return spanInfo;
                })
                .collect(Collectors.toList());
    }

    /**
     * 获取服务调用图
     */
    public List<ServiceCallInfo> getServiceCallGraph() {
        String cypher = "MATCH (s1:Service)-[:PROVIDES]->(o1:Operation) " +
                        "MATCH (s2:Service)-[:PROVIDES]->(o2:Operation) " +
                        "MATCH (span1:Span)-[:EXECUTES]->(o1) " +
                        "MATCH (span2:Span)-[:EXECUTES]->(o2) " +
                        "WHERE span1.id <> span2.id AND span1.startTime < span2.startTime " +
                        "RETURN s1.name as sourceService, s2.name as targetService, " +
                        "count(*) as callCount " +
                        "GROUP BY s1.name, s2.name";

        return neo4jTemplate.query(cypher, Collections.emptyMap())
                .stream()
                .map(map -> {
                    ServiceCallInfo callInfo = new ServiceCallInfo();
                    callInfo.setSourceService((String) map.get("sourceService"));
                    callInfo.setTargetService((String) map.get("targetService"));
                    callInfo.setCallCount(((Long) map.get("callCount")).intValue());
                    return callInfo;
                })
                .collect(Collectors.toList());
    }

}

5.3.3 前端应用

import React, { useState } from 'react';
import TraceVisualization from './components/TraceVisualization';
import ServiceCallGraph from './components/ServiceCallGraph';

function App() {
  const [businessId, setBusinessId] = useState('1');

  return (
    <div className="App">
      <h1>事务链路可视化和服务调用图</h1>
      
      <div className="trace-section">
        <h2>事务链路可视化</h2>
        <div className="input-group">
          <label>业务ID:</label>
          <input 
            type="text" 
            value={businessId} 
            onChange={(e) => setBusinessId(e.target.value)}
          />
        </div>
        <TraceVisualization businessId={businessId} />
      </div>
      
      <div className="graph-section">
        <h2>服务调用图</h2>
        <ServiceCallGraph />
      </div>
    </div>
  );
}

export default App;

六、最佳实践

6.1 分布式追踪最佳实践

原则

  • 全链路追踪:确保所有服务都集成了分布式追踪
  • 合理采样:根据系统负载设置合理的采样率
  • 统一命名:使用统一的操作命名规范,便于分析
  • 携带业务信息:在追踪信息中携带业务ID等关键信息
  • 及时清理:定期清理过期的追踪数据,避免存储压力

建议

  • 使用 Spring Cloud Sleuth + Zipkin 作为分布式追踪方案
  • 为每个重要的业务操作添加追踪点
  • 在关键操作中记录开始和结束时间
  • 将追踪信息与业务数据关联,便于根据业务ID查询

6.2 服务调用图最佳实践

原则

  • 实时更新:定期更新服务调用图,反映最新的调用关系
  • 层次清晰:合理组织服务调用图的层次结构
  • 突出重点:突出显示重要的服务和调用关系
  • 交互友好:提供交互功能,如缩放、拖拽、点击查看详情等
  • 性能优化:优化图表渲染性能,避免卡顿

建议

  • 使用 Neo4j 存储服务调用关系,便于查询和分析
  • 使用 D3.js 或 ECharts 等库实现可视化
  • 为服务调用图添加过滤和搜索功能
  • 提供服务调用统计信息,如调用次数、平均耗时等

6.3 可视化最佳实践

原则

  • 简洁明了:避免过于复杂的可视化,保持简洁明了
  • 信息丰富:提供足够的信息,便于分析和理解
  • 交互友好:提供良好的交互体验,便于用户操作
  • 响应式设计:适应不同屏幕尺寸
  • 性能优化:优化前端性能,确保流畅的用户体验

建议

  • 使用 React 或 Vue 等前端框架
  • 使用 D3.js 或 ECharts 等可视化库
  • 实现数据缓存,避免频繁请求
  • 提供导出和分享功能

6.4 系统集成最佳实践

原则

  • 低侵入性:最小化对现有系统的修改
  • 可扩展性:易于扩展和集成新的服务
  • 可靠性:确保追踪系统的可靠性,避免影响业务系统
  • 安全性:保护追踪数据的安全,避免敏感信息泄露
  • 可维护性:代码结构清晰,易于维护

建议

  • 使用 Spring Boot 自动配置,简化集成
  • 实现追踪服务的高可用
  • 对追踪数据进行加密和脱敏处理
  • 建立完善的监控和告警机制

互动话题

  1. 你在实际项目中是如何实现分布式追踪的?
  2. 你认为事务链路可视化和跨服务调用图最大的价值是什么?
  3. 你有使用过类似的工具或方案吗?

欢迎在评论区留言讨论!更多技术文章,欢迎关注公众号:服务端技术精选


标题:SpringBoot + 事务链路可视化 + 跨服务调用图:一笔订单涉及哪些服务?一图看清
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/04/10/1775467914952.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消