Skip to content

后端工程化全链路最佳实践

作者:Atom
字数统计:6k 字
阅读时长:23 分钟

本文从一个 HTTP 请求的完整生命周期出发, 系统梳理现代大型后端工程中的分层架构、DTO 设计、控制器与服务层职责划分、数据访问层抽象、事件驱动、API 版本控制与防腐层等核心工程化实践, 结合 NestJSSpring BootFastify 等主流框架的真实落地经验, 为构建高内聚、低耦合的后端系统提供一份全链路参考

一、分层架构总览

在大型后端工程中, 一个 HTTP 请求从发出到响应, 会经历严密的分层过滤与处理。这套体系的核心哲学就是各司其职高内聚低耦合

核心原则

每一层只关心自己的职责, 不越界、不跨层调用。依赖方向永远是从外向内, 内层不知道外层的存在

二、DTO: 数据传输对象

什么是 DTO

DTO(Data Transfer Object, 数据传输对象) 是一种经典的软件设计模式, 它的核心职责是在系统的不同层(如路由层、控制层、服务层)之间安全、规范地传递数据

可以把它理解为一个数据快递盒: 外部传进来的零散数据被装进这个盒子里, 经过安检(校验)后, 再被送到业务处理的核心部门

DTO 的核心作用

作用说明
解耦将外部请求的数据格式与内部数据库模型(Entity)隔离, 数据库字段变化不影响前端调用
数据隐藏返回数据时过滤敏感信息(密码、软删除标记等), 只暴露前端真正需要的字段
数据校验在数据进入 Service 层之前, 集中进行参数类型、必填项、长度等规则校验
类型安全结合 TypeScript 提供强大的代码自动补全和编译期类型检查

Request DTO 与 Response DTO

DTO一进一出两个阶段都会使用:

TypeScript 接口陷阱

如果 DTO 只是一个 TypeScriptinterface, 编译后接口完全消失。直接 return 数据库查询结果, 即使强制类型转换为 DTO, 前端依然会收到所有数据库字段(包括密码)。必须在运行时通过手动赋值、序列化拦截器或 JSON Schema 进行强制过滤

各框架中的 DTO 实现

typescript
// NestJS 深度使用 class-validator 装饰器
// DTO 自动承担请求参数校验 + Swagger 文档生成
import { IsString, IsEmail, Length } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @Length(3, 20)
  username: string;

  @IsEmail()
  email: string;
}

// Controller 中通过 @Body() + ValidationPipe 自动校验
@Post()
createUser(@Body() dto: CreateUserDto) {
  return this.userService.create(dto);
}
typescript
// Fastify 通过 JSON Schema 实现 DTO 的所有核心职责
// 底层使用 ajv 进行极速校验, 并根据 response schema 自动剔除多余字段
const createUserSchema = {
  body: {
    type: 'object',
    required: ['username', 'email'],
    properties: {
      username: { type: 'string', minLength: 3, maxLength: 20 },
      email: { type: 'string', format: 'email' }
    }
  },
  response: {
    200: {
      type: 'object',
      properties: {
        id: { type: 'number' },
        username: { type: 'string' }
        // password 字段未定义, 会被自动剔除
      }
    }
  }
};

fastify.post('/users', { schema: createUserSchema }, handler);
typescript
// Express 原生无 DTO 概念, 需手动结合 Zod / Joi / class-validator
import { z } from 'zod';

const CreateUserSchema = z.object({
  username: z.string().min(3).max(20),
  email: z.string().email()
});

type CreateUserDto = z.infer<typeof CreateUserSchema>;

// 通过中间件实现校验
function validateBody(schema: z.ZodSchema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({ errors: result.error.issues });
    }
    req.body = result.data; // 已校验 + 过滤的数据
    next();
  };
}

app.post('/users', validateBody(CreateUserSchema), createUserHandler);
java
// Spring Boot 结合 Jakarta Validation 注解
public class CreateUserDto {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20)
    private String username;

    @Email(message = "邮箱格式不正确")
    @NotBlank
    private String email;
}

