微服务架构设计实战:从单体到分布式

产品和代码之间
2025-12-16 08:59
阅读 754

哈喽大家好,我是刚入职鹅厂(咳咳,其实是腾讯系某子公司)不到三个月的试用期小菜鸟。坐标深圳南山科技园,工位还没捂热乎,就被赶鸭子上架参与一个“史诗级重构项目”。这篇文章就是我在被微服务折磨了整整两周后,含泪写下的血泪经验总结。如果你也正准备从单体应用迈向分布式世界,希望这篇能帮你少踩几个坑——或者至少知道怎么优雅地摔。


被逼上梁山:为什么我们要拆?

事情得从去年双11说起(虽然我还没经历过,但听老哥们吹得天花乱坠)。我们团队维护的那套核心交易系统,原本是个典型的 Spring Boot 单体应用:所有模块塞在一个 jar 包里,数据库就一张大库,部署靠运维大哥手动 scp + nohup java -jar 启动。听起来是不是有点复古?

结果去年大促当晚,系统直接崩了——不是挂,是慢得像 2G 网络刷抖音。用户下单卡在“支付中”半小时,客服电话被打爆。事后复盘会上,CTO 拍桌子:“再不拆微服务,明年双11我们就集体去送外卖!”

于是今年年初,领导拍板启动“凤凰计划”(名字很燃,活很苦),目标:把那个 50 万行代码的巨无霸,拆成十几个高内聚、低耦合的微服务。

而我,作为一个连公司内网 GitLab 都还没完全搞明白的新人,居然被分配到了“用户中心”微服务模块。理由是:“你简历写了熟悉 Spring Boot,又会点 Python,正好搞搞胶水逻辑。”

我当时内心 OS:“我会 Python 是因为爬过 GitHub 上的开源项目练手啊,不是用来写生产环境脚本的!”


初步拆分:别想一口吃成胖子

一开始我以为微服务就是把代码按功能切一切,打几个 jar 包,完事。天真如我。

第一天晨会,架构师老张(人称“张微服”)就泼了我一盆冷水:“微服务不是技术问题,是组织问题。你拆得再漂亮,服务间调用乱成一锅粥,照样线上爆炸。”

他甩给我一份《微服务拆分原则》,核心就三点:

  1. 业务边界清晰:比如用户管理、订单处理、库存扣减,各自独立。
  2. 数据自治:每个服务有自己的数据库,禁止跨库 join。
  3. 故障隔离:一个服务挂了,不能拖垮整个链路。

于是我们决定先从“用户中心”下手——它依赖最少,改动风险相对可控。原系统里,用户相关的代码散落在 user/, profile/, auth/ 多个包下,还和订单模块有千丝万缕的关联。

我的任务:用 Spring Boot 3.x 重新搭建一个独立服务,对外提供 RESTful API,内部用 MyBatis Plus 操作专属 MySQL 实例。


坑1:服务怎么发现彼此?Nacos 还是 Eureka?

原系统用的是硬编码 IP + 端口调用其他模块,简直是“分布式反面教材”。

“必须上注册中心!”老张斩钉截铁。

选型时团队吵了一架:

  • 老派 Java 工程师力推 Eureka(Netflix 系,情怀加成)
  • 新锐派主张 Nacos(阿里开源,支持配置中心+服务发现一体化)

最后 PM 一锤定音:“隔壁团队已经在用 Nacos 了,文档齐全,出了问题还能蹭他们的值班。” —— 真实世界的决策往往如此务实。

于是我在 pom.xml 里加上:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2022.0.0.0</version>
</dependency>

然后在 application.yml 配置:

spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: nacos.prod.internal:8848

启动后,服务自动注册到 Nacos 控制台。那一刻,我仿佛看到了微服务的曙光……直到第二天。


坑2:接口怎么调?Feign 还是 RestTemplate?

用户服务需要调用“风控服务”做登录校验。我第一反应是写个 RestTemplate

ResponseEntity<String> response = restTemplate.getForEntity(
    "http://risk-service/api/v1/check?uid=123", String.class);

结果被 Code Review 打回来了。老张批注:“硬编码服务名?你这是给线上事故埋雷!

正确姿势是用 OpenFeign + Ribbon 负载均衡(虽然 Ribbon 已停更,但 Spring Cloud Alibaba 还在用):

@FeignClient(name = "risk-service")
public interface RiskServiceClient {
    @GetMapping("/api/v1/check")
    RiskCheckResult checkUser(@RequestParam("uid") Long uid);
}

然后注入使用:

@Autowired
private RiskServiceClient riskClient;

public boolean isAllowed(Long userId) {
    return riskClient.checkUser(userId).isAllowed();
}

这样,Feign 会自动从 Nacos 拉取 risk-service 的实例列表,并做负载均衡。再也不用手动拼 URL 了!

不过这里有个细节:超时配置一定要设! 默认 Feign 超时只有 1 秒,而风控服务偶尔会慢到 2 秒。有一次大半夜报警,就是因为超时导致大量登录失败。后来补上:

feign:
  client:
    config:
      default:
        connectTimeout: 2000
        readTimeout: 5000

坑3:数据怎么同步?别再跨库查了!

原系统里,订单详情页直接 JOIN user_table 拿用户名。现在用户表移到了独立库,这招彻底失效。

产品经理一脸无辜:“用户昵称显示不出来?这不是基本功能吗?”

我差点脱口而出:“那你去跟数据库管理员说,让他开跨库权限!”——还好忍住了。

