Java基础架构设计(三)| 通用响应与异常处理(分布式应用通用方案)

Java基础架构设计(三)| 通用响应与异常处理(分布式应用通用方案)

  • 一、前置说明
    • [1.1 核心定位](#1.1 核心定位)
    • [1.2 术语说明](#1.2 术语说明)
  • 二、版本兼容&依赖总表(分布式扩展依赖)
    • [2.1 核心依赖清单(在单体基础上新增)](#2.1 核心依赖清单(在单体基础上新增))
    • [2.2 版本兼容说明(关键依赖对应关系)](#2.2 版本兼容说明(关键依赖对应关系))
  • 三、分布式项目核心实现(单体基础上叠加)
    • 场景1:分布式链路追踪(跨服务关联核心)
      • [1. 网关配置(GatewayConfig.java,仅网关服务)](#1. 网关配置(GatewayConfig.java,仅网关服务))
      • [2. 网关JWT解析过滤器(GatewayJwtFilter.java,仅网关服务)](#2. 网关JWT解析过滤器(GatewayJwtFilter.java,仅网关服务))
      • [3. Feign 拦截器(FeignTraceInterceptor.java,所有微服务需添加)](#3. Feign 拦截器(FeignTraceInterceptor.java,所有微服务需添加))
      • [4. 微服务上下文接收拦截器(MicroServiceTraceInterceptor.java,所有微服务)](#4. 微服务上下文接收拦截器(MicroServiceTraceInterceptor.java,所有微服务))
      • [5. 微服务Web配置(WebMvcConfig.java,所有微服务)](#5. 微服务Web配置(WebMvcConfig.java,所有微服务))
      • [6. 微服务链路追踪配置(application.yml,所有微服务)](#6. 微服务链路追踪配置(application.yml,所有微服务))
      • [7. Zipkin 服务器部署(Docker 方式,支持ES持久化)](#7. Zipkin 服务器部署(Docker 方式,支持ES持久化))
      • [8. 微服务启动类注解(所有微服务)](#8. 微服务启动类注解(所有微服务))
    • 场景2:分布式日志集中收集(跨服务日志核心)
      • [1. ELK 快速部署(docker-compose-elk.yml)](#1. ELK 快速部署(docker-compose-elk.yml))
      • [2. Logstash 配置(logstash.conf)](#2. Logstash 配置(logstash.conf))
      • [3. Elasticsearch 索引模板(优化检索效率)](#3. Elasticsearch 索引模板(优化检索效率))
      • [4. 微服务 Logback 配置(logback-spring.xml,替换单体的本地入库配置)](#4. 微服务 Logback 配置(logback-spring.xml,替换单体的本地入库配置))
      • [5. 微服务配置文件(application.yml,新增 Logstash 地址)](#5. 微服务配置文件(application.yml,新增 Logstash 地址))
    • 场景3:跨服务异常统一响应(跨服务协作基础)
      • [1. Feign 异常拦截器(FeignExceptionInterceptor.java,所有微服务)](#1. Feign 异常拦截器(FeignExceptionInterceptor.java,所有微服务))
      • [2. 网关全局异常处理(GatewayExceptionHandler.java,仅网关服务)](#2. 网关全局异常处理(GatewayExceptionHandler.java,仅网关服务))
      • [3. 微服务 Feign 客户端示例(OrderFeignClient.java)](#3. 微服务 Feign 客户端示例(OrderFeignClient.java))
    • 场景4:熔断降级(生产环境必备)
      • [1. Sentinel 控制台部署(Docker 方式)](#1. Sentinel 控制台部署(Docker 方式))
      • [2. 网关熔断配置(application.yml,仅网关服务)](#2. 网关熔断配置(application.yml,仅网关服务))
      • [3. 微服务熔断配置(application.yml,所有微服务)](#3. 微服务熔断配置(application.yml,所有微服务))
      • [4. 微服务熔断注解示例(StockService.java)](#4. 微服务熔断注解示例(StockService.java))
      • [5. Sentinel 熔断规则配置(通过控制台)](#5. Sentinel 熔断规则配置(通过控制台))
    • 场景5:服务依赖与链路监控(可选优化)
      • [1. Zipkin 链路告警(通过 Kibana 配置)](#1. Zipkin 链路告警(通过 Kibana 配置))
      • [2. ELK 索引生命周期管理(避免磁盘占满)](#2. ELK 索引生命周期管理(避免磁盘占满))
  • 四、分布式项目整合验证(跨服务流程闭环)
    • [1. 验证流程](#1. 验证流程)
    • [2. 校验清单](#2. 校验清单)
  • 五、分布式项目部署指南(生产级配置)
    • [1. 生产必查项](#1. 生产必查项)
    • [2. 常见问题排查(生产环境)](#2. 常见问题排查(生产环境))

一、前置说明

1.1 核心定位

  • 分布式项目:100%复用单体项目核心代码(响应统一、异常收口、链路追踪基础、双日志表设计),仅新增跨服务专属能力,无重构成本
  • 核心价值:解决分布式场景下「跨服务链路断裂、日志分散、异常格式混乱、服务依赖不可视」四大痛点,实现"单体标准化+分布式可追溯"
  • 架构适配:支持 Spring Cloud 微服务架构(Nacos 服务发现+Feign 远程调用+Gateway 网关),兼容 JDK1.8 + Spring Boot2.7.6 + MySQL8.0

1.2 术语说明

  • 网关透传:网关路由请求时,保留前端/traceId/operatorId 等上下文字段,传递给下游服务
  • Feign 拦截器:远程调用时,自动将当前服务的 traceId/bizId/operatorId 注入请求头,确保跨服务上下文一致
  • 链路可视化:通过 Zipkin 展示跨服务调用链路(如订单服务→支付服务→库存服务),包含每个节点耗时
  • 日志集中收集:所有微服务日志统一输出到 ELK 栈,支持按 traceId/bizId/服务名检索全链路日志
  • 服务注册发现:通过 Nacos 实现微服务注册与发现,Feign 基于服务名实现无感知远程调用
  • 熔断降级:通过 Sentinel 限制故障服务的调用流量,避免服务雪崩(生产环境必备)

二、版本兼容&依赖总表(分布式扩展依赖)

2.1 核心依赖清单(在单体基础上新增)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <!-- 继承单体项目依赖(或直接复用单体pom.xml) -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>monomer-core-demo</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>distributed-core-demo</artifactId>
    <name>distributed-core-demo</name>
    <description>分布式项目扩展方案示例</description>

    <properties>
        <!-- Spring Cloud 版本(适配 Spring Boot2.7.6) -->
        <spring-cloud.version>2021.0.4</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
        <zipkin.version>2.24.3</zipkin.version>
        <sentinel.version>1.8.6</sentinel.version>
    </properties>

    <!-- 分布式依赖版本锁定(避免冲突) -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 1. 服务注册发现(Nacos) -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- 2. 远程调用(Feign) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- 3. 分布式链路追踪(Sleuth+Zipkin) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-sleuth-zipkin</artifactId>
        </dependency>

        <!-- 4. 网关(Spring Cloud Gateway)- 仅网关服务需添加 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <!-- 排除Tomcat依赖,Gateway基于Netty -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 5. 分布式配置中心(Nacos)- 统一管理配置 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!-- 6. 日志集中收集(Logstash编码器) -->
        <dependency>
            <groupId>net.logstash.logback</groupId>
            <artifactId>logstash-logback-encoder</artifactId>
            <version>7.4</version>
        </dependency>

        <!-- 7. 熔断降级(Sentinel)- 生产环境必备 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.7.6</version>
            </plugin>
        </build>
</project>

2.2 版本兼容说明(关键依赖对应关系)

核心依赖 版本 适配说明
Spring Boot 2.7.6 分布式扩展的基础版本
Spring Cloud 2021.0.4 与 Spring Boot2.7.x 兼容
Spring Cloud Alibaba 2021.0.4.0 包含 Nacos 服务发现/配置中心、Sentinel
Spring Cloud Sleuth 3.1.9 分布式链路追踪核心,与 Sleuth 适配
Zipkin 2.24.3 链路可视化平台,支持 Sleuth 数据上报
Logstash-encoder 7.4 日志 JSON 格式化,适配 ELK 收集
Nacos 2.1.2 服务注册发现+配置中心,兼容 Spring Cloud
Sentinel 1.8.6 熔断降级,与 Spring Cloud Alibaba 适配
Elasticsearch/Kibana/Logstash 7.17.0 ELK 栈统一版本,确保兼容性

三、分布式项目核心实现(单体基础上叠加)

场景1:分布式链路追踪(跨服务关联核心)

  • 核心实现:网关透传+Feign 拦截器+微服务接收拦截器+Sleuth+Zipkin,复用单体 ThreadLocalUtils
  • 依赖说明:依赖单体的 ThreadLocalUtils 和链路追踪基础,新增 Spring Cloud 相关依赖

1. 网关配置(GatewayConfig.java,仅网关服务)

java 复制代码
package com.example.distributed.gateway.config;

import com.example.monomer.core.context.ThreadLocalUtils;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 网关全局过滤器(透传 traceId/operatorId,生成全局唯一 traceId)
 */
@Configuration
public class GatewayConfig {

    // 上下文传递请求头(与微服务约定)
    private static final String TRACE_ID_HEADER = "X-Trace-Id";
    private static final String OPERATOR_ID_HEADER = "X-Operator-Id";

    @Bean
    @Order(-1) // 最高优先级,确保在路由前执行
    public GlobalFilter traceIdGlobalFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpRequest.Builder requestBuilder = request.mutate();

            // 1. 处理 traceId:优先获取前端传递的,无则生成
            String traceId = request.getHeaders().getFirst(TRACE_ID_HEADER);
            if (traceId == null || traceId.trim().isEmpty()) {
                traceId = ThreadLocalUtils.generateTraceId();
            }
            // 注入请求头,传递给下游服务
            requestBuilder.header(TRACE_ID_HEADER, traceId);

            // 2. 处理 operatorId:前端/网关JWT解析后传递(默认anonymous)
            String operatorId = request.getHeaders().getFirst(OPERATOR_ID_HEADER);
            if (operatorId == null || operatorId.trim().isEmpty()) {
                operatorId = "anonymous";
            }
            requestBuilder.header(OPERATOR_ID_HEADER, operatorId);

            // 3. 将 traceId 存入响应头,便于前端关联
            exchange.getResponse().getHeaders().add(TRACE_ID_HEADER, traceId);

            return chain.filter(exchange.mutate().request(requestBuilder.build()).build())
                    .then(Mono.fromRunnable(() -> {
                        // 清理上下文(网关为响应式,避免内存泄漏)
                        ThreadLocalUtils.removeAll();
                    }));
        };
    }
}

2. 网关JWT解析过滤器(GatewayJwtFilter.java,仅网关服务)

java 复制代码
package com.example.distributed.gateway.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

/**
 * 网关JWT解析过滤器(统一解析token,提取operatorId)
 */
@Configuration
public class GatewayJwtFilter {
    @Value("${app.jwt.secret}")
    private String jwtSecret;
    private static final String JWT_HEADER = "Authorization";
    private static final String OPERATOR_ID_HEADER = "X-Operator-Id";

    @Bean
    @Order(-2) // 优先级低于traceId过滤器,确保先生成traceId
    public GlobalFilter jwtGlobalFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            ServerHttpRequest.Builder requestBuilder = request.mutate();

            // 解析JWT令牌
            String token = request.getHeaders().getFirst(JWT_HEADER);
            if (token != null && token.startsWith("Bearer ")) {
                try {
                    Claims claims = Jwts.parser()
                            .setSigningKey(jwtSecret.getBytes(StandardCharsets.UTF_8))
                            .parseClaimsJws(token.substring(7))
                            .getBody();
                    String operatorId = claims.get("userId", String.class);
                    if (operatorId != null) {
                        requestBuilder.header(OPERATOR_ID_HEADER, operatorId);
                    }
                } catch (Exception e) {
                    // token无效,设为anonymous
                    requestBuilder.header(OPERATOR_ID_HEADER, "anonymous");
                }
            } else {
                // 无token,设为anonymous
                requestBuilder.header(OPERATOR_ID_HEADER, "anonymous");
            }

            return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
        };
    }
}

3. Feign 拦截器(FeignTraceInterceptor.java,所有微服务需添加)

java 复制代码
package com.example.distributed.core.interceptor;

import com.example.monomer.core.context.ThreadLocalUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Feign 远程调用拦截器(传递 traceId/bizId/operatorId 上下文)
 */
@Configuration
public class FeignTraceInterceptor {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";
    private static final String BIZ_ID_HEADER = "X-Biz-Id";
    private static final String OPERATOR_ID_HEADER = "X-Operator-Id";

    @Bean
    public RequestInterceptor requestInterceptor() {
        return template -> {
            // 1. 传递 traceId(从 ThreadLocal 获取,网关生成或上游服务传递)
            String traceId = ThreadLocalUtils.getTraceId();
            if (traceId != null && !"UNKNOWN".equals(traceId)) {
                template.header(TRACE_ID_HEADER, traceId);
            }

            // 2. 传递 bizId(业务ID,如订单号)
            String bizId = ThreadLocalUtils.getBizId();
            if (bizId != null && !"UNKNOWN".equals(bizId)) {
                template.header(BIZ_ID_HEADER, bizId);
            }

            // 3. 传递 operatorId(操作人ID)
            String operatorId = ThreadLocalUtils.getOperatorId();
            template.header(OPERATOR_ID_HEADER, operatorId);
        };
    }
}

4. 微服务上下文接收拦截器(MicroServiceTraceInterceptor.java,所有微服务)

java 复制代码
package com.example.distributed.core.interceptor;

import com.example.monomer.core.context.ThreadLocalUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 微服务请求拦截器(接收网关/Feign传递的上下文头,更新ThreadLocal)
 */
@Component
public class MicroServiceTraceInterceptor implements HandlerInterceptor {
    private static final String TRACE_ID_HEADER = "X-Trace-Id";
    private static final String BIZ_ID_HEADER = "X-Biz-Id";
    private static final String OPERATOR_ID_HEADER = "X-Operator-Id";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 接收并设置traceId(网关生成或上游服务传递)
        String traceId = request.getHeader(TRACE_ID_HEADER);
        if (traceId != null && !traceId.trim().isEmpty()) {
            ThreadLocalUtils.setTraceId(traceId);
        }

        // 接收并设置bizId(上游服务传递的业务ID)
        String bizId = request.getHeader(BIZ_ID_HEADER);
        if (bizId != null && !bizId.trim().isEmpty()) {
            ThreadLocalUtils.setBizId(bizId);
        }

        // 接收并设置operatorId(网关解析或上游传递)
        String operatorId = request.getHeader(OPERATOR_ID_HEADER);
        if (operatorId != null && !operatorId.trim().isEmpty()) {
            ThreadLocalUtils.setOperatorId(operatorId);
        }

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ThreadLocalUtils.removeAll(); // 清理上下文,避免内存泄漏
    }
}

5. 微服务Web配置(WebMvcConfig.java,所有微服务)

java 复制代码
package com.example.distributed.core.config;

import com.example.distributed.core.interceptor.MicroServiceTraceInterceptor;
import com.example.monomer.core.interceptor.TraceIdInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 微服务Web配置(注册拦截器)
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 1. 注册微服务上下文接收拦截器(优先级更高)
        registry.addInterceptor(new MicroServiceTraceInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/error", "/actuator/**");

        // 2. 注册单体链路追踪切面拦截器
        registry.addInterceptor(new TraceIdInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/error", "/swagger-ui/**", "/api-docs/**");
    }
}

6. 微服务链路追踪配置(application.yml,所有微服务)

yaml 复制代码
spring:
  application:
    name: order-service # 每个服务名称唯一(如pay-service、stock-service)
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos 服务地址
      config:
        server-addr: localhost:8848 # Nacos 配置中心地址
        file-extension: yml
        shared-configs:
          - data-id: spring-cloud-sleuth.properties
            group: DEFAULT_GROUP
            refresh: true # 支持动态刷新采样率
    sleuth:
      sampler:
        probability: 0.1 # 生产环境采样率(0.1=10%,降低性能损耗)
        rate: 1000 # 每秒最大采样数
      baggage:
        remote-fields: traceId,bizId,operatorId # 跨服务传递的字段
        correlation-fields: traceId,bizId,operatorId # 与 MDC 关联的字段
  zipkin:
    base-url: http://localhost:9411 # Zipkin 服务器地址
    sender:
      type: web # 开发环境用HTTP上报,生产环境建议改为kafka
    service:
      name: ${spring.application.name} # Zipkin 中显示的服务名

# 应用自定义配置
app:
  jwt:
    secret: ${JWT_SECRET:your-jwt-secret-key-32bytes} # 从环境变量注入

# 复用单体的 ThreadLocalUtils,无需修改

7. Zipkin 服务器部署(Docker 方式,支持ES持久化)

bash 复制代码
# 1. 确保Elasticsearch已启动(与Zipkin在同一网络)
# 2. 启动Zipkin容器(数据持久化到ES,避免重启丢失)
docker run -d -p 9411:9411 \
--network elk-network \
-e STORAGE_TYPE=elasticsearch \
-e ES_HOSTS=http://elasticsearch:9200 \
-e ES_USERNAME=elastic \
-e ES_PASSWORD=elastic123 \
openzipkin/zipkin:2.24.3

8. 微服务启动类注解(所有微服务)

java 复制代码
package com.example.distributed.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * 订单服务启动类(其他服务类似,仅名称不同)
 */
@SpringBootApplication(scanBasePackages = "com.example") // 扫描单体核心包+分布式扩展包
@EnableDiscoveryClient // 启用 Nacos 服务发现
@EnableFeignClients(basePackages = "com.example.distributed.feign") // 扫描 Feign 客户端
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

验证方式

  1. 启动 Nacos(端口8848)、Elasticsearch(端口9200)、Zipkin(端口9411);
  2. 启动订单服务、支付服务、库存服务;
  3. 前端通过网关调用订单服务,订单服务通过 Feign 调用支付服务和库存服务;
  4. 访问 Zipkin 界面(http://localhost:9411),搜索 traceId,验证跨服务链路完整(订单→支付→库存);
  5. 查看三个服务的日志,验证 traceId 完全一致。

场景2:分布式日志集中收集(跨服务日志核心)

  • 核心实现:ELK 容器部署+Logback 配置 JSON 输出+TCP 发送到 Logstash+ES 索引模板
  • 依赖说明:依赖场景1的跨服务 traceId,新增 Logstash-encoder 依赖

1. ELK 快速部署(docker-compose-elk.yml)

yaml 复制代码
version: '3.8'
services:
  # Elasticsearch:存储日志
  elasticsearch:
    image: elasticsearch:7.17.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g" # 内存配置,根据服务器调整
      - "ELASTIC_PASSWORD=elastic123" # 密码
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    networks:
      - elk-network
    restart: always

  # Logstash:日志收集与解析
  logstash:
    image: logstash:7.17.0
    container_name: logstash
    depends_on:
      - elasticsearch
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    environment:
      - "LS_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "5044:5044" # TCP 端口,接收微服务日志
      - "9600:9600"
    networks:
      - elk-network
    restart: always

  # Kibana:日志可视化查询
  kibana:
    image: kibana:7.17.0
    container_name: kibana
    depends_on:
      - elasticsearch
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - ELASTICSEARCH_USERNAME=elastic
      - ELASTICSEARCH_PASSWORD=elastic123
    ports:
      - "5601:5601"
    networks:
      - elk-network
    restart: always

networks:
  elk-network:
    driver: bridge

volumes:
  es-data:

2. Logstash 配置(logstash.conf)

conf 复制代码
input {
  # 接收微服务 TCP 日志(JSON 格式)
  tcp {
    port => 5044
    codec => json_lines
  }
}

filter {
  # 解析时间字段(与微服务日志格式对齐)
  date {
    match => ["timestamp", "yyyy-MM-dd HH:mm:ss.SSS"]
    target => "@timestamp"
  }

  # 提取服务名(从日志的 appName 字段)
  mutate {
    rename => { "[appName]" => "serviceName" }
    remove_field => ["@version", "host"] # 移除冗余字段
  }
}

output {
  # 输出到 Elasticsearch
  elasticsearch {
    hosts => ["http://elasticsearch:9200"]
    user => "elastic"
    password => "elastic123"
    index => "distributed-log-%{+YYYY.MM.dd}" # 按日期分索引
    document_type => "_doc"
  }

  # 开发环境输出到控制台(调试用)
  stdout {
    codec => rubydebug
  }
}

3. Elasticsearch 索引模板(优化检索效率)

bash 复制代码
# 执行curl命令创建索引模板(定义字段类型,避免自动映射异常)
curl -X PUT "http://localhost:9200/_index_template/distributed-log-template" -H "Content-Type: application/json" -u elastic:elastic123 -d '{
  "index_patterns": ["distributed-log-*"],
  "template": {
    "mappings": {
      "properties": {
        "traceId": {"type": "keyword"},
        "bizId": {"type": "keyword"},
        "operatorId": {"type": "keyword"},
        "serviceName": {"type": "keyword"},
        "level": {"type": "keyword"},
        "message": {"type": "text"},
        "exception": {"type": "text"},
        "timestamp": {"type": "date", "format": "yyyy-MM-dd HH:mm:ss.SSS"},
        "environment": {"type": "keyword"}
      }
    },
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  },
  "priority": 10
}'

4. 微服务 Logback 配置(logback-spring.xml,替换单体的本地入库配置)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
  <!-- 上下文参数 -->
  <contextName>${spring.application.name}</contextName>
  <springProperty scope="context" name="appName" source="spring.application.name"/>
  <springProperty scope="context" name="logstashHost" source="logstash.host" defaultValue="localhost"/>
  <springProperty scope="context" name="logstashPort" source="logstash.port" defaultValue="5044"/>
  <springProperty scope="context" name="environment" source="spring.profiles.active" defaultValue="prod"/>

  <!-- 日志格式(JSON 格式,包含所有核心字段) -->
  <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
    <destination>${logstashHost}:${logstashPort}</destination>
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <!-- 自定义字段:服务名、环境、日志类型 -->
      <customFields>{"appName":"${appName}","environment":"${environment}","logType":"distributed_log"}</customFields>
      <!-- 包含 MDC 字段(traceId/operatorId/bizId) -->
      <includeMdcKeyName>traceId</includeMdcKeyName>
      <includeMdcKeyName>operatorId</includeMdcKeyName>
      <includeMdcKeyName>bizId</includeMdcKeyName>
      <!-- 时间戳格式 -->
      <timestampPattern>yyyy-MM-dd HH:mm:ss.SSS</timestampPattern>
    </encoder>
    <!-- 重连配置 -->
    <reconnectionDelay>10000</reconnectionDelay>
    <queueSize>1024</queueSize>
  </appender>

  <!-- 控制台输出(开发环境) -->
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId:-NA}] [%X{bizId:-NA}] [%p] %logger{50} - %msg%n</pattern>
      <charset>UTF-8</charset>
    </encoder>
  </appender>

  <!-- 屏蔽第三方冗余日志 -->
  <logger name="org.springframework" level="WARN"/>
  <logger name="com.alibaba.nacos" level="WARN"/>
  <logger name="feign" level="WARN"/>
  <logger name="zipkin2" level="WARN"/>
  <logger name="com.alibaba.csp.sentinel" level="WARN"/>

  <!-- 根日志配置 -->
  <root level="INFO">
    <appender-ref ref="LOGSTASH"/> <!-- 发送到 ELK -->
    <springProfile name="dev">
      <appender-ref ref="CONSOLE"/> <!-- 开发环境启用控制台 -->
    </springProfile>
  </root>
</configuration>

5. 微服务配置文件(application.yml,新增 Logstash 地址)

yaml 复制代码
# 日志集中收集配置
logstash:
  host: localhost # 生产环境改为 ELK 服务器地址
  port: 5044

# 关闭单体的本地日志入库(分布式用 ELK 集中存储)
logging:
  level:
    root: INFO
    com.example: DEBUG
  config: classpath:logback-spring.xml

# ELK 索引生命周期管理(通过 Kibana 配置,保留7天)

验证方式:

  1. 启动 ELK 容器(docker-compose -f docker-compose-elk.yml up -d);
  2. 启动所有微服务,发起跨服务调用;
  3. 访问 Kibana 界面(http://localhost:5601),登录(用户名elastic,密码elastic123);
  4. 创建索引模式(distributed-log-*),关联时间字段@timestamp;
  5. 按 traceId 检索,验证能查询到所有服务的日志(订单+支付+库存);
  6. 按 serviceName 筛选,验证能单独查看某个服务的日志。

场景3:跨服务异常统一响应(跨服务协作基础)

  • 核心实现:Feign 异常拦截器(转换远程异常格式)+ 网关全局异常处理
  • 依赖说明:依赖单体的 R 响应类和 BizException 异常类

1. Feign 异常拦截器(FeignExceptionInterceptor.java,所有微服务)

java 复制代码
package com.example.distributed.core.interceptor;

import com.example.monomer.core.enums.BizErrorCodeEnum;
import com.example.monomer.core.exception.BizException;
import com.example.monomer.core.vo.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
 * Feign 异常解码器(将远程服务的异常响应转换为本地 R<T> 格式)
 */
@Slf4j
@Configuration
public class FeignExceptionInterceptor {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Bean
    public ErrorDecoder errorDecoder() {
        return (methodKey, response) -> {
            try {
                // 读取远程服务的异常响应体(JSON 格式)
                String responseBody = Util.toString(response.body().asReader(Util.UTF_8));
                log.warn("Feign 远程调用异常:methodKey={},status={},response={}",
                        methodKey, response.status(), responseBody);

                // 解析为 R<T> 对象
                R<?> errorResponse = objectMapper.readValue(responseBody, R.class);
                // 匹配错误码枚举,抛出本地 BizException
                BizErrorCodeEnum errorEnum = BizErrorCodeEnum.valueOf(errorResponse.getCode());
                return new BizException(errorEnum, errorResponse.getMessage());
            } catch (IOException | IllegalArgumentException e) {
                log.error("Feign 异常解析失败", e);
                // 解析失败时返回系统异常
                return new BizException(BizErrorCodeEnum.SYSTEM_ERROR, "远程服务调用失败");
            }
        };
    }
}

2. 网关全局异常处理(GatewayExceptionHandler.java,仅网关服务)

java 复制代码
package com.example.distributed.gateway.config;

import com.example.monomer.core.enums.BizErrorCodeEnum;
import com.example.monomer.core.vo.R;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 网关全局异常处理器(路由失败、超时等异常标准化)
 */
@Slf4j
@RestControllerAdvice
public class GatewayExceptionHandler {

    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 处理服务未找到异常(如服务未注册)
     */
    @ExceptionHandler(NotFoundException.class)
    public Mono<Void> handleNotFoundException(NotFoundException e, ServerWebExchange exchange) {
        return buildErrorResponse(exchange, HttpStatus.NOT_FOUND,
                BizErrorCodeEnum.RESOURCE_NOT_FOUND.getCode(), "服务暂不可用");
    }

    /**
     * 处理路由超时异常
     */
    @ExceptionHandler(ResponseStatusException.class)
    public Mono<Void> handleResponseStatusException(ResponseStatusException e, ServerWebExchange exchange) {
        if (e.getStatus() == HttpStatus.GATEWAY_TIMEOUT) {
            return buildErrorResponse(exchange, HttpStatus.GATEWAY_TIMEOUT,
                    BizErrorCodeEnum.SYSTEM_ERROR.getCode(), "服务响应超时");
        }
        return buildErrorResponse(exchange, e.getStatus(),
                BizErrorCodeEnum.SYSTEM_ERROR.getCode(), e.getReason());
    }

    /**
     * 处理系统异常
     */
    @ExceptionHandler(Exception.class)
    public Mono<Void> handleException(Exception e, ServerWebExchange exchange) {
        log.error("网关系统异常", e);
        return buildErrorResponse(exchange, HttpStatus.INTERNAL_SERVER_ERROR,
                BizErrorCodeEnum.SYSTEM_ERROR.getCode(), "网关繁忙,请稍后再试");
    }

    /**
     * 构建标准化异常响应(R<T> 格式)
     */
    private Mono<Void> buildErrorResponse(ServerWebExchange exchange, HttpStatus httpStatus, int code, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

        // 构建 R<T> 异常响应
        R<Void> errorResponse = R.fail(code, message);
        try {
            byte[] bytes = objectMapper.writeValueAsBytes(errorResponse);
            DataBuffer buffer = response.bufferFactory().wrap(bytes);
            return response.writeWith(Mono.just(buffer));
        } catch (JsonProcessingException e) {
            log.error("网关异常响应序列化失败", e);
            return response.setComplete();
        }
    }
}

3. 微服务 Feign 客户端示例(OrderFeignClient.java)

java 复制代码
package com.example.distributed.feign;

import com.example.distributed.core.interceptor.FeignExceptionInterceptor;
import com.example.monomer.core.vo.R;
import com.example.monomer.entity.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * 订单服务 Feign 客户端(支付服务调用订单服务)
 */
@FeignClient(
        name = "order-service", // 服务名(Nacos 注册的订单服务名称)
        configuration = FeignExceptionInterceptor.class // 绑定异常拦截器
)
public interface OrderFeignClient {

    @GetMapping("/api/order/getById")
    R<Order> getOrderById(@RequestParam("id") Long id);
}

验证方式:

  1. 模拟支付服务调用订单服务时,订单服务抛出业务异常(如订单不存在);
  2. 查看支付服务的响应,验证异常格式为标准化 R 格式(code=610,message=订单不存在);
  3. 模拟网关路由到未启动的服务,验证网关返回 R 格式异常(code=404,message=服务暂不可用);
  4. 前端接收所有跨服务异常,无需区分"本地异常"和"远程异常"。

场景4:熔断降级(生产环境必备)

  • 核心实现:Sentinel 控制台+网关熔断+微服务熔断
  • 依赖说明:依赖 Spring Cloud Alibaba Sentinel 相关依赖

1. Sentinel 控制台部署(Docker 方式)

bash 复制代码
# 启动 Sentinel 控制台(默认端口8080,账号密码:sentinel/sentinel)
docker run -d -p 8080:8080 sentinelhub/sentinel-dashboard:1.8.6

2. 网关熔断配置(application.yml,仅网关服务)

yaml 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080 # Sentinel控制台地址
        port: 8719 # 客户端端口(默认8719,冲突时自动递增)
      scg:
        fallback:
          mode: response # 熔断返回响应
          response-status: 500
          response-body: >
            {
              "code":500,
              "success":false,
              "message":"服务繁忙,请稍后再试",
              "traceId":"${traceId}",
              "timestamp":${timestamp}
            }
      datasource:
        # 熔断规则持久化到Nacos(可选)
        ds1:
          nacos:
            server-addr: localhost:8848
            data-id: sentinel-gateway-rules
            group-id: DEFAULT_GROUP
            rule-type: gw-flow

3. 微服务熔断配置(application.yml,所有微服务)

yaml 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
      feign:
        enabled: true # 启用Feign熔断
      datasource:
        ds1:
          nacos:
            server-addr: localhost:8848
            data-id: sentinel-service-rules
            group-id: DEFAULT_GROUP
            rule-type: flow

# Feign 超时配置(与熔断配合)
feign:
  client:
    config:
      default:
        connect-timeout: 3000 # 连接超时3秒
        read-timeout: 5000 # 读取超时5秒

4. 微服务熔断注解示例(StockService.java)

java 复制代码
package com.example.distributed.service;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.example.monomer.core.enums.BizErrorCodeEnum;
import com.example.monomer.core.exception.BizException;
import com.example.monomer.entity.Stock;
import com.example.monomer.mapper.StockMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 库存服务(含熔断注解)
 */
@Service
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    /**
     * 扣减库存(添加熔断注解)
     * fallback:熔断后的降级方法
     */
    @SentinelResource(value = "stock-deduct", fallback = "deductFallback")
    public boolean deductStock(Long productId, Integer count) {
        Stock stock = stockMapper.selectById(productId);
        if (stock == null) {
            throw new BizException(BizErrorCodeEnum.RESOURCE_NOT_FOUND, "商品不存在");
        }
        if (stock.getStock() < count) {
            throw new BizException(BizErrorCodeEnum.INVENTORY_INSUFFICIENT, "库存不足");
        }
        stock.setStock(stock.getStock() - count);
        return stockMapper.updateById(stock) > 0;
    }

    /**
     * 降级方法(与原方法参数一致)
     */
    public boolean deductFallback(Long productId, Integer count) {
        log.warn("库存服务熔断降级:productId={}, count={}", productId, count);
        // 降级策略:返回false,或调用备用服务等
        return false;
    }
}

5. Sentinel 熔断规则配置(通过控制台)

  1. 访问 Sentinel 控制台(http://localhost:8080),登录账号密码:sentinel/sentinel;
  2. 网关服务规则配置:
    • 选择"网关流控"→"新增网关流控规则";
    • 资源名:路由ID(如order-route);
    • 阈值类型:QPS;
    • 单机阈值:100(每秒最多100个请求);
    • 点击"新增"。
  3. 微服务规则配置:
    • 选择服务名(如stock-service)→"流控规则"→"新增";
    • 资源名:stock-deduct(注解中的value);
    • 阈值类型:QPS;
    • 单机阈值:50;
    • 点击"新增"。

验证方式:

  1. 用压测工具(如JMeter)向库存服务的扣减接口发送100QPS请求(超过阈值50);
  2. 观察 Sentinel 控制台,验证触发熔断;
  3. 查看调用方(订单服务)的响应,验证返回降级提示(服务繁忙,请稍后再试);
  4. 停止压测后,等待熔断恢复时间(默认10秒),验证服务恢复正常。

场景5:服务依赖与链路监控(可选优化)

  • 核心实现:Zipkin 告警+ELK 索引生命周期管理+Prometheus 监控

1. Zipkin 链路告警(通过 Kibana 配置)

  1. 访问 Kibana→Alerts→Create alert,选择"Threshold"规则;
  2. 配置触发条件:distributed-log-* 索引中,level:ERRORserviceName:order-service 的文档数5分钟内超过5条;
  3. 配置通知渠道:钉钉机器人(Webhook),告警消息包含 traceId、服务名、异常信息;
  4. 保存规则,启用告警。

2. ELK 索引生命周期管理(避免磁盘占满)

  1. 访问 Kibana→Stack Management→Index Lifecycle Policies;
  2. 创建策略:
    • 阶段1:热数据(0-7天),可写入可查询;
    • 阶段2:删除(7天后),自动删除索引;
  3. 将策略绑定到 distributed-log-* 索引模式;
  4. 验证:7天后自动清理过期日志索引。

四、分布式项目整合验证(跨服务流程闭环)

1. 验证流程

  1. 前端通过网关调用订单服务的"创建订单"接口(order-service);
  2. 订单服务通过 Feign 调用支付服务(pay-service)完成支付,调用库存服务(stock-service)扣减库存;
  3. 支付服务模拟支付成功,库存服务模拟库存不足抛出业务异常;
  4. 订单服务捕获库存服务的异常,返回标准化响应;
  5. 查看 Zipkin 链路:订单→支付(成功)→库存(失败),耗时明细可见;
  6. 查看 ELK 日志:按 traceId 检索,三个服务的日志完整展示;
  7. 前端接收异常响应:code=620,message=库存不足,格式与本地异常一致;
  8. 压测库存服务,触发 Sentinel 熔断,验证降级响应。

2. 校验清单

校验项 标准要求 验证方式
跨服务 traceId 一致 所有服务的日志、Zipkin 链路、响应中的 traceId 完全相同 查看接口调用日志与异步任务日志的traceId
日志集中可追溯 ELK 能按 traceId/bizId/serviceName 快速检索 按 traceId 检索全链路日志
异常响应统一 本地异常、远程异常、网关异常均为 R 格式 模拟各类异常,查看响应格式
服务依赖可视 Zipkin 能展示服务调用关系(订单依赖支付、库存) 查看 Zipkin 链路图
熔断降级生效 超过阈值后触发降级,返回标准化提示 压测服务,验证降级响应
告警生效 ERROR 日志量突增时,收到钉钉告警通知 模拟批量异常,查看告警渠道

五、分布式项目部署指南(生产级配置)

1. 生产必查项

安全配置

  • 敏感信息加密:数据库密码、JWT密钥、Nacos账号密码通过环境变量注入,禁止硬编码;
  • 接口安全:网关启用JWT鉴权,禁止未授权访问;
  • 中间件安全:Nacos、Elasticsearch、Sentinel配置访问密码,限制内网访问,禁止公网暴露。

性能优化

  • 连接池参数:Druid连接池maxActive=50,Feign超时设为3~5秒;
  • 日志性能:Logstash队列大小调整为2048,Elasticsearch分片数按服务器核数调整;
  • 采样率优化:Sleuth采样率probability=0.3,避免高并发下性能损耗;
  • 熔断阈值:根据服务性能测试结果配置合理阈值(如QPS=100~500)。

监控告警

  • 中间件监控:监控 Nacos/Elasticsearch/Zipkin/Sentinel 的 CPU、内存、磁盘使用率;
  • 服务监控:通过 Prometheus+Grafana 监控服务响应时间、错误率、QPS;
  • 日志告警:配置 ERROR 日志量、慢查询(超过500ms)告警;
  • 熔断告警:Sentinel 触发熔断时,通过钉钉/邮件通知运维。

2. 常见问题排查(生产环境)

问题现象 排查方向 解决方案
跨服务 traceId 丢失 1. 网关未透传 traceId;2. Feign 拦截器未注入;3. 微服务未接收 1. 检查网关过滤器请求头;2. 确认 Feign 拦截器被扫描;3. 验证 MicroServiceTraceInterceptor 注册
ELK 未收集到日志 1. Logstash 端口未映射;2. 微服务 Logstash 地址错误;3. 日志格式异常 1. 确认 5044 端口映射;2. 核对 logstash.host 配置;3. 查看 Logstash 日志(docker logs logstash)
Feign 调用异常格式混乱 1. 未绑定 FeignExceptionInterceptor;2. 远程服务异常未标准化 1. FeignClient 配置异常拦截器;2. 确保远程服务使用全局异常处理器
Zipkin 无跨服务链路 1. 服务未注册到 Nacos;2. Sleuth 依赖缺失;3. Zipkin 地址错误 1. Nacos 控制台确认服务注册;2. 核对 Sleuth+Zipkin 依赖;3. 修正 zipkin.base-url
熔断降级未生效 1. Sentinel 依赖缺失;2. 注解配置错误;3. 规则未配置 1. 核对 Sentinel 依赖;2. 确保 @SentinelResource 注解正确;3. 检查 Sentinel 控制台规则
网关路由失败 1. 路由规则错误;2. 服务名配置错误;3. 网关未排除 Tomcat 依赖 1. 核对网关路由 uri(lb://服务名);2. 与 Nacos 服务名一致;3. 网关依赖排除 spring-boot-starter-web
相关推荐
消失的旧时光-19432 小时前
401 自动刷新 Token 的完整架构设计(Dio 实战版)
开发语言·前端·javascript
wadesir2 小时前
Rust中的条件变量详解(使用Condvar的wait方法实现线程同步)
开发语言·算法·rust
我是Superman丶2 小时前
《Spring WebFlux 实战:基于 SSE 实现多类型事件流(支持聊天消息、元数据与控制指令混合传输)》
java
tap.AI2 小时前
RAG系列(二)数据准备与向量索引
开发语言·人工智能
廋到被风吹走2 小时前
【Spring】常用注解分类整理
java·后端·spring
阿蒙Amon2 小时前
C#每日面试题-重写和重载的区别
开发语言·c#
是一个Bug2 小时前
Java基础20道经典面试题(二)
java·开发语言
Z_Easen2 小时前
Spring 之元编程
java·开发语言
liliangcsdn3 小时前
python下载并转存http文件链接的示例
开发语言·python