1. 背景介绍
1.1 什么是 Transaction ID(事务ID)
Transaction ID(事务ID,简称 trancid 或 traceId)是一种用于唯一标识一次完整业务请求的追踪标识符。在分布式系统或复杂调用链中,一个用户操作可能会触发多个服务之间的调用,通过在整个调用链路中传递同一个事务ID,可以将分散在不同服务、不同日志文件中的日志串联起来。
1.2 为什么需要事务ID
在现代微服务架构和前后端分离的系统中,事务ID解决了以下核心问题:
-
分布式链路追踪
- 一个前端请求可能经过 API网关 → 业务服务A → 业务服务B → 数据库
- 通过统一的事务ID,可以快速定位整个调用链路中的问题节点
-
快速问题定位
- 用户报告问题时,通过事务ID可以立即检索出该次请求的完整日志
- 大幅减少问题排查时间,从数小时降低到分钟级
-
性能分析与优化
- 通过事务ID聚合统计,分析接口响应时间、失败率等指标
- 识别系统瓶颈,针对性优化慢接口
-
日志聚合与监控
- 配合ELK、Splunk等日志分析平台,实现日志智能聚合
- 支持按事务维度进行告警和监控
-
审计与合规
- 满足金融、医疗等行业的审计要求
- 提供完整的操作轨迹记录
1.3 事务ID的特点
- 全局唯一性:在整个系统生命周期内不重复
- 时序性(可选):部分实现方案支持从ID中提取时间信息
- 透传性:在整个调用链路中保持不变
- 低开销:生成和传递的性能开销可忽略不计
1.4 典型应用场景
用户操作:点击"查询患者信息"按钮
前端 → 生成 trancid: a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d
↓
API网关(记录trancid)
↓
患者服务(使用同一trancid记录日志)
↓
EMPI服务(透传trancid)
↓
数据库(在SQL注释中包含trancid)
当查询失败时,通过 trancid 可以快速在所有服务的日志中检索到完整调用链:
- 前端发起时间:14:32:15.123
- 网关接收时间:14:32:15.145(耗时22ms)
- 患者服务处理时间:14:32:15.150(耗时5ms)
- EMPI服务查询时间:14:32:15.155(耗时3200ms)❌ 问题定位
- 数据库查询时间:14:32:15.160(耗时3150ms)❌ 慢查询
2. 前端集成(Vue + Axios)
2.1 生成规则方案对比与选型
方案一:crypto.randomUUID()
示例代码:
const trancid = crypto.randomUUID();
// 输出: "a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d"
优点:
- ✅ 浏览器原生API,无需额外依赖
- ✅ 性能最优(原生实现)
- ✅ 符合RFC 4122标准
- ✅ 生成速度极快(纳秒级)
缺点:
- ❌ 兼容性有限(Chrome 92+, Safari 15.4+, Firefox 95+)
- ❌ 不支持IE和旧版浏览器
- ❌ Node.js需要v14.17.0+(且需要特殊处理)
适用场景:
- 现代浏览器项目(不需要支持旧版浏览器)
- 内部管理系统
- 对性能要求极高的场景
👉 查看最新兼容性:caniuse.com - crypto.randomUUID
方案二:uuid 库(v4 方法)
示例代码:
import { v4 as uuidv4 } from 'uuid';
const trancid = uuidv4();
// 输出: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
优点:
- ✅ 兼容性极佳(支持所有浏览器和Node.js)
- ✅ 生态成熟,npm周下载量1亿+
- ✅ 提供多种UUID版本(v1/v3/v4/v5)
- ✅ 符合RFC 4122标准
- ✅ 支持自定义随机数生成器
缺点:
- ❌ 增加打包体积(约4-5KB gzipped)
- ❌ 性能略低于原生API(但差异可忽略)
适用场景:
- 需要广泛浏览器兼容的项目✅ 推荐
- 已经使用uuid库的项目(本项目已引入)✅
- 需要不同UUID版本的场景
方案三:自定义简易UUID函数
示例代码:
function generateSimpleUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const trancid = generateSimpleUUID();
// 输出: "3d8f4a2b-1e5c-4f7a-b8c3-9d2e1f4a5b6c"
优点:
- ✅ 无需额外依赖,代码简洁
- ✅ 打包体积最小
- ✅ 全浏览器兼容
缺点:
- ❌ 随机性依赖Math.random()(非加密级安全)
- ❌ 理论上存在碰撞风险(虽然极低)
- ❌ 不符合RFC 4122严格标准
- ❌ 不支持高并发场景(Math.random种子问题)
适用场景:
- 简单项目,对唯一性要求不高
- 追求极致打包体积优化
- 非金融/医疗等严格合规场景
📋 方案对比总结表
| 对比维度 | crypto.randomUUID() | uuid库(v4) | 自定义简易函数 |
|---|---|---|---|
| 兼容性 | ⚠️ 有限(现代浏览器) | ✅ 优秀(全兼容) | ✅ 优秀(全兼容) |
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 打包体积 | ✅ 0KB | ⚠️ 4-5KB | ✅ <1KB |
| 标准符合性 | ✅ RFC 4122 | ✅ RFC 4122 | ⚠️ 部分符合 |
| 安全性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 生态成熟度 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 维护成本 | ✅ 低 | ✅ 低 | ⚠️ 中(需自维护) |
| 推荐场景 | 现代浏览器内部系统 | 企业级项目(推荐) | 简单项目 |
2.2 ⭐ 选型建议
针对本项目的推荐方案:uuid 库(v4)
理由:
- ✅ 项目已经引入uuid库(package.json第63行),无额外成本
- ✅ 医疗行业项目,需要严格的唯一性和合规性
- ✅ 需要支持多种浏览器环境(包括医院内网旧版浏览器)
- ✅ 当前代码已经在使用(axios.js第9行和第422行)
通用选型决策树:
需要支持旧版浏览器?
├─ 是 → 使用 uuid 库 ✅
└─ 否 → 是否已有uuid依赖?
├─ 是 → 使用 uuid 库 ✅
└─ 否 → 是否为严格合规项目?
├─ 是 → 使用 uuid 库 ✅
└─ 否 → 使用 crypto.randomUUID()
2.3 trancid 命名规范建议
2.3.1 HTTP Header 命名
推荐命名:
X-Transaction-Id ✅ 推荐(清晰易懂)
X-Trace-Id ✅ 常用(业界标准)
X-Request-Id ⚠️ 当前使用(可能与Request ID混淆)
命名原则:
- 使用 X- 前缀:表示自定义非标准Header(RFC 6648已不强制,但仍是惯例)
- 语义清晰:避免使用缩写(如 trancid)
- 与业界标准对齐:
- OpenTelemetry 使用
traceparent - AWS X-Ray 使用
X-Amzn-Trace-Id - Zipkin 使用
X-B3-TraceId
- OpenTelemetry 使用
本项目建议:
- 保持
X-Request-Id作为单个请求的唯一标识 - 新增
X-Transaction-Id或X-Trace-Id作为业务事务标识 - 在前后端统一使用相同命名
2.3.2 代码中的变量命名
// ✅ 推荐
const transactionId = uuidv4();
const traceId = uuidv4();
// ⚠️ 可接受
const requestId = uuidv4(); // 如果确实指代单个请求
const txId = uuidv4(); // 在内部工具函数中可用
// ❌ 不推荐
const trancid = uuidv4(); // 拼写非标准
const id = uuidv4(); // 语义不明确
const uuid = uuidv4(); // 混淆了ID类型和值
2.4 请求携带方式建议
方式一:HTTP Header(推荐)✅
优点:
- ✅ 语义清晰,不污染业务参数
- ✅ 适用于所有HTTP方法(GET/POST/PUT/DELETE)
- ✅ 后端拦截器统一处理
- ✅ 不影响接口缓存策略
- ✅ 符合RESTful设计原则
缺点:
- ❌ 某些网络设备可能会过滤自定义Header(极少见)
实现代码:
// src/plugins/axios.js
import { v4 as uuidv4 } from 'uuid';
// 方式1:每个请求生成新ID
instance.interceptors.request.use(config => {
config.headers['X-Transaction-Id'] = uuidv4();
return config;
});
// 方式2:同一业务流程使用同一ID(高级用法)
// 适用于:一个页面操作触发多个接口调用的场景
let currentTransactionId = null;
export function startTransaction() {
currentTransactionId = uuidv4();
return currentTransactionId;
}
export function endTransaction() {
currentTransactionId = null;
}
instance.interceptors.request.use(config => {
// 如果有活跃的事务ID,使用它;否则生成新ID
config.headers['X-Transaction-Id'] = currentTransactionId || uuidv4();
return config;
});
方式二:Query参数(不推荐)⚠️
适用场景:
- WebSocket连接建立
- SSE(Server-Sent Events)
- 第三方服务不支持自定义Header
缺点:
- ❌ GET请求URL过长
- ❌ 日志中暴露,可能泄露追踪信息
- ❌ 影响接口缓存键
- ❌ 污染业务参数空间
实现代码:
// 仅用于特殊场景
config.params = {
...config.params,
_tid: uuidv4() // 使用下划线前缀避免与业务参数冲突
};
方式三:Body参数(不推荐)❌
缺点:
- ❌ 仅适用于POST/PUT请求
- ❌ 污染业务数据模型
- ❌ 需要前后端数据结构特殊处理
2.5 完整实现代码(基于现有项目)
2.5.1 优化 axios.js 配置
// src/plugins/axios.js(优化现有代码)
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { Message } from '@imfe/ui';
// ... 现有代码 ...
// 当前事务ID(用于关联同一业务流程的多个请求)
let currentTransactionId = null;
/**
* 开始一个新的业务事务
* @param {string} businessType - 业务类型(用于日志分类)
* @returns {string} 事务ID
*/
export function startTransaction(businessType = '') {
currentTransactionId = uuidv4();
console.log(`[Transaction Started] ${currentTransactionId} - ${businessType}`);
return currentTransactionId;
}
/**
* 结束当前业务事务
*/
export function endTransaction() {
if (currentTransactionId) {
console.log(`[Transaction Ended] ${currentTransactionId}`);
currentTransactionId = null;
}
}
/**
* 获取当前事务ID
*/
export function getCurrentTransactionId() {
return currentTransactionId;
}
// 请求拦截器(在现有拦截器中添加)
instance.interceptors.request.use(config => {
// ... 现有逻辑 ...
// 添加事务ID(修改现有的handleRequestId函数)
handleTransactionId(config);
return config;
}, error => {
// ... 现有错误处理 ...
});
/**
* 添加事务ID和请求ID
* @param {Object} config - axios配置对象
*/
function handleTransactionId(config) {
// X-Request-Id: 单个HTTP请求的唯一标识(每次都生成新的)
config.headers['X-Request-Id'] = uuidv4();
// X-Transaction-Id: 业务事务标识(同一业务流程共享)
// 如果有活跃的事务ID,使用它;否则与Request-Id相同
config.headers['X-Transaction-Id'] = currentTransactionId || config.headers['X-Request-Id'];
// 可选:添加到响应中,方便前端获取
config._transactionId = config.headers['X-Transaction-Id'];
config._requestId = config.headers['X-Request-Id'];
}
/**
* 原有的handleRequestId函数可以改为调用新函数
* 保持向后兼容
*/
function handleRequestId(config) {
handleTransactionId(config);
}
// 响应拦截器(添加日志输出)
instance.interceptors.response.use(
response => {
// 记录成功响应
const { config } = response;
console.log(`[API Success] ${config.method?.toUpperCase()} ${config.url}`, {
transactionId: config._transactionId,
requestId: config._requestId,
duration: Date.now() - config._requestStartTime
});
// ... 现有逻辑 ...
return response;
},
error => {
// 记录失败响应
const { config } = error;
console.error(`[API Error] ${config?.method?.toUpperCase()} ${config?.url}`, {
transactionId: config?._transactionId,
requestId: config?._requestId,
error: error.message,
duration: Date.now() - (config?._requestStartTime || Date.now())
});
// ... 现有错误处理 ...
}
);
// 添加请求开始时间(用于计算耗时)
instance.interceptors.request.use(config => {
config._requestStartTime = Date.now();
return config;
});
export default instance;
2.5.2 业务代码使用示例
// src/project-patient/pages/patient-view/Index.vue
<template>
<div class="patient-view">
<el-button @click="handleLoadPatientData">加载患者信息</el-button>
</div>
</template>
<script>
import axios, { startTransaction, endTransaction } from '@/plugins/axios';
export default {
name: 'PatientView',
methods: {
async handleLoadPatientData() {
// 开始一个新的业务事务
const transactionId = startTransaction('LOAD_PATIENT_DATA');
try {
// 这些请求会共享同一个 transactionId
const patientInfo = await axios.post('/api/patient/info', {
patientId: this.patientId
});
const visitRecords = await axios.post('/api/patient/visits', {
patientId: this.patientId
});
const labResults = await axios.post('/api/patient/lab-results', {
patientId: this.patientId
});
// 处理数据...
this.patientData = {
info: patientInfo,
visits: visitRecords,
labs: labResults
};
console.log(`[业务成功] 事务ID: ${transactionId}`);
} catch (error) {
console.error(`[业务失败] 事务ID: ${transactionId}`, error);
this.$message.error(`加载失败,事务ID: ${transactionId}`);
} finally {
// 结束事务
endTransaction();
}
}
}
};
</script>
2.5.3 全局错误处理增强
// src/utils/error-handler.js
import { getCurrentTransactionId } from '@/plugins/axios';
import { Message } from '@imfe/ui';
/**
* 全局错误处理器
*/
export function handleGlobalError(error, vm, info) {
const transactionId = getCurrentTransactionId();
console.error('[Global Error]', {
error,
component: vm?.$options?.name,
info,
transactionId,
timestamp: new Date().toISOString()
});
// 显示用户友好的错误信息
Message.error({
message: `操作失败,请联系管理员\n事务ID: ${transactionId}`,
duration: 5000,
showClose: true
});
// 可选:上报到监控平台
reportErrorToMonitoring({
error,
transactionId,
component: vm?.$options?.name,
info
});
}
/**
* 上报错误到监控平台(示例)
*/
function reportErrorToMonitoring(errorInfo) {
// 集成Sentry、阿里云ARMS等监控平台
if (window.Sentry) {
window.Sentry.captureException(errorInfo.error, {
tags: {
transactionId: errorInfo.transactionId,
component: errorInfo.component
}
});
}
}
2.5.4 在 main.js 中注册
// src/project-patient/main.js
import Vue from 'vue';
import App from './App.vue';
import { handleGlobalError } from '@/utils/error-handler';
// 注册全局错误处理
Vue.config.errorHandler = handleGlobalError;
new Vue({
render: h => h(App)
}).$mount('#app');
3. 后端集成(Spring Boot)
3.1 添加依赖
通常Spring Boot项目已经包含所需依赖,无需额外添加。如果使用基础Spring项目,需确认以下依赖:
<!-- pom.xml -->
<dependencies>
<!-- Spring Web(通常已有) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Servlet API(通常已有) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- SLF4J + Logback(Spring Boot自带) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
</dependencies>
3.2 创建 Filter 实现(推荐方案)✅
3.2.1 完整Filter代码
package com.zhyl.dsj.common.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
/**
* 事务ID过滤器
* 功能:
* 1. 从请求头提取或生成 Transaction ID
* 2. 将 Transaction ID 存入 MDC(Mapped Diagnostic Context)
* 3. 将 Transaction ID 添加到响应头
* 4. 请求完成后清理 MDC
*
* @author Your Name
* @date 2025-11-19
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级,确保最先执行
public class TransactionFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TransactionFilter.class);
// MDC 键名
private static final String MDC_TRANSACTION_ID_KEY = "transactionId";
private static final String MDC_REQUEST_ID_KEY = "requestId";
// HTTP Header 名称
private static final String HEADER_TRANSACTION_ID = "X-Transaction-Id";
private static final String HEADER_REQUEST_ID = "X-Request-Id";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
logger.info("TransactionFilter initialized");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
// 1. 获取或生成 Transaction ID
String transactionId = getOrGenerateTransactionId(httpRequest);
String requestId = getOrGenerateRequestId(httpRequest);
// 2. 存入 MDC(线程上下文)
MDC.put(MDC_TRANSACTION_ID_KEY, transactionId);
MDC.put(MDC_REQUEST_ID_KEY, requestId);
// 3. 添加到响应头(方便前端获取)
httpResponse.setHeader(HEADER_TRANSACTION_ID, transactionId);
httpResponse.setHeader(HEADER_REQUEST_ID, requestId);
// 4. 记录请求开始日志
long startTime = System.currentTimeMillis();
logger.info("Request started: {} {} from {}",
httpRequest.getMethod(),
httpRequest.getRequestURI(),
httpRequest.getRemoteAddr());
// 5. 执行请求
chain.doFilter(request, response);
// 6. 记录请求完成日志
long duration = System.currentTimeMillis() - startTime;
logger.info("Request completed: {} {} - Status: {} - Duration: {}ms",
httpRequest.getMethod(),
httpRequest.getRequestURI(),
httpResponse.getStatus(),
duration);
} catch (Exception e) {
// 记录异常
logger.error("Request failed: {} {} - Error: {}",
httpRequest.getMethod(),
httpRequest.getRequestURI(),
e.getMessage(),
e);
throw e;
} finally {
// 7. 清理 MDC(非常重要!避免内存泄漏)
MDC.remove(MDC_TRANSACTION_ID_KEY);
MDC.remove(MDC_REQUEST_ID_KEY);
MDC.clear();
}
}
@Override
public void destroy() {
logger.info("TransactionFilter destroyed");
}
/**
* 获取或生成 Transaction ID
* 优先从请求头获取,如果没有则生成新的
*/
private String getOrGenerateTransactionId(HttpServletRequest request) {
String transactionId = request.getHeader(HEADER_TRANSACTION_ID);
if (!StringUtils.hasText(transactionId)) {
// 如果前端没有传递,则生成新的
transactionId = generateUUID();
logger.debug("Generated new Transaction ID: {}", transactionId);
} else {
logger.debug("Using Transaction ID from request header: {}", transactionId);
}
return transactionId;
}
/**
* 获取或生成 Request ID
*/
private String getOrGenerateRequestId(HttpServletRequest request) {
String requestId = request.getHeader(HEADER_REQUEST_ID);
if (!StringUtils.hasText(requestId)) {
requestId = generateUUID();
}
return requestId;
}
/**
* 生成UUID
*/
private String generateUUID() {
return UUID.randomUUID().toString();
}
}
3.2.2 为什么推荐 Filter 而非 Interceptor?
| 对比维度 | Filter(推荐)✅ | Interceptor(备选)⚠️ |
|---|---|---|
| 执行时机 | Servlet容器级别,最早执行 | Spring MVC级别,晚于Filter |
| 覆盖范围 | 所有请求(包括静态资源、错误页面) | 仅Controller请求 |
| 异常捕获 | 可捕获所有异常(包括404) | 仅捕获Controller异常 |
| 技术标准 | Java Servlet标准 | Spring特有 |
| 适用场景 | 通用基础设施(日志、鉴权、CORS) | 业务拦截(权限校验、参数校验) |
结论: Transaction ID属于基础设施功能,应该覆盖所有请求,因此使用 Filter 是最佳实践。
3.3 注册 Filter
方式一:自动注册(推荐)✅
使用 @Component 注解,Spring Boot 会自动注册所有实现 Filter 接口的Bean。
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 控制Filter执行顺序
public class TransactionFilter implements Filter {
// 代码同上...
}
优点:
- ✅ 配置简单,零XML
- ✅ 自动扫描,无需手动注册
- ✅ 使用Spring的依赖注入
缺点:
- ❌ URL匹配模式固定为
/*(匹配所有请求) - ❌ 初始化参数不灵活
方式二:显式注册(灵活配置)⚠️
当需要自定义URL匹配模式或初始化参数时使用。
package com.zhyl.dsj.common.config;
import com.zhyl.dsj.common.filter.TransactionFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
/**
* Filter 配置类
*/
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TransactionFilter> transactionFilterRegistration() {
FilterRegistrationBean<TransactionFilter> registration = new FilterRegistrationBean<>();
// 1. 设置Filter实例(不使用@Component,手动创建)
registration.setFilter(new TransactionFilter());
// 2. 设置URL匹配模式
registration.addUrlPatterns("/api/*"); // 仅匹配 /api/ 开头的请求
registration.addUrlPatterns("/gateway/*");
// 3. 设置执行顺序
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
// 4. 设置Filter名称
registration.setName("transactionFilter");
// 5. 设置初始化参数(可选)
registration.addInitParameter("excludeUrls", "/health,/actuator");
return registration;
}
}
使用场景:
- 需要精确控制URL匹配规则
- 需要传递初始化参数
- 需要动态启用/禁用Filter
- 需要根据环境配置不同的Filter行为
方式三:使用 @WebFilter(不推荐)❌
@WebFilter(urlPatterns = "/*", filterName = "transactionFilter")
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TransactionFilter implements Filter {
// 代码同上...
}
缺点:
- ❌ 需要在启动类添加
@ServletComponentScan - ❌ 无法使用Spring的依赖注入
- ❌ 不推荐在Spring Boot项目中使用
3.4 配置 Logback 日志格式
方式 A:通过 application.yml(简单场景)✅
适用于简单的日志格式配置,无需复杂的Appender和Logger策略。
# application.yml
# 日志配置
logging:
# 日志级别
level:
root: INFO
com.zhyl.dsj: DEBUG
org.springframework.web: INFO
# 日志格式(包含 Transaction ID)
pattern:
# 控制台输出格式
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [TransactionId:%X{transactionId}] [RequestId:%X{requestId}] %logger{36} - %msg%n"
# 文件输出格式
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [TransactionId:%X{transactionId}] [RequestId:%X{requestId}] %logger{50} - %msg%n"
# 日志文件配置
file:
name: logs/dsj-clinical.log
max-size: 100MB
max-history: 30
total-size-cap: 10GB
# Spring Boot Actuator(可选,用于健康检查)
management:
endpoints:
web:
exposure:
include: health,info,metrics
日志输出示例:
2025-11-19 14:32:15.123 [http-nio-8080-exec-1] INFO [TransactionId:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RequestId:f47ac10b-58cc-4372-a567-0e02b2c3d479] c.z.d.patient.controller.PatientController - 查询患者信息: patientId=12345
2025-11-19 14:32:15.456 [http-nio-8080-exec-1] INFO [TransactionId:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RequestId:f47ac10b-58cc-4372-a567-0e02b2c3d479] c.z.d.patient.service.PatientService - 从数据库加载患者数据: patientId=12345
2025-11-19 14:32:15.789 [http-nio-8080-exec-1] INFO [TransactionId:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RequestId:f47ac10b-58cc-4372-a567-0e02b2c3d479] c.z.d.common.filter.TransactionFilter - Request completed: GET /api/patient/info - Status: 200 - Duration: 666ms
格式说明:
%d{yyyy-MM-dd HH:mm:ss.SSS}- 时间戳(精确到毫秒)[%thread]- 线程名%-5level- 日志级别(INFO/DEBUG/ERROR等)[TransactionId:%X{transactionId}]- 事务ID(从MDC获取)[RequestId:%X{requestId}]- 请求ID(从MDC获取)%logger{36}- Logger名称(最长36个字符)%msg%n- 日志消息 + 换行
方式 B:通过 logback-spring.xml(企业级推荐)✅
适用于复杂的日志策略,如按日期归档、异步日志、不同级别输出到不同文件等。
<?xml version="1.0" encoding="UTF-8"?>
<!-- logback-spring.xml(放在 src/main/resources 目录) -->
<configuration>
<!-- 引入 Spring Boot 默认配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- ============================================ -->
<!-- 变量定义 -->
<!-- ============================================ -->
<property name="LOG_HOME" value="logs"/>
<property name="APP_NAME" value="dsj-clinical"/>
<!-- 日志格式(包含 Transaction ID) -->
<property name="LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [TID:%X{transactionId:-N/A}] [RID:%X{requestId:-N/A}] %logger{50} - %msg%n"/>
<!-- JSON格式日志(用于日志采集系统) -->
<property name="LOG_PATTERN_JSON"
value='{"timestamp":"%d{yyyy-MM-dd HH:mm:ss.SSS}","thread":"%thread","level":"%-5level","transactionId":"%X{transactionId:-}","requestId":"%X{requestId:-}","logger":"%logger{50}","message":"%msg"}%n'/>
<!-- ============================================ -->
<!-- Appender 定义 -->
<!-- ============================================ -->
<!-- 1. 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 仅输出 INFO 及以上级别 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<!-- 2. 所有日志文件(按日期归档) -->
<appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-all.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 滚动策略:按日期和大小 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件命名规则 -->
<fileNamePattern>${LOG_HOME}/${APP_NAME}-all-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<!-- 单个文件最大100MB -->
<maxFileSize>100MB</maxFileSize>
<!-- 保留30天 -->
<maxHistory>30</maxHistory>
<!-- 总大小限制10GB -->
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 3. 错误日志文件(单独记录ERROR级别) -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-error.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 仅输出 ERROR 级别 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-error-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>90</maxHistory> <!-- 错误日志保留更久 -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 4. 业务日志文件(用于业务审计) -->
<appender name="FILE_BUSINESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-business.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-business-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>180</maxHistory> <!-- 业务日志保留更久 -->
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 5. JSON格式日志(用于ELK等日志采集系统) -->
<appender name="FILE_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-json.log</file>
<encoder>
<pattern>${LOG_PATTERN_JSON}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-json-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>7</maxHistory> <!-- JSON日志通常被实时采集,保留时间短 -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 6. 异步日志(提升性能) -->
<appender name="ASYNC_FILE_ALL" class="ch.qos.logback.classic.AsyncAppender">
<!-- 队列大小 -->
<queueSize>512</queueSize>
<!-- 队列满时丢弃 TRACE/DEBUG/INFO 日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 不阻塞业务线程 -->
<neverBlock>true</neverBlock>
<appender-ref ref="FILE_ALL"/>
</appender>
<!-- ============================================ -->
<!-- Logger 配置 -->
<!-- ============================================ -->
<!-- 业务日志Logger(单独输出到业务日志文件) -->
<logger name="com.zhyl.dsj.business" level="INFO" additivity="false">
<appender-ref ref="FILE_BUSINESS"/>
<appender-ref ref="CONSOLE"/>
</logger>
<!-- 数据库SQL日志(开发环境开启) -->
<logger name="com.zhyl.dsj.mapper" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
<!-- Spring框架日志(减少冗余) -->
<logger name="org.springframework" level="INFO"/>
<logger name="org.springframework.web" level="INFO"/>
<logger name="org.mybatis" level="INFO"/>
<!-- 根Logger -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE_ALL"/>
<appender-ref ref="FILE_ERROR"/>
<appender-ref ref="FILE_JSON"/>
</root>
<!-- ============================================ -->
<!-- 环境配置(Spring Profile) -->
<!-- ============================================ -->
<!-- 开发环境:仅输出到控制台 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!-- 测试环境:输出到控制台和文件 -->
<springProfile name="test">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE_ALL"/>
</root>
</springProfile>
<!-- 生产环境:仅输出到文件 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_FILE_ALL"/>
<appender-ref ref="FILE_ERROR"/>
<appender-ref ref="FILE_JSON"/>
</root>
</springProfile>
</configuration>
配置说明:
-
多文件策略:
dsj-clinical-all.log- 所有日志dsj-clinical-error.log- 错误日志(单独文件便于告警)dsj-clinical-business.log- 业务审计日志dsj-clinical-json.log- JSON格式日志(供ELK采集)
-
日志归档策略:
- 按日期滚动(每天一个文件)
- 单文件大小限制(100MB自动切割)
- 历史文件压缩(.gz格式节省空间)
- 总大小限制(防止磁盘爆满)
-
异步日志:
- 使用
AsyncAppender提升性能 - 避免日志IO阻塞业务线程
- 生产环境必备
- 使用
-
环境隔离:
- 开发环境:控制台 + DEBUG级别
- 测试环境:控制台 + 文件 + INFO级别
- 生产环境:仅文件 + INFO级别 + JSON格式
方式 C:使用 Logstash JSON Encoder(高级)⚡
适用于微服务架构,需要将日志发送到ELK、Splunk等日志分析平台。
添加依赖:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.3</version>
</dependency>
logback-spring.xml配置:
<!-- Logstash JSON Appender -->
<appender name="LOGSTASH" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-logstash.json</file>
<!-- 使用 LogstashEncoder -->
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<!-- 自定义字段 -->
<customFields>{"app_name":"dsj-clinical","environment":"${spring.profiles.active}"}</customFields>
<!-- 包含 MDC 数据 -->
<includeMdcKeyName>transactionId</includeMdcKeyName>
<includeMdcKeyName>requestId</includeMdcKeyName>
<!-- 包含调用堆栈 -->
<includeCallerData>true</includeCallerData>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-logstash-%d{yyyy-MM-dd}.%i.json</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
输出的JSON格式:
{
"@timestamp": "2025-11-19T14:32:15.123+08:00",
"@version": "1",
"message": "查询患者信息: patientId=12345",
"logger_name": "com.zhyl.dsj.patient.controller.PatientController",
"thread_name": "http-nio-8080-exec-1",
"level": "INFO",
"level_value": 20000,
"transactionId": "a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d",
"requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"app_name": "dsj-clinical",
"environment": "prod"
}
3.5 业务代码中使用 Transaction ID
3.5.1 在 Controller 中使用
package com.zhyl.dsj.patient.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/patient")
public class PatientController {
private static final Logger logger = LoggerFactory.getLogger(PatientController.class);
@PostMapping("/info")
public Result<PatientInfo> getPatientInfo(@RequestBody PatientQuery query) {
// 直接使用 logger,Transaction ID 会自动包含在日志中
logger.info("查询患者信息: patientId={}", query.getPatientId());
// 如果需要在业务逻辑中使用 Transaction ID(例如返回给前端)
String transactionId = MDC.get("transactionId");
logger.debug("当前事务ID: {}", transactionId);
// 业务逻辑...
PatientInfo patientInfo = patientService.getPatientInfo(query.getPatientId());
return Result.success(patientInfo)
.withTransactionId(transactionId); // 可选:返回给前端
}
}
3.5.2 在 Service 中使用
package com.zhyl.dsj.patient.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class PatientService {
private static final Logger logger = LoggerFactory.getLogger(PatientService.class);
public PatientInfo getPatientInfo(String patientId) {
// 自动包含 Transaction ID
logger.info("从数据库加载患者数据: patientId={}", patientId);
try {
// 数据库查询...
PatientInfo info = patientMapper.selectById(patientId);
if (info == null) {
logger.warn("患者信息不存在: patientId={}", patientId);
throw new BusinessException("患者信息不存在");
}
logger.info("患者信息加载成功: patientId={}, patientName={}",
patientId, info.getName());
return info;
} catch (Exception e) {
// 异常日志会自动包含 Transaction ID
logger.error("加载患者信息失败: patientId={}", patientId, e);
throw new SystemException("系统错误,请联系管理员", e);
}
}
}
3.5.3 在异常处理中使用
package com.zhyl.dsj.common.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
String transactionId = MDC.get("transactionId");
logger.error("系统异常,事务ID: {}", transactionId, e);
// 返回用户友好的错误信息,包含 Transaction ID
return Result.error("系统繁忙,请稍后重试")
.withTransactionId(transactionId)
.withMessage("如需帮助,请提供事务ID: " + transactionId);
}
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
String transactionId = MDC.get("transactionId");
logger.warn("业务异常,事务ID: {}, 错误信息: {}", transactionId, e.getMessage());
return Result.error(e.getMessage())
.withTransactionId(transactionId);
}
}
3.5.4 统一响应结构(包含 Transaction ID)
package com.zhyl.dsj.common.model;
/**
* 统一响应结构
*/
public class Result<T> {
private Integer code;
private String message;
private T data;
private String transactionId; // 添加 Transaction ID
private Long timestamp;
// 构造方法和Builder...
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public Result<T> withTransactionId(String transactionId) {
this.transactionId = transactionId;
return this;
}
// Getters and Setters...
}
4. 高级建议
4.1 透传给下游服务
在微服务架构中,Transaction ID 需要在服务间透传,以保持完整的调用链追踪。
4.1.1 使用 RestTemplate 透传
package com.zhyl.dsj.common.config;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 添加拦截器,自动透传 Transaction ID
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add((request, body, execution) -> {
// 从 MDC 获取 Transaction ID
String transactionId = MDC.get("transactionId");
String requestId = MDC.get("requestId");
if (transactionId != null) {
request.getHeaders().add("X-Transaction-Id", transactionId);
}
if (requestId != null) {
request.getHeaders().add("X-Request-Id", requestId);
}
return execution.execute(request, body);
});
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
使用示例:
@Service
public class PatientService {
@Autowired
private RestTemplate restTemplate;
public EmpiData getEmpiData(String patientId) {
// Transaction ID 会自动添加到请求头
String url = "http://empi-service/api/empi/info?patientId=" + patientId;
return restTemplate.getForObject(url, EmpiData.class);
}
}
4.1.2 使用 Feign 客户端透传
package com.zhyl.dsj.common.feign;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign 请求拦截器配置
*/
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor transactionIdInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 从 MDC 获取并透传 Transaction ID
String transactionId = MDC.get("transactionId");
String requestId = MDC.get("requestId");
if (transactionId != null) {
template.header("X-Transaction-Id", transactionId);
}
if (requestId != null) {
template.header("X-Request-Id", requestId);
}
}
};
}
}
Feign 接口定义:
@FeignClient(name = "empi-service", configuration = FeignConfig.class)
public interface EmpiServiceClient {
@GetMapping("/api/empi/info")
EmpiData getEmpiData(@RequestParam("patientId") String patientId);
}
4.2 与 OpenTelemetry / Spring Cloud Sleuth 集成
4.2.1 Spring Cloud Sleuth 集成(推荐)✅
Spring Cloud Sleuth 是 Spring 生态的分布式追踪解决方案,可以与自定义 Transaction ID 共存。
添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- 可选:集成 Zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
配置:
# application.yml
spring:
sleuth:
sampler:
probability: 1.0 # 采样率(1.0 = 100%)
baggage:
correlation-fields:
- transactionId # 将自定义 Transaction ID 添加到 Sleuth Baggage
remote-fields:
- transactionId # 在服务间透传
zipkin:
base-url: http://localhost:9411 # Zipkin 服务地址
sender:
type: web
集成代码:
package com.zhyl.dsj.common.filter;
import brave.Tracer;
import brave.baggage.BaggageField;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
/**
* 集成 Sleuth 的 Transaction Filter
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TransactionFilter implements Filter {
@Autowired(required = false)
private Tracer tracer; // Sleuth 的 Tracer
private static final String HEADER_TRANSACTION_ID = "X-Transaction-Id";
private static final BaggageField TRANSACTION_ID_FIELD = BaggageField.create("transactionId");
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
// 1. 获取或生成 Transaction ID
String transactionId = httpRequest.getHeader(HEADER_TRANSACTION_ID);
if (transactionId == null) {
transactionId = UUID.randomUUID().toString();
}
// 2. 存入 MDC
MDC.put("transactionId", transactionId);
// 3. 存入 Sleuth Baggage(会自动透传到下游服务)
if (tracer != null) {
TRANSACTION_ID_FIELD.updateValue(transactionId);
// 可选:获取 Sleuth 的 Trace ID 和 Span ID
String traceId = tracer.currentSpan().context().traceIdString();
String spanId = tracer.currentSpan().context().spanIdString();
MDC.put("traceId", traceId);
MDC.put("spanId", spanId);
}
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
日志输出(包含 Sleuth 信息):
2025-11-19 14:32:15.123 [http-nio-8080-exec-1] INFO [dsj-clinical,a7b3d9e2,f47ac10b] [TID:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] c.z.d.patient.controller.PatientController - 查询患者信息
格式说明:[应用名,TraceId,SpanId]
4.2.2 OpenTelemetry 集成(云原生推荐)⚡
OpenTelemetry 是 CNCF 的标准可观测性框架,支持多语言、多后端。
添加依赖:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.32.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-spring-boot-starter</artifactId>
<version>1.32.0-alpha</version>
</dependency>
配置:
# application.yml
otel:
traces:
sampler:
probability: 1.0
exporter:
otlp:
endpoint: http://localhost:4317 # OTLP Collector 地址
service:
name: dsj-clinical
resource:
attributes:
environment: production
集成代码:
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
@Service
public class PatientService {
@Autowired
private Tracer tracer;
public PatientInfo getPatientInfo(String patientId) {
// 创建自定义 Span
Span span = tracer.spanBuilder("getPatientInfo")
.setParent(Context.current())
.startSpan();
try {
// 添加自定义属性
span.setAttribute("patient.id", patientId);
span.setAttribute("transaction.id", MDC.get("transactionId"));
// 业务逻辑...
PatientInfo info = patientMapper.selectById(patientId);
span.addEvent("患者信息加载完成");
return info;
} catch (Exception e) {
span.recordException(e);
throw e;
} finally {
span.end();
}
}
}
4.3 异步线程中的 MDC 传递
问题: 在使用 @Async、CompletableFuture、线程池等异步编程时,MDC 不会自动传递到子线程。
4.3.1 使用 TaskDecorator(推荐)✅
package com.zhyl.dsj.common.config;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.Map;
import java.util.concurrent.Executor;
/**
* 异步任务配置
*/
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-task-");
// 设置 TaskDecorator,自动传递 MDC
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
/**
* MDC 传递装饰器
*/
private static class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 保存当前线程的 MDC
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// 在子线程中恢复 MDC
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
runnable.run();
} finally {
// 清理 MDC
MDC.clear();
}
};
}
}
}
使用示例:
@Service
public class PatientService {
private static final Logger logger = LoggerFactory.getLogger(PatientService.class);
@Async
public void sendNotificationAsync(String patientId) {
// Transaction ID 会自动传递到异步线程
logger.info("发送异步通知: patientId={}", patientId);
// 异步业务逻辑...
}
}
4.3.2 使用 CompletableFuture 传递 MDC
package com.zhyl.dsj.common.util;
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
/**
* 支持 MDC 传递的 CompletableFuture 工具类
*/
public class MdcCompletableFuture {
/**
* 创建带 MDC 的异步任务
*/
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier, Executor executor) {
// 保存当前线程的 MDC
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return CompletableFuture.supplyAsync(() -> {
try {
// 在子线程中恢复 MDC
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
return supplier.get();
} finally {
// 清理 MDC
MDC.clear();
}
}, executor);
}
}
使用示例:
@Service
public class PatientService {
@Autowired
@Qualifier("taskExecutor")
private Executor executor;
public void processPatientDataAsync(String patientId) {
// 使用自定义的 MdcCompletableFuture
MdcCompletableFuture.supplyAsync(() -> {
logger.info("异步处理患者数据: patientId={}", patientId);
// Transaction ID 会自动传递
return patientMapper.selectById(patientId);
}, executor)
.thenAccept(data -> {
logger.info("数据处理完成: {}", data);
})
.exceptionally(ex -> {
logger.error("数据处理失败", ex);
return null;
});
}
}
4.3.3 使用 TransmittableThreadLocal(终极方案)⚡
阿里巴巴开源的 TTL(TransmittableThreadLocal)可以自动在线程池中传递 ThreadLocal。
添加依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.3</version>
</dependency>
配置:
import com.alibaba.ttl.threadpool.TtlExecutors;
import java.util.concurrent.Executor;
@Configuration
public class ThreadPoolConfig {
@Bean
public Executor ttlTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setThreadNamePrefix("ttl-task-");
executor.initialize();
// 使用 TTL 包装,自动传递 ThreadLocal
return TtlExecutors.getTtlExecutor(executor.getThreadPoolExecutor());
}
}
优点:
- ✅ 无需手动传递 MDC
- ✅ 支持所有 ThreadLocal(不仅限于MDC)
- ✅ 支持线程池、ForkJoinPool 等多种场景
4.4 性能优化建议
4.4.1 使用异步日志
<!-- logback-spring.xml -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>true</neverBlock>
<appender-ref ref="FILE_ALL"/>
</appender>
4.4.2 日志采样(高流量场景)
@Component
public class TransactionFilter implements Filter {
private static final double SAMPLING_RATE = 0.1; // 10% 采样率
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 根据采样率决定是否记录详细日志
boolean shouldSample = Math.random() < SAMPLING_RATE;
MDC.put("sampled", String.valueOf(shouldSample));
// ...
}
}
4.4.3 避免过度日志
// ❌ 不推荐:在循环中记录日志
for (Patient patient : patients) {
logger.debug("处理患者: {}", patient.getId()); // 高频日志
}
// ✅ 推荐:聚合后记录
logger.info("批量处理患者: 共{}个", patients.size());
5. 完整示例:查询患者信息完整链路
5.1 前端代码
// 用户点击"查询患者信息"按钮
import axios, { startTransaction, endTransaction } from '@/plugins/axios';
async function loadPatientData(patientId) {
const tid = startTransaction('LOAD_PATIENT_DATA');
console.log(`[前端] 事务开始: ${tid}`);
try {
const response = await axios.post('/api/patient/info', { patientId });
console.log(`[前端] 事务成功: ${tid}`);
return response;
} catch (error) {
console.error(`[前端] 事务失败: ${tid}`, error);
throw error;
} finally {
endTransaction();
}
}
5.2 后端代码
// Controller
@RestController
@RequestMapping("/api/patient")
public class PatientController {
@PostMapping("/info")
public Result<PatientInfo> getPatientInfo(@RequestBody PatientQuery query) {
logger.info("查询患者信息: patientId={}", query.getPatientId());
// Transaction ID 自动包含在日志中
PatientInfo info = patientService.getPatientInfo(query.getPatientId());
return Result.success(info).withTransactionId(MDC.get("transactionId"));
}
}
// Service
@Service
public class PatientService {
public PatientInfo getPatientInfo(String patientId) {
logger.info("从数据库加载患者数据: patientId={}", patientId);
// Transaction ID 自动包含在日志中
// 调用下游服务(Transaction ID 自动透传)
EmpiData empiData = empiServiceClient.getEmpiData(patientId);
return buildPatientInfo(patientId, empiData);
}
}
5.3 日志输出示例
# 前端日志(浏览器控制台)
[Transaction Started] a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d - LOAD_PATIENT_DATA
[API Success] POST /api/patient/info transactionId=a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d requestId=f47ac10b-58cc-4372-a567-0e02b2c3d479 duration=666ms
[Transaction Ended] a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d
# 后端日志(dsj-clinical-all.log)
2025-11-19 14:32:15.123 [http-nio-8080-exec-1] INFO [TID:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RID:f47ac10b-58cc-4372-a567-0e02b2c3d479] c.z.d.common.filter.TransactionFilter - Request started: POST /api/patient/info from 192.168.1.100
2025-11-19 14:32:15.145 [http-nio-8080-exec-1] INFO [TID:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RID:f47ac10b-58cc-4372-a567-0e02b2c3d479] c.z.d.patient.controller.PatientController - 查询患者信息: patientId=12345
2025-11-19 14:32:15.167 [http-nio-8080-exec-1] INFO [TID:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RID:f47ac10b-58cc-4372-a567-0e02b2c3d479] c.z.d.patient.service.PatientService - 从数据库加载患者数据: patientId=12345
2025-11-19 14:32:15.789 [http-nio-8080-exec-1] INFO [TID:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RID:f47ac10b-58cc-4372-a567-0e02b2c3d479] c.z.d.common.filter.TransactionFilter - Request completed: POST /api/patient/info - Status: 200 - Duration: 666ms
# EMPI 服务日志(下游服务)
2025-11-19 14:32:15.200 [http-nio-8081-exec-5] INFO [TID:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d] [RID:b8c4e21d-69dd-5483-c678-1f3g4b5c6d7e] c.z.empi.service.EmpiService - 查询EMPI数据: patientId=12345
关键点: 通过 TID:a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d 可以快速检索到整个调用链的所有日志。
6. 故障排查实战案例
场景:用户反馈"查询患者信息失败"
步骤 1:获取 Transaction ID
- 用户提供截图,显示错误提示:"操作失败,事务ID: a7b3d9e2..."
步骤 2:检索日志
# 在日志文件中搜索
grep "a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d" dsj-clinical-all.log
# 或使用 ELK
GET /logs/_search
{
"query": {
"match": { "transactionId": "a7b3d9e2-4f1c-4a5b-8c9d-1e2f3a4b5c6d" }
}
}
步骤 3:分析日志
14:32:15.123 - Request started
14:32:15.145 - 查询患者信息
14:32:15.167 - 从数据库加载患者数据
14:32:15.200 - [EMPI服务] 查询EMPI数据
14:32:18.400 - [EMPI服务] ERROR: 数据库连接超时 ❌ 问题定位!
14:32:18.450 - Request failed: 系统错误
结论: EMPI 服务的数据库连接超时,导致整个请求失败。定位时间:< 5分钟 ✅
7. 总结与最佳实践
7.1 核心要点
- ✅ 前端:使用 uuid 库生成 Transaction ID,通过 HTTP Header 传递
- ✅ 后端:使用 Filter + MDC 实现 Transaction ID 的接收和传递
- ✅ 日志:使用 Logback 配置,在日志格式中包含 Transaction ID
- ✅ 透传:使用 RestTemplate/Feign 拦截器自动透传到下游服务
- ✅ 异步:使用 TaskDecorator 或 TTL 在异步线程中传递 MDC
7.2 命名规范
- HTTP Header:
X-Transaction-Id或X-Trace-Id - MDC Key:
transactionId - 变量名:
transactionId或traceId
7.3 注意事项
- ⚠️ 必须在 Filter 的 finally 块中清理 MDC,避免内存泄漏
- ⚠️ 异步场景必须显式传递 MDC,否则子线程获取不到
- ⚠️ 高流量场景考虑采样,避免日志爆炸
- ⚠️ 敏感信息不要记录到日志,即使有 Transaction ID
7.4 进阶扩展
- 🚀 集成 Zipkin/Jaeger 实现分布式追踪可视化
- 🚀 集成 ELK/Splunk 实现日志智能分析和告警
- 🚀 集成 Prometheus + Grafana 实现性能监控
- 🚀 使用 OpenTelemetry 实现云原生可观测性
附录
相关资源
- UUID RFC 4122: datatracker.ietf.org/doc/html/rf…
- Spring Cloud Sleuth: spring.io/projects/sp…
- OpenTelemetry: opentelemetry.io/
- Logback 官方文档: logback.qos.ch/
- 浏览器兼容性: caniuse.com