// Controller 中通过 @Valid 自动触发校验
@PostMapping("/users")
public UserResponseDto createUser(@Valid @RequestBody CreateUserDto dto) {
    return userService.create(dto);
}

DTO 如何实现解耦

当数据库字段发生变化时, DTO 层就像一道防火墙, 拦截物理变动:

typescript
// 原始情况: 数据库有 full_name 字段
// DTO:  { fullName: string }
// 映射: dto.fullName = dbUser.full_name

// ————— 数据库重构: full_name 拆成 first_name + last_name —————

// DTO 保持不变: { fullName: string }
// 只需修改 Mapper 层的映射逻辑:
dto.fullName = `${dbUser.first_name} ${dbUser.last_name}`;

// 结果: 前端依然收到 fullName, 零感知, 不需要改一行代码

白名单机制

DTO 的核心安全特性: 只有 DTO 中定义的字段, 前端才能访问。数据库里有 id, name, password, is_deleted 四个字段, Response DTO 只定义 idname, 前端无论如何也拿不到 password

三、Controller 层: 瘦控制器

核心职责

Controller 是应用的边界, 负责处理所有与 HTTP 协议相关的工作。在架构设计中, 遵循瘦控制器, 胖服务(Skinny Controller, Fat Service) 原则

职责说明类比
接收请求从 URL、Body、Headers 中提取参数, 组装 Request DTO服务员接单
基础校验格式校验: 邮箱格式、密码长度、必填项检查检查菜单
调度分发将校验后的 DTO 传给对应的 Service 处理把订单传给后厨
封装响应设定 HTTP 状态码、返回格式, 将 Response DTO 发回前端上菜

边界区分

Controller 做格式校验: 邮箱格式对不对、密码长度够不够、字段是否为空

Service 做业务校验: 账号余额是否足够支付、用户名是否已被注册(需要查数据库的复杂校验)

Controller 代码示例

typescript
@Controller('api/v1/orders')
export class OrderController {
  constructor(private readonly orderService: OrderService) {}

  @Post()
  @HttpCode(201)
  async createOrder(@Body() dto: CreateOrderDto): Promise<OrderResponseDto> {
    // Controller 极其精简: 接收 DTO → 调用 Service → 返回结果
    const order = await this.orderService.create(dto);
    return plainToInstance(OrderResponseDto, order, {
      excludeExtraneousValues: true // 自动剔除 DTO 未定义的字段
    });
  }
}
java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    private final OrderService orderService;

    // 构造器注入(Spring 推荐方式)
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public OrderResponseDto createOrder(@Valid @RequestBody CreateOrderDto dto) {
        return orderService.create(dto);
    }
}

四、Service 层: 核心业务引擎

为什么 Service 层很重

Service 层不知道、也不应该知道任何关于 HTTP 的事情(不应出现 req, res, statusCode)。它只接收普通的参数(DTO), 返回普通的结果

将业务逻辑写在 Service 而不是 Controller 的原因:

  • 隔离 HTTP 语境: 业务逻辑不绑定传输协议
  • 高复用性: 不仅可以被 Web Controller 调用, 还可以被定时任务(Cron Job)、RPC 接口、命令行脚本调用
  • 可测试性: 单元测试无需模拟 HTTP 请求, 直接传参即可

实现高复用的 5 个策略

Service 层变得庞大时, 需要将一大块石头打碎成可以拼接的乐高积木:

策略 1: 单一职责 — 拆分 Service

反例: 上帝类

typescript
// OrderService 包含了创建订单、扣库存、算折扣、发通知、写日志
class OrderService {
  async createOrder() { /* 500 行代码 */ }
}

正例: 按功能领域拆分

typescript
// OrderService 变成编排者(Orchestrator)
class OrderService {
  constructor(
    private inventoryService: InventoryService,   // 管库存
    private promotionService: PromotionService,   // 算折扣
    private notificationService: NotificationService // 发通知
  ) {}

