从单体到微服务:一个深夜码农的两年重构血泪史

AlgoMaster
2026-01-03 06:47
阅读 623

凌晨2点,咖啡见底,屏幕右下角Copilot的小图标还在默默闪着绿光——这玩意儿陪我熬过了太多个这样的夜晚。作为GitHub Copilot付费用户快两年了,它早已不是“辅助”,而是我深夜写代码时最靠谱的搭子。尤其是在去年我们组启动那个“史诗级”重构项目时,没有它,我可能早就被产品经理的PRD和运维兄弟的日志告警逼疯了。

事情得从去年双11说起。我们那个跑了五年的Java单体应用,在流量洪峰面前直接跪了——数据库连接池打满,线程阻塞,GC停顿十几秒,用户下单页面转圈圈转到怀疑人生。老板在战报会上黑着脸说:“再这么下去,明年双11我们就不用过了。”于是,微服务化,成了唯一出路。

但说实话,一开始我是抗拒的。不是技术问题,是心理阴影——之前看过太多团队“为拆而拆”,最后搞出一堆分布式事务地狱、链路追踪黑洞、配置管理泥潭。可现实没得选:业务增长太快,单体架构的耦合度已经高到改一行代码要全量回归测试三天。领导拍板:“拆!哪怕踩坑,也得往前走。”


为什么是现在?为什么是Java?

先说清楚背景:我们的核心系统是基于Spring Boot + MyBatis的传统Java单体,MySQL主从,Redis缓存,Nginx负载均衡。听起来很稳?其实内部模块边界模糊得像一锅粥——订单、库存、用户、营销全挤在一个war包里,连日志都混在一起打。

微服务不是银弹,但对我们来说,解耦独立部署是刚需。比如营销活动经常临时加需求,每次上线都得拉整个系统重启,测试同学看到我就绕道走。

技术栈选择上,我们没折腾新语言,继续用Java。原因很简单:团队熟悉、生态成熟、性能可控。而且Spring Cloud Alibaba那一套(Nacos、Sentinel、Seata)在国内生产环境验证充分,比纯Spring Cloud更接地气。别听某些面试题吹“Go更适合微服务”——真到了线上调优、JVM监控、Arthas排查阶段,Java老炮儿的优势就出来了。


拆分策略:别一上来就画大饼

很多教程一上来就说“按业务域拆”,听着高大上,实际落地容易翻车。我们一开始也想按DDD四层架构搞,结果发现领域模型还没对齐,产品经理又改需求了。

最后采用渐进式拆分

  1. 识别高频变更模块:营销、通知这类需求多变的优先剥离
  2. 识别资源密集型服务:比如图片处理、报表生成,单独部署避免拖垮核心交易
  3. 识别数据隔离明显的模块:用户中心天然适合独立数据库

第一刀,我们切出了用户服务。理由很朴素:它被所有模块调用,但自身逻辑相对独立,且数据敏感度高,单独部署后能做更细粒度的权限和审计。

拆的时候,最头疼的不是代码,是数据迁移与双写。比如原来订单表里存着user_name,现在得通过RPC查用户服务。我们搞了个中间态:新写入走新服务,旧数据保留字段,读取时先查本地,miss再调远程。配合灰度开关,逐步切换。

// 用户信息查询代理类(带本地缓存+远程兜底)
@Service
public class UserProxy {

    @Autowired
    private UserServiceFeignClient userClient;

    @Cacheable(value = "user", key = "#userId")
    public UserDTO getUserById(Long userId) {
        // 先尝试从本地缓存或DB历史字段获取
        UserDTO local = tryGetFromLocal(userId);
        if (local != null) {
            return local;
        }
        // 否则调用用户微服务
        return userClient.getUserById(userId);
    }

    private UserDTO tryGetFromLocal(Long userId) {
        // 从订单表冗余字段 or 本地缓存尝试获取
        // TODO: 这段代码将在三个月后下线
        return legacyUserRepository.findByNameByOrderId(userId);
    }
}

这种“缝合怪”代码当然丑,但保证了平滑过渡。上线那周,我每天半夜盯着Grafana面板,生怕某个接口RT飙升。还好,有Copilot帮我快速生成大量兼容性测试用例,不然光手写Mock就得累死。


分布式事务:别信“最终一致性万能论”

拆完服务,第一个大坑就是跨服务数据一致性。比如创建订单要扣库存、发优惠券、记积分——三个服务,怎么保证要么全成功,要么全失败?

