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

本文从一个 HTTP 请求的完整生命周期出发, 系统梳理现代大型后端工程中的分层架构、DTO 设计、控制器与服务层职责划分、数据访问层抽象、事件驱动、API 版本控制与防腐层等核心工程化实践, 结合 NestJS、Spring Boot、Fastify 等主流框架的真实落地经验, 为构建高内聚、低耦合的后端系统提供一份全链路参考
一、分层架构总览
在大型后端工程中, 一个 HTTP 请求从发出到响应, 会经历严密的分层过滤与处理。这套体系的核心哲学就是各司其职和高内聚低耦合
核心原则
每一层只关心自己的职责, 不越界、不跨层调用。依赖方向永远是从外向内, 内层不知道外层的存在
二、DTO: 数据传输对象
什么是 DTO
DTO(Data Transfer Object, 数据传输对象) 是一种经典的软件设计模式, 它的核心职责是在系统的不同层(如路由层、控制层、服务层)之间安全、规范地传递数据
可以把它理解为一个数据快递盒: 外部传进来的零散数据被装进这个盒子里, 经过安检(校验)后, 再被送到业务处理的核心部门
DTO 的核心作用
| 作用 | 说明 |
|---|---|
| 解耦 | 将外部请求的数据格式与内部数据库模型(Entity)隔离, 数据库字段变化不影响前端调用 |
| 数据隐藏 | 返回数据时过滤敏感信息(密码、软删除标记等), 只暴露前端真正需要的字段 |
| 数据校验 | 在数据进入 Service 层之前, 集中进行参数类型、必填项、长度等规则校验 |
| 类型安全 | 结合 TypeScript 提供强大的代码自动补全和编译期类型检查 |
Request DTO 与 Response DTO
DTO 在一进一出两个阶段都会使用:
TypeScript 接口陷阱
如果 DTO 只是一个 TypeScript 的 interface, 编译后接口完全消失。直接 return 数据库查询结果, 即使强制类型转换为 DTO, 前端依然会收到所有数据库字段(包括密码)。必须在运行时通过手动赋值、序列化拦截器或 JSON Schema 进行强制过滤
各框架中的 DTO 实现
// 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);
}// 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);// 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);// 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 层就像一道防火墙, 拦截物理变动:
// 原始情况: 数据库有 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 只定义 id 和 name, 前端无论如何也拿不到 password
三、Controller 层: 瘦控制器
核心职责
Controller 是应用的边界, 负责处理所有与 HTTP 协议相关的工作。在架构设计中, 遵循瘦控制器, 胖服务(Skinny Controller, Fat Service) 原则
| 职责 | 说明 | 类比 |
|---|---|---|
| 接收请求 | 从 URL、Body、Headers 中提取参数, 组装 Request DTO | 服务员接单 |
| 基础校验 | 格式校验: 邮箱格式、密码长度、必填项检查 | 检查菜单 |
| 调度分发 | 将校验后的 DTO 传给对应的 Service 处理 | 把订单传给后厨 |
| 封装响应 | 设定 HTTP 状态码、返回格式, 将 Response DTO 发回前端 | 上菜 |
边界区分
Controller 做格式校验: 邮箱格式对不对、密码长度够不够、字段是否为空
Service 做业务校验: 账号余额是否足够支付、用户名是否已被注册(需要查数据库的复杂校验)
Controller 代码示例
@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 未定义的字段
});
}
}@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
反例: 上帝类
// OrderService 包含了创建订单、扣库存、算折扣、发通知、写日志
class OrderService {
async createOrder() { /* 500 行代码 */ }
}正例: 按功能领域拆分
// 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 容器自动完成
// 函数式视角: 依赖作为参数传入(高阶函数)
const createOrder = (inventoryService, promotionService) => (orderData) => {
// ...
};
// OOP 视角: 框架自动注入依赖
@Injectable()
class OrderService {
// NestJS 自动实例化并注入所有依赖
constructor(
private inventoryService: InventoryService,
private promotionService: PromotionService
) {}
}构造函数过度注入
如果一个 Service 被注入了 10+ 个依赖, 说明它管得太宽了。解决方案:
- 聚合关联服务: 积分和优惠券合并为
MarketingService统一对外暴露 - 转为事件驱动: 发邮件、送积分的服务自己去监听事件, 不需要主服务注入它们
策略 3: 事件驱动
反例: 同步串行
// 订单服务要挨个调用, 臃肿且任何一个报错都导致下单失败
async createOrder(dto) {
const order = await this.saveOrder(dto);
await this.smsService.send(order); // 如果短信服务挂了, 下单也失败?
await this.scoreService.add(order); // 积分服务超时, 用户等半天?
return order;
}正例: 事件驱动异步消费
// 核心逻辑完成后发布事件, 旁路服务异步消费
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: 提取纯函数工具
// 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 层应该重在业务规则, 而不是如何拼装 SQL。Repository 层负责屏蔽底层数据库的具体实现:
反例: Service 直接写 SQL
class UserService {
async findUser(id: number) {
return db.query('SELECT * FROM users WHERE id = ? AND is_deleted = 0', [id]);
}
}正例: 通过 Repository 隔离
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):
// 常规操作: 使用 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
`;// 常规操作
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 前缀:
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:
@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) { /* ... */ }
}@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 就能搜出整个请求在所有机器上的所有日志
统一响应格式
// 全局拦截器封装统一返回结构
@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不需要注入SmsService和ScoreService - 容错: 积分服务宕机不会导致下单失败
防止事件混乱的标准
滥用事件驱动最悲惨的结局是事件意大利面(Event Spaghetti): 查 Bug 时根本不知道一个事件发出后触发了哪些角落的代码
| 规范 | 说明 |
|---|---|
| 统一事件总线 | 单体应用用 EventEmitter; 微服务用专业中间件(RabbitMQ / Kafka / Redis Pub/Sub) |
| 严格事件契约 | 事件载荷必须有严格的格式定义, 使用 CloudEvents 规范或 JSON Schema 约束结构 |
| 命名规范 | 采用 领域.实体.过去时动作 格式, 如 trade.order.created、user.account.verified |
| 文档化 | 使用 AsyncAPI 规范记录"谁发布了什么事件, 谁消费了什么事件" |
| 链路追踪 | 接入 OpenTelemetry, 保证一个请求产生的全部异步事件都能在日志里串起来 |
事件流转示意
代码示例
// ———— 事件定义: 严格的 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 异常 |
| 改变字段数据类型 | 禁止 | id 从 Number 变 String 会导致老前端解析失败 |
| 非必填变必填 | 禁止 | 老前端不传该字段, 请求直接被拒绝 |
版本控制策略
当业务发生巨变, 无法遵循"只加不改"时, 引入多版本共存:
# 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/1 | GET /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), 所有脏活封锁在边缘层:
// ═══════════════════════════════════════════
// 绝对纯洁的 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 停用时, 直接
Delete掉UserControllerV1文件, 核心逻辑毫无损伤 - 副作用剥离: 发短信这种历史遗留动作被挡在
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+
- 任一子服务卡顿, 可能导致连接池耗尽, 引发雪崩效应
解决方案
反例: 串行调用
// 10ms + 10ms + 10ms = 30ms
const user = await userService.getUser(userId);
const score = await scoreService.getScore(userId);
const orders = await orderService.getOrders(userId);正例: 并行调用
// 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/放全局通用的基础设施代码, 不包含任何业务逻辑