首页 / 文章 / 后端反模式:7个代价高昂的错误及其实用解决方案

后端反模式:7个代价高昂的错误及其实用解决方案

了解如何识别并修复七种常见的后端工程错误,从臃肿的控制器到未处理的异常,从而避免其在生产环境中引发问题。

2073 词

在开始学习后端开发时,人们很容易认为真正的挑战在于掌握更多工具。

Node.js。

Express。

PostgreSQL。

Redis。

Docker。

消息队列。

系统设计。

但在开发了多款应用之后,一个不同的真相便显现出来。

掌握更多工具并不会自动让你成为更出色的后端工程师。

真正的成长大多来自于犯错、分析错误原因,并确保同类错误不再发生。

以下是七种值得借鉴的后端开发错误,以及更有效的解决方式。

1. 将所有功能都放在控制器中

这往往是人们最先犯的错误之一。

一个接口最初可能看起来像这样:

app.post("/orders", async (req, res) => {
  const { userId, productId, quantity } = req.body;
  const user = await db.users.findUnique({
    where: { id: userId }
  });  if (!user) {
    return res.status(404).json({
      message: "User not found"
    });
  }  const product = await db.products.findUnique({
    where: { id: productId }
  });  if (!product) {
    return res.status(404).json({
      message: "Product not found"
    });
  }  if (product.stock < quantity) {
    return res.status(400).json({
      message: "Not enough stock"
    });
  }  const order = await db.orders.create({
    data: {
      userId,
      productId,
      quantity
    }
  });  await sendEmail(user.email);  return res.status(201).json(order);
});

它能正常工作。

但看看这个函数现在要处理的所有内容:

  • 验证
  • 数据库查询
  • 业务规则
  • 库存检查
  • 订单创建
  • 邮件处理
  • HTTP响应

随着项目规模不断扩大,承担如此多职责的函数会逐渐变成难以管理的庞大逻辑块。

解决办法就是拆分这些职责。

Request
   ↓
Controller
   ↓
Service
   ↓
Repository
   ↓
Databas

控制器的职责是处理HTTP请求。

服务层负责业务逻辑。

数据访问层负责与数据的交互。

这并不意味着小型应用就需要六层抽象结构。

它的含义是系统的每个部分都应拥有明确界定的职责。

2. 信任前端

这种做法可能会引入错误,更糟糕的是还会造成安全漏洞。

假设前端发送了如下数据:

{
  "price": 10,
  "quantity": 2
}

人们很容易想直接用客户端发送的价格来计算订单总额。

不要这么做。

没有任何东西能阻止恶意客户端发送:

{
  "price": 1,
  "quantity": 100
}

前端由用户控制,而非你。

你的后端需要验证并执行真正重要的规则。

例如:

const product = await productRepository.findById(
  productId
);
const total = product.price * quantity;

定价的权威应来自后端,而非客户端。

同样的谨慎态度也需应用到客户端可能试图影响的其它多个领域:

  • 用户角色
  • 权限
  • 折扣
  • 库存
  • 支付金额
  • 账户状态
  • 资源所有权

把前端视为塑造用户使用产品体验的工具即可。

这并非安全边界。

3. 不良的错误处理方式

早期,错误处理通常如下所示:

try {
  // something
} catch (error) {
  console.log(error);
  return res.status(500).json({
    message: "Something went wrong"
  });
}

使用通用兜底处理方式本身并无问题。

问题在于在所有情况下都依赖它。

用户记录缺失并不一定属于500级错误。

请求格式错误也不一定属于500级错误。

邮箱重复同样不一定是500级错误。

后端需要区分不同的故障类型。

例如:

400 → Invalid request
401 → Authentication required
403 → Not allowed
404 → Resource not found
409 → Conflict
422 → Validation failure
500 → Unexpected server error

所选择的特定状态码取决于API的规范,但保持一致性比具体的方案更重要。

结构化的错误响应也有帮助。

例如:

{
  "success": false,
  "message": "User already exists",
  "code": "USER_ALREADY_EXISTS"
}

采用这种格式后,前端无需猜测出了什么问题。

4. 硬编码配置

在尝试部署之前,这种错误似乎并无大碍。

例如:

const databaseUrl =
  "postgresql://user:password@localhost:5432/app";

