SpringBoot + 事务链路可视化 + 跨服务调用图:一笔订单涉及哪些服务?一图看清
前言
在微服务架构中,一个业务操作往往涉及多个服务的协作。例如,创建一笔订单可能需要调用订单服务、库存服务、支付服务、物流服务等多个服务。当系统出现问题时,如何快速定位问题所在?如何了解整个事务链路的执行情况?这些问题对于微服务的运维和故障排查至关重要。
想象一下这样的场景:用户下单后,订单状态一直显示为"处理中",但不知道具体卡在了哪个服务。此时,如果你能看到整个事务的执行链路,以及各个服务之间的调用关系,就能快速定位问题所在。
事务链路可视化和跨服务调用图正是为了解决这个问题而设计的。通过可视化的方式展示事务的执行过程和服务间的调用关系,可以帮助开发者和运维人员快速理解系统的运行状态,定位问题所在。本文将详细介绍如何在 Spring Boot 中实现事务链路可视化和跨服务调用图。
一、核心概念
1.1 事务链路
事务链路是指一个业务操作从开始到结束的完整执行过程,包括所有参与的服务和操作。在微服务架构中,一个事务链路可能跨越多个服务,涉及多个数据库操作和网络调用。
1.2 分布式追踪
分布式追踪是一种用于监控和观察分布式系统的技术,通过在请求中添加唯一的追踪标识符,记录请求在各个服务之间的传递过程。分布式追踪系统可以帮助我们了解请求的执行路径、耗时和状态。
1.3 跨服务调用图
跨服务调用图是一种可视化表示,展示了服务之间的调用关系和依赖关系。通过调用图,我们可以清晰地看到一个业务操作涉及哪些服务,以及这些服务之间的调用顺序和依赖关系。
1.4 追踪上下文
追踪上下文是指在分布式系统中,用于传递追踪信息的上下文对象。它包含了追踪标识符、跨度信息等,用于将不同服务的操作关联到同一个事务链路中。
1.5 跨度(Span)
跨度是分布式追踪中的基本概念,表示一个操作的执行过程。一个跨度可以包含多个子跨度,形成一个树状结构,代表了操作的执行层次。
二、技术方案
2.1 架构设计
事务链路可视化和跨服务调用图的架构设计主要包括以下几个部分:
- 追踪数据采集:在各个服务中采集追踪数据,包括请求的开始、结束时间,以及服务间的调用关系
- 追踪数据存储:将采集到的追踪数据存储到数据库或消息队列中
- 追踪数据分析:分析追踪数据,构建事务链路和调用关系图
- 可视化展示:通过前端界面展示事务链路和调用关系图
2.2 技术选型
- Spring Boot:作为基础框架,提供依赖注入、配置管理等功能
- Spring Cloud Sleuth:用于分布式追踪,生成追踪标识符和跨度信息
- Zipkin:用于存储和可视化追踪数据
- Spring Cloud OpenFeign:用于服务间调用,自动传递追踪上下文
- Neo4j:用于存储和查询服务间的调用关系
- React + D3.js:用于前端可视化展示
- Spring Boot Actuator:用于暴露监控端点
2.3 核心流程
- 请求开始:当一个请求进入系统时,Spring Cloud Sleuth 会生成一个唯一的追踪标识符(Trace ID)
- 服务调用:当服务调用其他服务时,Spring Cloud Sleuth 会生成一个跨度(Span),并将追踪标识符传递给被调用的服务
- 数据采集:各个服务将追踪数据发送到 Zipkin
- 数据存储:Zipkin 将追踪数据存储到数据库中
- 数据分析:分析追踪数据,构建事务链路和调用关系图
- 可视化展示:通过前端界面展示事务链路和调用关系图
三、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 自动配置,简化集成
- 实现追踪服务的高可用
- 对追踪数据进行加密和脱敏处理
- 建立完善的监控和告警机制
互动话题:
- 你在实际项目中是如何实现分布式追踪的?
- 你认为事务链路可视化和跨服务调用图最大的价值是什么?
- 你有使用过类似的工具或方案吗?
欢迎在评论区留言讨论!更多技术文章,欢迎关注公众号:服务端技术精选
标题:SpringBoot + 事务链路可视化 + 跨服务调用图:一笔订单涉及哪些服务?一图看清
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/04/10/1775467914952.html
公众号:服务端技术精选
- 前言
- 一、核心概念
- 1.1 事务链路
- 1.2 分布式追踪
- 1.3 跨服务调用图
- 1.4 追踪上下文
- 1.5 跨度(Span)
- 二、技术方案
- 2.1 架构设计
- 2.2 技术选型
- 2.3 核心流程
- 三、Spring Boot 事务链路可视化实现
- 3.1 依赖配置
- 3.2 配置文件
- 3.3 服务间调用
- 3.3.1 Feign 客户端
- 3.3.2 服务实现
- 3.4 追踪服务
- 3.5 控制器
- 四、跨服务调用图实现
- 4.1 前端可视化
- 4.1.1 依赖配置
- 4.1.2 事务链路可视化组件
- 4.1.3 服务调用图组件
- 五、Spring Boot 完整实现
- 5.1 项目结构
- 5.2 核心配置
- 5.2.1 订单服务配置
- 5.2.2 追踪服务配置
- 5.3 核心代码
- 5.3.1 订单服务
- 5.3.2 追踪服务
- 5.3.3 前端应用
- 六、最佳实践
- 6.1 分布式追踪最佳实践
- 6.2 服务调用图最佳实践
- 6.3 可视化最佳实践
- 6.4 系统集成最佳实践
评论