从单体到微服务:一个奶爸程序员的深夜重构手记

Commit写错了
2025-12-29 10:30
阅读 2553

上周五晚上十点半,娃终于睡了,我蹑手蹑脚摸出MacBook Pro,准备把白天没搞完的微服务拆分代码提交掉。老婆在隔壁房间喊:“别熬太晚!”——我嘴上答应着,心里却想着:再不把这单体项目拆开,下个月大促又要“原地爆炸”了。

我是两个娃的奶爸,主业是Java后端开发,在这个组快两年了。白天上班写代码、怼需求、跟产品经理battle“这个功能很简单”,晚上回家换尿布、哄睡觉、辅导老大写作业。能用来学习的时间,基本都在娃睡后的两小时黄金窗口。所以,这篇文章不是什么高屋建瓴的架构论文,而是我在有限时间里,用血泪踩坑总结出来的微服务实战最佳实践


起因:那个撑不住的“巨无霸”项目

我们原来的系统是个典型的Spring Boot单体应用,三年前启动时只有几个模块,跑得飞快。但随着业务膨胀,代码量突破30万行,数据库表超过80张,每次发布都要停机10分钟,CI/CD流水线跑一次要25分钟。最离谱的是,有一次因为一个订单状态更新的小bug,整个用户中心都挂了——典型的“牵一发而动全身”。

去年双11前一周,运维兄弟半夜打电话:“老哥,CPU又飙到98%了,日志堆成山!”那一刻我就知道:单体架构的天花板到了

领导拍板:“拆!往微服务走。”于是,我这个“资深(被迫)架构师”开始带着三个小伙伴,开启拆分之旅。


拆还是不拆?先画边界!

很多人一说微服务,就急着上Spring Cloud、Nacos、Sentinel。但我觉得,拆分边界比技术选型更重要。我们花了整整一周,用“领域驱动设计(DDD)”的思想重新梳理业务:

  • 用户中心(User Service)
  • 商品服务(Product Service)
  • 订单服务(Order Service)
  • 支付网关(Payment Gateway)

每个服务独立数据库,通过Feign + OpenFeign调用,配合Resilience4j做熔断降级。关键原则:高内聚、低耦合,一个服务只干一件事

💡 奶爸经验:别贪多!我们第一期只拆了用户和订单两个核心服务,剩下的慢慢来。毕竟晚上学习时间有限,不能一口吃成胖子。


数据库怎么拆?别让事务把你整崩溃

最头疼的其实是分布式事务。原来单体里一个@Transactional搞定的事,现在跨服务了怎么办?

我们试过Saga模式,也研究过TCC,但考虑到团队规模和维护成本,最终选择了柔性事务 + 最终一致性

  • 关键操作发MQ(RabbitMQ)
  • 消费方幂等处理
  • 状态补偿机制兜底

比如用户下单后,订单服务发一条order_created消息,用户服务监听后扣减积分。如果用户服务暂时不可用,消息会重试,直到成功或进入死信队列人工干预。

// 订单服务 - 创建订单后发消息
@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    rabbitTemplate.convertAndSend("order.exchange", "order.created", order.getId());
}

// 用户服务 - 监听并处理
@RabbitListener(queues = "user.order.queue")
public void handleOrderCreated(String orderId) {
    if (idempotentService.isProcessed(orderId)) return; // 幂等检查
    userService.deductPointsForOrder(orderId);
    idempotentService.markAsProcessed(orderId);
}

上线初期,因为没做好幂等,导致有用户被重复扣积分……测试妹子差点拿扫帚追着我打。还好监控告警及时,人工回滚了数据。血的教训:分布式场景下,幂等不是可选项,是必选项!


接口设计:别让下游服务骂你祖宗

微服务之间靠API说话,接口设计不好,协作效率直接拉胯。我们定了三条铁律:

  1. 版本化:所有接口带v1/前缀,避免升级时炸裂
  2. 契约先行:用OpenAPI 3.0写好文档,前后端、测试都照着来
  3. 错误码统一:自定义BusinessException,返回结构如:
{
  "code": "ORDER_NOT_FOUND",
  "message": "订单不存在",
  "data": null
}

以前经常出现“你这接口怎么突然改字段了?”、“为啥返回null不报错?”——现在有了契约,大家各司其职,联调时间少了60%。


运维与可观测性:别等线上炸了才看日志

微服务拆完,服务一多,日志分散在各个Pod里,排查问题像大海捞针。我们赶紧补上了可观测性三件套:

工具 用途 奶爸点评
ELK 日志收集与搜索 必备!不然半夜救火全靠猜
Prometheus + Grafana 指标监控(QPS、延迟、错误率) 大屏一挂,老板看了直呼专业
SkyWalking 链路追踪 一眼看出哪个服务拖后腿

特别推荐TraceID透传!我们在网关生成全局TraceID,通过MDC注入日志,再通过Feign拦截器传递到下游。查问题时,只要一个ID,就能串起整条调用链。

// Feign拦截器自动传递TraceID
public class TraceFeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        String traceId = MDC.get("traceId");
        if (traceId != null) {
            template.header("X-Trace-ID", traceId);
        }
    }
}

效果如何?值得吗?

拆分完成后,效果立竿见影:

  • 发布时间从10分钟 → 2分钟(只发变更的服务)
  • 故障隔离:订单服务挂了,用户还能浏览商品
  • 团队协作更清晰,新人上手更快
  • 双11零重大事故(运维请我喝了奶茶)

当然,代价也有:运维复杂度上升,调试成本增加,网络延迟略增。但对我们这种中等规模业务来说,利远大于弊


给同行的几句真心话

  1. 别为了微服务而微服务。如果你的系统日活不到1万,单体+模块化可能更香。
  2. 自动化是生命线。没有CI/CD、没有监控,微服务就是“微灾难”。
  3. 文档和沟通比代码更重要。服务一多,信息同步必须靠规范。
  4. 留点时间陪家人。我见过太多同事熬夜调服务,结果体检报告亮红灯。技术再牛,也得活着不是?

写完这篇,已经是凌晨一点。合上电脑,轻手轻脚去厨房热了杯牛奶。明天还要早起送老大上学,然后继续在工位上和那些“永远改不完的需求”斗智斗勇。

但我知道,每一次架构演进,都是为了让系统更稳、让用户更爽,也让自己少加点班——多陪陪那两个小家伙。

代码人生,不止有Bug和Deadline,还有奶粉和晚安吻。

共勉。

评论 0

最热最新
暂无评论
Commit写错了Lv.1
0
影响力
0
文章
0
粉丝