或者:

const jwtSecret = "my-secret";

应完全避免这种模式。

您运行的每个环境都需要独立的设置。

您可能会遇到:

Development
    ↓
localhost
Staging
    ↓
staging databaseProduction
    ↓
production database

相反,应采用基于环境的配置方式:

DATABASE_URL=
REDIS_URL=
JWT_SECRET=
PAYMENT_API_KEY=
EMAIL_API_KEY=

并且绝不要将敏感信息提交到版本控制系统中。

.env 文件适用于本地开发,但生产环境需要合适的敏感信息与配置管理工具。

核心原则是:

您的代码不应与特定于环境的配置值紧密绑定。

5. 在真正需要之前就进行扩展

这是许多开发者在某个阶段都会陷入的陷阱,当时很容易为其找借口。

一个团队启动新项目后立刻会想到:

“如果明天有1000万人出现会怎样?”

于是他们就决定:

Microservices
Kafka
Redis
Kubernetes
Multiple databases
API Gateway
Event-driven architecture

从理论上讲,该系统能够处理大规模需求。

但实际上只有五名用户。

这并非稳健的架构,只是多了些目前并不需要的复杂性。

对于大多数项目而言,从简单开始更为合理:

Client
  ↓
Node.js Application
  ↓
PostgreSQL

只有当有明确理由时,才引入新组件:

Need caching?
→ Redis
Need background jobs?
→ Queue + WorkerNeed more API capacity?
→ Multiple instances + Load BalancerDatabase becoming a bottleneck?
→ Optimize queries / indexes / architecture

让架构随着实际且已验证的需求逐步发展。

不要仅仅因为某些视频声称“真正的”高级工程师都这么做,就盲目采用分布式系统。

6. 拦截处理缓慢的任务请求

这种情况会严重破坏使用 API 的体验。

想象这样的场景:

app.post("/order", async (req, res) => {
  const order = await createOrder();  await sendEmail();  await generateInvoice();  await notifyWarehouse();  await updateAnalytics();  return res.json(order);
});

用户只能等待这五个步骤依次完成。

如果哪怕只有一个外部调用多耗时五秒,整个响应时间就会随之增加五秒。

更好的做法是只在请求中处理那些确实需要立即完成的操作。

其余任务都可以放入队列中进行后台处理。

Client
  ↓
API
  ↓
Create Order
  ↓
Queue Jobs
  ↓
Response

接下来是:

Queue
  ↓
Worker
  ├── Send Email
  ├── Generate Invoice
  ├── Notification
  └── Analytics

正是在这种场景下,BullMQ 与 Redis 的组合才能发挥其作用。

但这里还有另一个容易被忽视的要点:

后台任务必须具备幂等性,并能妥善处理失败情况。

如果某个任务意外执行了两次,可能带来的风险包括:

  • 向客户多次收费
  • 发送重复的通知
  • 在数据库中写入重复的记录

仅仅将任务放入队列并不能解决这个问题。

必须从设计阶段就考虑到这一点。

7. 生产环境中的盲目操作

这种错误通常在出现问题之前都难以被发现。

想象一下,某个正在运行的 API 突然开始出现错误。

乍看之下服务器似乎没有问题。

代码也看起来正常。

但实际存在的问题是:

No useful logs
No request IDs
No metrics
No error tracking
No database monitoring

此时,唯一的办法就是猜测。

调试过程就变成了接连不断的无奈摇头:

“也许是 Redis 崩溃了?”“也许是数据库运行速度变慢了?”“也许是支付服务出现了问题?”

作为工程师,身处这种状况实在令人不安。

至少,有用的日志是必不可少的。

例如:

{
  "level": "error",
  "requestId": "req_123",
  "route": "/orders",
  "userId": "user_456",
  "message": "Payment provider timeout"
}

有了这样的输出,就能准确追踪问题出在什么地方以及原因是什么。

随着系统的不断发展,可观测性通常会扩展到涵盖以下方面:

  • 应用程序日志
  • 错误追踪
  • CPU和内存使用情况
  • 数据库层面的指标
  • API响应时间
  • 队列积压量
  • 外部API的故障情况
  • 健康检查端点

如果无法了解系统的运行状况,它就无法保持健康状态。