  async createOrder(dto: CreateOrderDto) {
    const discount = await this.promotionService.calculate(dto.items);
    await this.inventoryService.deduct(dto.items);
    const order = await this.saveOrder(dto, discount);
    await this.notificationService.send(order);
    return order;
  }
}

新用户注册也需要发邮件? 直接复用 NotificationService, 不需要重写发邮件逻辑

策略 2: 依赖注入

依赖注入(DI)本质上就是函数式编程中的参数传入, 只不过在 OOP 框架中由 IoC 容器自动完成

typescript
// 函数式视角: 依赖作为参数传入(高阶函数)
const createOrder = (inventoryService, promotionService) => (orderData) => {
  // ...
};

// OOP 视角: 框架自动注入依赖
@Injectable()
class OrderService {
  // NestJS 自动实例化并注入所有依赖
  constructor(
    private inventoryService: InventoryService,
    private promotionService: PromotionService
  ) {}
}

构造函数过度注入

如果一个 Service 被注入了 10+ 个依赖, 说明它管得太宽了。解决方案:

  1. 聚合关联服务: 积分和优惠券合并为 MarketingService 统一对外暴露
  2. 转为事件驱动: 发邮件、送积分的服务自己去监听事件, 不需要主服务注入它们

策略 3: 事件驱动

反例: 同步串行

typescript
// 订单服务要挨个调用, 臃肿且任何一个报错都导致下单失败
async createOrder(dto) {
  const order = await this.saveOrder(dto);
  await this.smsService.send(order);    // 如果短信服务挂了, 下单也失败?
  await this.scoreService.add(order);   // 积分服务超时, 用户等半天?
  return order;
}

正例: 事件驱动异步消费

typescript
// 核心逻辑完成后发布事件, 旁路服务异步消费
async createOrder(dto) {
  const order = await this.saveOrder(dto);
  this.eventBus.emit('order.created', new OrderCreatedEvent(order));
  return order; // 前端瞬间收到 "下单成功"
}

// 旁路服务在后台默默监听
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent) {
  await this.smsService.send(event.order);
  await this.scoreService.add(event.order);
}

策略 4: Repository 模式

将数据访问逻辑从 Service 中剥离(详见第五节)

策略 5: 提取纯函数工具

typescript
// utils/crypto.ts — 无状态、无副作用、随调随走
export function hashPassword(password: string): string {
  return bcrypt.hashSync(password, 10);
}

export function generateTreeStructure<T>(flatList: T[]): TreeNode<T>[] {
  // ...
}

五、数据访问层: Repository 与 ORM

Repository 模式

Service 层应该重在业务规则, 而不是如何拼装 SQLRepository 层负责屏蔽底层数据库的具体实现:

反例: Service 直接写 SQL

typescript
class UserService {
  async findUser(id: number) {
    return db.query('SELECT * FROM users WHERE id = ? AND is_deleted = 0', [id]);
  }
}

正例: 通过 Repository 隔离

typescript
class UserService {
  constructor(private userRepo: UserRepository) {}

  async findUser(id: number) {
    return this.userRepo.findById(id); // 不关心底层是 MySQL 还是 MongoDB
  }
}

ORM 的核心价值

ORM(Object-Relational Mapping) 不仅仅是把 SQL 变成 .add() / .find():

价值说明
对象映射将数据库二维表数据自动转化为带类型的实体对象
屏蔽差异MySQL 换 PostgreSQL 时, 业务代码通常不需要改
防 SQL 注入底层自动处理参数转义
迁移管理通过 Migration 文件版本化管理数据库 Schema 变更

80/20 法则

最佳实践

80% 的日常 CRUD 和简单关联 → 使用 ORM API 提升开发效率

20% 的复杂报表查询、多表深层 Join → 退回使用原生 SQL 或 Query Builder

现代 ORM 都提供了逃生舱(Escape Hatch):

