从一次重构说起:如何在技术探索中做出真正有价值的选择

赵雨萱·
2025-06-22 19:49
阅读 714

引言:一次“小题大做”的重构尝试

引言:一次“小题大做”的重构尝试

去年我在一家中型互联网公司负责一个后端业务模块的优化工作。当时的项目是一个面向中小商家的线上订单管理系统,用户量不算大但并发请求频繁,尤其是高峰期经常会出现接口超时、数据库连接池被打满等问题。

项目上线已有两年,代码库已经积累了相当的复杂度。我接手的时候,技术栈主要是 Node.js + Express + Sequelize + MySQL,前端是 React,部署在阿里云 ECS 上,使用 Nginx 做负载均衡。

老板说:“这个系统现在撑得过去年双十一流量,但再往上走肯定不行了。你看看能不能重构一下,顺便试试有没有更合适的架构。”

这句话听起来挺轻巧的,但背后其实藏着很多现实问题。我当时也觉得不就是个模块重写嘛,干就完了。结果整个过程比我预想的要曲折得多,但也因此收获了很多对“技术探索与实践”的深刻理解。


问题描述:为什么一定要重构?

问题描述:为什么一定要重构?

系统现状的问题

  • 接口响应时间不稳定,尤其在高峰期,大量请求超时
  • 数据层封装不够合理,Sequelize 的模型嵌套复杂,维护成本高
  • 日志记录混乱,出现问题排查困难
  • 扩展性差,新增一个业务逻辑往往需要改动多个地方
  • 没有监控体系,出了问题全靠被动反馈

这并不是简单的性能瓶颈问题,而是整体结构设计带来的连锁反应。我们当时面临两个选择:

  1. 就地修修补补,加缓存、做异步、优化 SQL;
  2. 架构层面调整,引入服务拆分、模块化改造。

最终我们选择了后者 —— 因为从长远来看,系统本身的可维护性和扩展性才是最关键的。但这一步一旦踏出去,意味着我们要面对一系列全新的挑战。


解决方案:从 Monolith 走向微服务化

解决方案:从 Monolith 走向微服务化

技术选型的思考

我们决定不再局限于原有的框架和模式,转而尝试基于 Node.js + NestJS + TypeORM + Redis + RabbitMQ 的方式来构建一个新的核心服务模块。

选择 NestJS 是因为其模块化的设计非常契合我们想要的组织结构;TypeORM 相比 Sequelize 在 TypeScript 支持方面更成熟;Redis 和 RabbitMQ 则是用来解决缓存和异步任务处理的问题。

此外,我们还开始尝试用 Docker 部署,并借助 Kubernetes 实现服务编排。虽然当时团队里没有 K8s 经验,但我们愿意付出学习成本来换取长期的技术红利。

设计思路

我们采用了**领域驱动设计(DDD)**的思路,将系统划分成几个核心服务:

  • 用户服务:负责用户信息管理
  • 订单服务:核心业务逻辑处理
  • 通知服务:处理推送和消息发送
  • 缓存服务:统一对外提供缓存能力

通过引入**接口网关(API Gateway)**来聚合各个服务的 API,前端只需对接网关即可。

这种架构变化让我们能更好地实现职责隔离,也让每个模块更加清晰可控。


代码实践:从旧代码到新架构的迁移

代码实践:从旧代码到新架构的迁移

这里以订单服务为例,展示一下我们在重构中的一些关键代码变化。

原始代码片段(Express + Sequelize)