更深层的启示

仔细分析后就会发现,几乎所有这些问题都源于同一个根本性的习惯。

许多决策所依据的问题往往是:

“这个功能能用吗?”

而实际上应该问的是:

“这个功能是否易于修改、调试以及在生产环境中运行?”

这种思维方式的转变几乎改变了软件开发的方方面面。

在认为API开发完成之前需要验证的内容

在将某个后端功能标记为已完成之前,最好先进行以下检查。

代码

  • 每一层是否有明确且单一的职责?
  • 业务逻辑能否独立进行测试?
  • 控制器是否保持适度简洁?

安全性

  • 所有传入的输入是否都经过验证?
  • 权限检查是否在服务器端执行?
  • 敏感信息是否得到妥善保护?
  • 身份验证和授权机制是否得到妥善实现?
  • 数据库

    • 查询效率是否较高?
    • 是否存在合适的索引?
    • 在需要处是否使用了事务处理?
    • 是否存在隐藏的N+1查询问题?

    性能

    • 那些可以避免的顺序调用是否已被消除?
    • 繁重的任务是否被转至后台进程处理?
    • 使用缓存能否提升性能?

    可靠性

    • 如果外部API出现故障,有什么备用方案?
    • 重试机制是否设置合理?
    • 后台任务是否可以安全地多次运行?
    • 如果Redis或数据库无法访问,会怎样?

    运维

    • 当出现问题时,能否查明原因?
    • 日志是否真正有用?
    • 是否有健康检查机制?
  • 能否衡量 API 的性能?
  • 完美的系统并非目标。

    但必须清楚出问题时究竟发生了什么。

    七大常见错误一览

    错误类型 更好的处理方式
    所有功能都集中在控制器中 将职责分配到不同层次
    轻信前端传来的数据 在服务器端对所有数据进行验证
    采用千篇一律的错误处理方式 使用统一的错误处理策略
    使用硬编码的敏感信息 妥善管理环境和配置
    过早进行扩展 根据实际瓶颈再行扩展
    耗时的操作阻塞请求处理 将其放到后台任务中处理
    无法了解生产环境状况
    添加日志记录与监控功能

    核心要点

    人们普遍认为,提升后端开发者的水平意味着掌握更多工具和技术。

    更准确的看法是,关键在于理解权衡取舍

    这个操作应该是同步还是异步?

    这些数据值得缓存吗?

    这个查询需要建立索引吗?

    这应该成为一个独立的服务吗?

    如果 Redis 停止工作,有什么应对方案?

    如果数据库运行速度变得极慢,有什么应对方案?

    如果某个任务意外被执行了两次,有什么应对方案?

    如果依赖的外部 API 出现故障,有什么应对方案?

    掌握这些问题的答案远比单纯知道如何安装另一个软件包重要得多。

    因为生产系统的优劣并非取决于一切正常时的表现。

    真正的工程能力体现在出现问题时。

    今天能识别的每一个错误,都能减少日后需要处理的紧急故障。

    相关阅读

  • 在 Promise.all、Promise.race 与顺序等待之间如何选择 — 了解何时使用 Promise.all() 能提升 Node.js API 的性能,为何一旦出现拒绝就会迅速失败,以及如何制定决策框架来选择合适的异步模式。
  • 为何回溯式正则表达式会悄悄让服务器挂起 — 了解贪婪匹配与灾难性回溯如何将看似正确的正则表达式变成占用大量 CPU 资源并导致服务中断的问题,以及如何识别这类风险。
  • REST与GraphQL:每种架构背后的真实权衡 — 阐述了REST和GraphQL各自解决的具体问题、其内部运作机制,以及在选择API架构时需要考虑的隐性权衡。
  • 超越P95:衡量用户实际体验的延迟 — 为何良好的P95指标仍可能伴随缓慢的产品性能,队列等待时间和数据分发机制如何被仪表板隐藏,以及如何通过逐步骤计时结束对延迟责任的推诿。
  • 实现 Node.js 服务可靠连接的六种集成模式 — 了解构建稳定 Node.js 集成的核心模式:请求-响应、轮询、Webhook、API 密钥、JWT 与 OAuth 认证、带退避机制的重试,以及数据映射。