typescript
// 常规操作: 使用 ORM API
const users = await prisma.user.findMany({
  where: { status: 'ACTIVE' },
  include: { profile: true }
});

// 复杂查询: 退回原生 SQL
const report = await prisma.$queryRaw`
  SELECT u.id, COUNT(o.id) as order_count, SUM(o.amount) as total
  FROM users u
  LEFT JOIN orders o ON u.id = o.user_id
  WHERE o.created_at >= ${startDate}
  GROUP BY u.id
  HAVING total > 10000
  ORDER BY total DESC
`;
typescript
// 常规操作
const users = await userRepository.find({
  where: { status: 'ACTIVE' },
  relations: ['profile']
});

// 复杂查询: 使用 QueryBuilder
const report = await userRepository
  .createQueryBuilder('u')
  .leftJoin('u.orders', 'o')
  .select('u.id', 'userId')
  .addSelect('COUNT(o.id)', 'orderCount')
  .addSelect('SUM(o.amount)', 'total')
  .where('o.createdAt >= :startDate', { startDate })
  .groupBy('u.id')
  .having('total > :threshold', { threshold: 10000 })
  .orderBy('total', 'DESC')
  .getRawMany();

六、完整请求链路

两级路由机制

在现代 Web 部署中, 路由是分级存在的。以请求 GET https://yourdomain.com/api/v1/users/123 为例:

第一级: 基础设施层(Nginx)— 宏观路由

Nginx 不关心什么是"用户"、什么是"订单", 它只看 URL 前缀:

nginx
server {
    listen 443 ssl;
    server_name yourdomain.com;

    # 静态资源: 直接返回文件
    location /static {
        root /var/www/dist;
        expires 30d;
    }

    # API 请求: 反向代理到 Node.js 进程
    location /api {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Request-Id $request_id;  # 注入链路追踪 ID
    }
}

第二级: 应用层(框架 Router)— 微观路由

框架对 URL 进行精细拆解, 匹配路径 + HTTP Method, 分发到具体的 Controller:

typescript
@Controller('api/v1/users')
export class UserController {
  // GET /api/v1/users/123
  @Get(':id')
  getUser(@Param('id') id: string) { /* ... */ }

  // POST /api/v1/users
  @Post()
  createUser(@Body() dto: CreateUserDto) { /* ... */ }

  // DELETE /api/v1/users/123
  @Delete(':id')
  deleteUser(@Param('id') id: string) { /* ... */ }
}
java
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @GetMapping("/{id}")
    public UserDto getUser(@PathVariable Long id) { /* ... */ }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto createUser(@Valid @RequestBody CreateUserDto dto) { /* ... */ }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable Long id) { /* ... */ }
}

完整生命周期

用户下单为例, 走完全部链路:

TraceID 贯穿全链路

链路追踪 ID 从 Nginx 注入后, 会在日志中贯穿所有层级。出了 Bug, 拿着这个 ID 就能搜出整个请求在所有机器上的所有日志

统一响应格式

typescript
// 全局拦截器封装统一返回结构
@Injectable()
export class TransformInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        code: 200,
        data,
        message: 'success',
        traceId: context.switchToHttp().getRequest().headers['x-request-id']
      }))
    );
  }
}

// 全局异常过滤器: 绝不把 Stack Trace 暴露给前端
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const status = exception instanceof HttpException
      ? exception.getStatus()
      : 500;

    response.status(status).json({
      code: status,
      data: null,
      message: status === 500 ? '服务器内部错误' : exception.message,
      traceId: ctx.getRequest().headers['x-request-id']
    });
  }
}

七、事件驱动: 避免事件意大利面

为什么需要事件驱动

核心接口只做核心的事, 非核心的旁路逻辑通过事件异步执行:

  • 性能: 前端瞬间收到"下单成功", 不用等短信发完
  • 解耦: OrderService 不需要注入 SmsServiceScoreService
  • 容错: 积分服务宕机不会导致下单失败