网上一堆人说“用消息队列做最终一致性就行”,但现实是:用户支付成功后发现库存没扣,或者积分没到账,客服电话直接被打爆。金融级场景,强一致还是绕不开

我们最终采用了 Seata AT模式 + 补偿机制 的混合方案:

  • 核心交易链路(下单→扣库存)用Seata全局事务,保证ACID
  • 非核心操作(发通知、埋点)走MQ异步,配合定时对账补偿
# seata.conf
service.vgroup_mapping.my_tx_group = "default"
client.rm.async.commit.buffer.limit = 10000

但Seata也不是神。有一次因为网络抖动,分支事务状态没及时上报,导致全局锁一直不释放,后续请求全部卡住。当时真的想砸电脑。后来我们加了超时熔断+人工干预入口:超过5分钟未完成的事务,自动触发补偿脚本,并告警到钉钉群。

顺便吐槽一句:现在好多Java面试题问“如何实现分布式事务”,标准答案背得飞起,但真让你处理Seata日志里的BranchReportFailedException,估计一半人懵圈。


性能与可观测性:微服务不是性能杀手

很多人担心微服务会带来性能损耗。确实,一次HTTP调用 vs 一次方法调用,延迟差了10倍不止。但我们通过几个关键优化,把影响压到最低:

优化手段 效果 备注
Feign + OkHttp 连接池 RT降低35% 替换默认HttpURLConnection
本地缓存 (Caffeine) 减少70%远程调用 热点数据如商品基础信息
异步非阻塞调用 (CompletableFuture) 吞吐提升2倍 并行调用多个非依赖服务
gRPC替代部分REST 序列化开销减少50% 内部高性能服务间通信

特别提一下链路追踪。刚拆完那会儿,一个慢请求要翻十几个服务的日志,运维差点辞职。接入SkyWalking后,终于能在UI上看到完整调用树,还能直接定位到是哪个SQL慢了、哪个Feign调用超时。

// 在关键方法加@Trace注解(SkyWalking)
@Trace
public void processOrder(Order order) {
    // 业务逻辑
}

现在排查问题,基本5分钟定位瓶颈。上周五晚上,我就靠它发现一个N+1查询问题——某个接口没加@BatchSize,导致循环调用用户服务200次。当场修复,第二天早上提PR,测试同学直呼“神速”。


那些没人告诉你的运维细节

微服务上线只是开始,真正的挑战在运维:

  • 配置爆炸:每个服务都有dev/test/prod三套配置。我们用Nacos统一管理,但初期经常有人改错环境配置。后来强制要求所有配置变更走GitOps流程,合并前自动校验。
  • 服务雪崩:某次下游服务慢,导致上游线程池打满。现在所有Feign调用都配了超时+熔断
    feign:
      client:
        config:
          default:
            connectTimeout: 1000
            readTimeout: 3000
      sentinel:
        enabled: true
    
  • 日志分散:ELK + TraceID串联是标配。但要注意日志量暴增——我们曾因debug日志没关,一天产生2TB日志,差点被运维封账号。

回头看:值得吗?

两年过去,系统拆成了20+个微服务。双11再次来临,系统稳如老狗——订单峰值TPS提升4倍,故障隔离效果显著(上次库存服务宕机,订单还能正常创建,只是暂时不能扣减)。

但代价也不小:研发效率短期下降,联调成本上升,新人上手门槛变高。不过长期看,独立部署带来的业务敏捷性远超成本。现在市场部提个新玩法,我们两天就能上线,再也不用拉着全组开会评估影响范围。

如果你也在考虑微服务,我的建议是:

  • 别为了技术潮流而拆,先问业务是否需要
  • 从小模块试水,别一上来重构核心交易
  • 工具链先行:监控、日志、CI/CD没准备好,别动生产代码
  • 算法能力很重要:微服务调度、负载均衡、服务网格背后都是图论和优化算法——所以那些LeetCode Hard题,真不是白刷的

最后,感谢Copilot这位深夜战友。每次我敲出// TODO: handle distributed transaction rollback,它总能精准补全补偿逻辑。虽然偶尔也会生成一些“看起来能跑但逻辑反人类”的代码(比如把补偿操作放在try块里),但总体靠谱度90分以上。

微服务不是终点,而是应对复杂性的手段之一。架构没有银弹,只有权衡。而我们这些码农,就在一次次权衡中,把混乱变成秩序——哪怕是在凌晨三点的咖啡渍旁边。

(完)

评论 0

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