// routes/order.js
router.post('/create', async (req, res) => {
  try {
    const order = await Order.create(req.body);
    const user = await User.findByPk(order.userId);
    if (!user) throw new Error('User not found');

    // 更多复杂的嵌套逻辑...
    sendNotificationToUser(user.id, 'Your order is created');
    
    res.json(order);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

这段代码看似简单,但实际上隐藏着巨大的耦合风险:创建订单的同时处理用户验证、调用通知等等,违反了单一职责原则。

新架构下的实现(NestJS)

// create-order.dto.ts
export class CreateOrderDto {
  readonly userId: number;
  readonly productIds: number[];
}

// order.controller.ts
@Post()
async createOrder(@Body() dto: CreateOrderDto) {
  return this.orderService.create(dto);
}

// order.service.ts
@Injectable()
export class OrderService {
  constructor(
    private readonly orderRepo: OrderRepository,
    private readonly userClient: UserServiceClient,
    private readonly eventPublisher: EventPublisher
  ) {}

  async create(dto: CreateOrderDto) {
    const user = await this.userClient.getUserById(dto.userId);
    if (!user) throw new NotFoundException('User not found');

    const order = this.orderRepo.create(dto);
    await this.orderRepo.save(order);

    this.eventPublisher.publish(new OrderCreatedEvent(order.id));
    
    return order;
  }
}

这种写法不仅结构清晰,而且把责任进行了合理的分离,便于后期测试和维护。


踩坑经验:别让新技术变成新负担

1. RabbitMQ 消费堆积

初期我们在订单服务发布事件后通过 RabbitMQ 广播给通知服务。但在压测过程中发现,当并发量上万时,队列出现了严重堆积。

原因分析: 默认的消费者确认机制未开启手动提交,导致部分消息被重复消费甚至丢失。

解决方案: 开启手动 ACK,并增加消费者并发数,配合死信队列(DLQ)做异常兜底。

// 使用 amqplib 的示例代码
channel.consume(queueName, async msg => {
  try {
    const data = JSON.parse(msg.content.toString());
    await handle(data);
    channel.ack(msg);
  } catch (err) {
    console.error('failed to process message:', err);
    channel.reject(msg, false); // 进入 DLQ
  }
}, { noAck: false });

2. NestJS 的依赖注入陷阱

NestJS 的 DI 确实强大,但也有坑。比如,我们在某些 Service 中引入了全局配置类,结果在单元测试时发现配置读取不到。

根本原因: 测试环境下模块注册不完整,导致依赖注入链条断裂。

解决办法: 明确依赖项,并在 TestingModule 中模拟相关 Provider。


效果总结:系统焕然一新,团队更有信心了

重构完成后,我们做了全面的压测和灰度上线:

  • QPS 提升了约 3.5 倍
  • P99 接口响应时间下降了 60%
  • 线程阻塞情况基本消失
  • 新功能开发效率提高了 40%

更重要的是,我们建立了一套规范化的服务通信机制和日志追踪体系,后续维护成本大大降低。


经验分享:技术探索的底线是“解决问题”,不是“炫技”

不要为了“重构”而重构

很多人看到系统老旧就想推翻重来,但如果你只是为了追求“先进性”或者“简历好看”,那很可能适得其反。真正的重构应该是为了提升系统的稳定性、可维护性、可扩展性

合理评估技术成本

我们当初为了推动 K8s 化部署,花了整整一个月时间搭建环境、调试配置。中间几次差点放弃,但最后证明这笔账是值得的。不过你也得考虑当前团队是否有足够的能力和人力来支撑这个投入。

多用工具,少造轮子

在整个过程中,我们并没有自己写 ORM 或者消息总线,而是选择成熟的开源工具组合。比如:

  • 使用 Winston 做日志管理
  • Prometheus + Grafana 做监控
  • OpenTelemetry 做链路追踪
  • ESLint + Prettier 做代码规范

这些工具帮助我们快速建立起一套稳定的开发运维流程,避免了不必要的重复劳动。

给正在探索的你几点建议

  1. 带着问题去探索:别盲目学技术,而是先想清楚你在实际工作中遇到了哪些痛点。
  2. 小范围先行试点:可以先在某一个模块或服务中尝试新技术,验证后再推广。
  3. 关注落地成本:有时候“更优解”并不一定适合你的项目阶段,权衡好短期收益与长期成本。
  4. 文档和沟通是关键:新技术引入后,一定要同步更新文档,确保团队成员都能理解并接受。
  5. 保留回滚路径:任何重构都存在风险,务必提前做好备份和降级预案。

写在最后:技术探索是一场修行

这次重构让我深刻体会到:技术从来不是冰冷的代码和框架,它是我们面对复杂业务需求时的一种“思维方式”。

每当我们站在技术路口做选择时,不仅要问“这个好不好用”,更要问“这个是否真的能帮我们解决问题”。那些深夜查文档、改配置的日子也许很累,但它们最终沉淀成了我们的经验和底气。

希望这篇文章能给你一些启发,愿你也能在自己的技术旅途中找到属于自己的答案。共勉!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