防止事件混乱的标准

滥用事件驱动最悲惨的结局是事件意大利面(Event Spaghetti): 查 Bug 时根本不知道一个事件发出后触发了哪些角落的代码

规范说明
统一事件总线单体应用用 EventEmitter; 微服务用专业中间件(RabbitMQ / Kafka / Redis Pub/Sub)
严格事件契约事件载荷必须有严格的格式定义, 使用 CloudEvents 规范或 JSON Schema 约束结构
命名规范采用 领域.实体.过去时动作 格式, 如 trade.order.createduser.account.verified
文档化使用 AsyncAPI 规范记录"谁发布了什么事件, 谁消费了什么事件"
链路追踪接入 OpenTelemetry, 保证一个请求产生的全部异步事件都能在日志里串起来

事件流转示意

代码示例

typescript
// ———— 事件定义: 严格的 Schema ————
class OrderCreatedEvent {
  readonly eventType = 'trade.order.created';
  readonly timestamp = new Date();

  constructor(
    public readonly orderId: string,
    public readonly userId: string,
    public readonly totalAmount: number,
    public readonly traceId: string
  ) {}
}

// ———— 发布方: 只管发, 不管谁来消费 ————
@Injectable()
export class OrderService {
  constructor(private eventEmitter: EventEmitter2) {}

  async createOrder(dto: CreateOrderDto) {
    const order = await this.orderRepo.save(dto);
    this.eventEmitter.emit(
      'trade.order.created',
      new OrderCreatedEvent(order.id, dto.userId, order.total, dto.traceId)
    );
    return order;
  }
}

// ———— 消费方 A: 发送通知 ————
@Injectable()
export class NotificationListener {
  @OnEvent('trade.order.created')
  async handleOrderCreated(event: OrderCreatedEvent) {
    await this.pushService.send(event.userId, `订单 ${event.orderId} 创建成功`);
  }
}

// ———— 消费方 B: 增加积分 ————
@Injectable()
export class ScoreListener {
  @OnEvent('trade.order.created')
  async handleOrderCreated(event: OrderCreatedEvent) {
    const points = Math.floor(event.totalAmount / 10);
    await this.scoreService.add(event.userId, points);
  }
}

八、API 版本控制与向下兼容

铁律: 只增不减, 只加不改

在同一个接口版本内, 只能做加法:

操作是否安全说明
Response 新增字段允许老前端拿到不认识的字段会自动忽略
Request 新增非必填参数允许老前端不传, 后端用默认值兜底
删除/重命名字段禁止老前端直接崩溃或报 Null 异常
改变字段数据类型禁止idNumberString 会导致老前端解析失败
非必填变必填禁止老前端不传该字段, 请求直接被拒绝

版本控制策略

当业务发生巨变, 无法遵循"只加不改"时, 引入多版本共存:

# URL 路径版本化(最常见、最直观)
GET /api/v1/orders    ← 老 App 继续访问
GET /api/v2/orders    ← 新 App 访问

# Header 头版本化(RESTful 原教旨主义)
GET /api/orders
Accept: application/vnd.company.v1+json

大型项目的路由军规