最终方案:事件驱动 + 最终一致性

当用户修改昵称时,user-service 发一条 Kafka 消息:

// 用户更新后
kafkaTemplate.send("user.profile.updated", new UserProfileEvent(userId, nickname));

订单服务消费这条消息,更新本地冗余字段:

# 订单服务用 Python 写的消费者(对,我们真用了 Python!)
from kafka import KafkaConsumer
import json

consumer = KafkaConsumer('user.profile.updated', 
                         bootstrap_servers='kafka.prod:9092',
                         value_deserializer=lambda m: json.loads(m.decode('utf-8')))

for msg in consumer:
    event = msg.value
    # 更新订单表中的 user_nickname 字段
    update_order_nickname(event['user_id'], event['nickname'])

为什么用 Python? 因为团队里有个 Python 老哥说:“这种轻量消费者,Python 几行搞定,何必折腾 JVM?” 领导居然同意了……果然,技术选型有时候看的是谁嗓门大

不过要小心:Kafka 消息可能重复、乱序。我们在数据库加了 version 字段做幂等控制,确保同一条消息不会覆盖新数据。


坑4:资源隔离没做好,差点背锅

上线前压测,用户服务 QPS 到 800 就 CPU 打满。运维大哥冲过来质问:“你这服务是不是内存泄漏了?”

我慌得一批,赶紧查监控。结果发现:线程池没隔离!

默认 Tomcat 线程池同时处理 HTTP 请求 + Feign 调用。当风控服务变慢,Feign 占用大量线程,导致新请求进不来——典型的“雪崩效应”。

解决方案:Hystrix 或 Resilience4j 做熔断隔离。但 Hystrix 已停止维护,我们选了 Resilience4j:

@Bean
public Customizer<Resilience4jBulkheadConfigurationBuilder> bulkheadCustomizer() {
    return builder -> builder.addBulkheadConfigurations(
        Collections.singletonList(BulkheadConfig.of("riskClient")
            .maxConcurrentCalls(20) // 最多20个并发调用风控
            .maxWaitDuration(Duration.ofMillis(100))
        )
    );
}

配合 Feign 使用:

@FeignClient(name = "risk-service", configuration = FeignConfig.class)
public interface RiskServiceClient {
    // ...
}

@Configuration
public class FeignConfig {
    @Bean
    public BulkheadFeignDecorator bulkheadFeignDecorator() {
        return new BulkheadFeignDecorator(BulkheadRegistry.ofDefaults());
    }
}

这样一来,即使风控服务挂了,最多只占 20 个线程,不影响主流程。资源隔离,真的能救命。


GitHub 上的宝藏:站在巨人肩膀上

过程中遇到不少难题,比如 JWT 鉴权怎么在微服务间传递、日志如何追踪全链路 ID……与其自己造轮子,不如去 GitHub 找答案。

我收藏了几个超实用的仓库:

仓库 用途 Star 数
alibaba/Sentinel 流量控制、熔断降级 21k+
spring-cloud/spring-cloud-gateway API 网关 9k+
apache/skywalking 分布式追踪 20k+

特别是 SkyWalking,集成后可以在 UI 上看到一次请求经过哪些服务、耗时多少。上次排查“为什么登录变慢”,一眼就发现卡在风控服务的数据库查询上——没有链路追踪的微服务,就像蒙眼开车


效果如何?线上稳了!

经过三周地狱式开发 + 两次凌晨上线(感谢运维兄弟陪我熬大夜),用户服务终于平稳运行。

对比数据如下:

指标 单体时代 微服务后
部署时间 15分钟(全量) 2分钟(单服务)
故障影响范围 全站不可用 仅用户相关功能
日均错误率 0.8% 0.05%
开发并行度 3人共用一个 repo 5个服务独立迭代

最爽的是:现在改个用户头像逻辑,不用拉整个大团队回归测试了!


给新人的几句真心话

  1. 微服务不是银弹:如果你的业务还没到一定规模,强行拆分只会增加复杂度。我们团队现在还在为“要不要把通知服务再拆成短信/邮件/推送”吵架……
  2. 监控和告警比代码更重要:没有完善的 Metrics、Logging、Tracing(俗称可观测性三件套),微服务就是定时炸弹。
  3. 沟通成本暴涨:以前改个字段自己搞定,现在要跟三个团队对齐接口。建议每天站会多聊两句,少写两行代码。
  4. 别怕用非 Java 技术:Python 脚本、Shell 工具、甚至 Go 写的 sidecar,只要能解决问题,都是好工具。我们团队现在有个共识:“语言只是工具,交付才是王道”

结语

写这篇文章的时候,已经是凌晨一点。窗外深圳湾的灯火依旧通明,工位旁的咖啡杯堆成了小山。作为试用期员工,我其实压力山大——听说上个月有个同事因为线上事故没过试用期。

但这次重构让我真正理解了什么叫“分布式系统的艺术”:它不只是技术,更是对业务的理解、对风险的敬畏、对协作的耐心。

如果你也在经历类似的转型,别慌。记住:每一个优雅的微服务架构背后,都有一群熬过通宵、骂过产品经理、对着 Nacos 控制台傻笑的程序员。

GitHub 上 star 一下你的项目,说不定哪天我也能抄你的作业 😄


本文代码片段已脱敏,部分配置简化。实际生产环境请结合公司规范调整。
作者:某腾讯系公司试用期小透明,欢迎关注我的 GitHub @coder-in-shenzhen(假的,别找)

评论 0

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