规范正例反例
资源命名(名词化)GET /api/v1/users/1GET /api/v1/getUserById?id=1
强制版本号/api/v1/orders/api/orders
小写 + 连字符/api/v1/user-profiles/api/v1/userProfiles
领域划分用户服务统一 /api/v1/users/*订单服务里出现 /api/v1/getUserInfo

优雅废弃流程(Sunset Policy)

九、防腐层: 保护核心服务的纯洁性

问题: 接口新增导致历史代码堆积

每次业务变动都新增 v2 接口、老接口原封不动保留, 短期最安全, 但长期是饮鸩止渴:

  • 修 Bug 可能需要同时在 v1/v2/v3 中修复
  • 新人看到三个高度相似但微妙不同的接口, 无法分辨
  • 历史接口绑定旧表结构, 导致数据库重构被死死拖住

核心原则: 接口可以多版本, 业务逻辑永远只有一份

通过防腐层(Anti-Corruption Layer, ACL) 将老接口降级为适配器(Adapter), 所有脏活封锁在边缘层:

typescript
// ═══════════════════════════════════════════
// 绝对纯洁的 ServiceV2 — 只用最新业务模型
// 铁律: 不能出现 isV1 / isLegacy 之类的判断参数
// ═══════════════════════════════════════════
interface CreateUserCommand {
  firstName: string;
  lastName: string;
  source: string;  // V2 强制要求的新字段
}

@Injectable()
export class UserServiceV2 {
  async createUser(command: CreateUserCommand) {
    // 只关心当前最新的业务逻辑, 完全不知道 V1 的存在
    const user = await this.userRepo.save({
      firstName: command.firstName,
      lastName: command.lastName,
      source: command.source
    });
    this.eventBus.emit('user.created', new UserCreatedEvent(user));
    return user;
  }
}

// ═══════════════════════════════════════════
// 防腐层 — 承担所有 "脏活" 的 ControllerV1
// 这个文件就是隔离区, 随时可以整个 Delete
// ═══════════════════════════════════════════
@Controller('api/v1/users')
export class UserControllerV1 {
  constructor(
    private readonly userServiceV2: UserServiceV2,        // 调用最新的 Service
    private readonly legacySmsService: LegacySmsService    // 老短信服务只注入到这里
  ) {}

  @Post()
  async createUser(@Body() oldBody: { fullName: string; needSms?: boolean }) {
    // ——— 步骤 1: 入参翻译(老格式 → 新格式) ———
    const [firstName = '', lastName = ''] = oldBody.fullName.split(' ');
    const v2Command: CreateUserCommand = {
      firstName,
      lastName,
      source: 'LEGACY_APP_V1'  // 填补老版本根本没传的字段
    };

    // ——— 步骤 2: 调用纯洁的核心服务 ———
    const user = await this.userServiceV2.createUser(v2Command);

    // ——— 步骤 3: 处理被新版废弃的副作用 ———
    if (oldBody.needSms) {
      await this.legacySmsService.send(oldBody.fullName);
    }

    // ——— 步骤 4: 出参伪装(新结果 → 老格式) ———
    return {
      userId: user.id,
      name: `${user.firstName} ${user.lastName}`.trim(),
      status: 'success'  // 伪造老前端依赖的废弃字段
    };
  }
}

// ═══════════════════════════════════════════
// 干净的 ControllerV2 — 直接对接最新 Service
// ═══════════════════════════════════════════
@Controller('api/v2/users')
export class UserControllerV2 {
  constructor(private readonly userServiceV2: UserServiceV2) {}

  @Post()
  async createUser(@Body() dto: CreateUserDtoV2) {
    return this.userServiceV2.createUser(dto);
  }
}

为什么这叫 "严格保护"

  • 没有 if/else 污染: UserServiceV2 里没有一行代码在判断"是不是老版本"
  • 随时可以下线: V1 停用时, 直接 DeleteUserControllerV1 文件, 核心逻辑毫无损伤
  • 副作用剥离: 发短信这种历史遗留动作被挡在 Service 外面

架构权衡

写在 Service 里 — 分布式的毒药

核心代码里充斥 if (version === 'v1'), 修改时心惊胆战, 技术债务全身性蔓延

写在 Adapter 里 — 集中式的隔离区

代码又长又丑, 但债务被隔离。核心 Service 干净清爽, 随时可以连根拔起。这种看似繁琐的实现, 是用牺牲眼前的开发效率, 换取系统长期的可维护性和安全重构的底气

减轻 DTO 拼装痛苦的手段

大量手动 a.name = b.firstName + b.lastName 确实让人抓狂, 现代工程的缓解方案:

手段说明
对象映射工具Java 用 MapStruct, TypeScript 用 class-transformer / @automapper/core, 自动生成转换代码
BFF 层将 DTO 拼装逻辑抽离为独立的 Backend for Frontend 服务, 底层微服务永远只提供最新 API
GraphQL天然按需查询, 后端 Schema 增加字段时老前端不查就行, 大幅减轻兼容负担

十、性能考量: 进程内 vs 跨网络

单体应用中: 无性能问题

在单体架构中, 大量 Service 拆分只是代码组织上的解耦, 底层都跑在同一个进程里。所谓的"注入大量子服务"和"依赖注入", 在底层仅仅是内存中对象指针的传递, CPU 执行这种函数调用每秒可达数千万次, 性能损耗可以忽略不计

微服务中: 网络延迟放大

当服务分布在不同服务器上, 同步调用多个微服务会触发网络延迟放大(Network Latency Amplification):

  • 内存调用: ~1ns, 内网 HTTP 调用: ~10ms
  • 串行调用 5 个服务, 光网络延迟就 50ms+
  • 任一子服务卡顿, 可能导致连接池耗尽, 引发雪崩效应

解决方案

反例: 串行调用

typescript
// 10ms + 10ms + 10ms = 30ms
const user = await userService.getUser(userId);
const score = await scoreService.getScore(userId);
const orders = await orderService.getOrders(userId);

正例: 并行调用

typescript
// max(10ms, 10ms, 10ms) = 10ms
const [user, score, orders] = await Promise.all([
  userService.getUser(userId),
  scoreService.getScore(userId),
  orderService.getOrders(userId)
]);
方案适用场景
Promise.all 并行调用无依赖关系的多个查询
gRPC 替代 HTTP微服务间高频通信, 基于 HTTP/2 + Protobuf, 速度快数倍
BFF 层聚合为前端定制的数据拼装层, 部署在离网关最近的位置
事件异步化能异步的绝不同步等待, 核心操作完成后发消息即返回

十一、工程目录结构参考

一个遵循上述最佳实践的 NestJS 项目, 典型的目录结构:

src/
├── main.ts                          # 应用入口
├── app.module.ts                    # 根模块
├── common/                          # 全局通用层
│   ├── filters/                     # 全局异常过滤器
│   │   └── global-exception.filter.ts
│   ├── interceptors/                # 响应格式化拦截器
│   │   └── transform.interceptor.ts
│   ├── guards/                      # 鉴权守卫
│   │   └── jwt-auth.guard.ts
│   ├── pipes/                       # 校验管道
│   └── utils/                       # 纯函数工具
│       ├── crypto.util.ts
│       └── tree.util.ts
├── modules/                         # 按领域划分的业务模块
│   ├── user/
│   │   ├── user.module.ts
│   │   ├── user.controller.ts       # 路由 + 参数校验
│   │   ├── user.service.ts          # 核心业务逻辑
│   │   ├── user.repository.ts       # 数据访问层
│   │   ├── dto/
│   │   │   ├── create-user.dto.ts   # Request DTO
│   │   │   └── user-response.dto.ts # Response DTO
│   │   ├── entities/
│   │   │   └── user.entity.ts       # 数据库实体
│   │   ├── events/
│   │   │   └── user-created.event.ts
│   │   └── listeners/
│   │       └── user-notification.listener.ts
│   └── order/
│       ├── order.module.ts
│       ├── v1/                      # V1 防腐层(适配器)
│       │   └── order-v1.controller.ts
│       ├── order.controller.ts      # V2 控制器
│       ├── order.service.ts
│       ├── order.repository.ts
│       ├── dto/
│       └── entities/
└── config/                          # 配置管理
    ├── database.config.ts
    └── app.config.ts

目录设计原则

  • 业务领域划分模块, 而非按技术层级(controllers/, services/)
  • 历史版本的适配器放在模块内的 v1/ 子目录, 方便整体清理
  • common/ 放全局通用的基础设施代码, 不包含任何业务逻辑