后端架构演进:从单体到云原生——一个Cursor重度用户的血泪实践

云边有个仓库
2025-12-14 02:25
阅读 502

上周五晚上十点半,我正瘫在沙发上用 Cursor 写一个微服务的接口逻辑,突然产品经理微信弹窗:“老张,下个月我们要支持百万级并发,能搞不?”
我一口可乐差点喷出来——我们的系统还是三年前那套 Spring Boot 单体应用,数据库主从都没配全

但吐槽归吐槽,活儿还得干。作为一个被 ChatGPT/Claude 养大的远程程序员,早就习惯了“需求来得快,架构崩得更快”的日常。更惨的是,最近还在啃《Cloud Native Patterns》,领导一句“你不是学 AI 的吗?那肯定懂云原生啊”,直接把我架在火上烤。

于是,就有了这篇记录我们后端 Java 系统从单体走向云原生的真实踩坑史。这不是教科书式的理想路径,而是一个普通团队在 deadline 压力下、资源有限、还要保证线上稳定的实战复盘


起点:那个“又慢又脆”的单体时代

我们的产品是个 B2B SaaS 平台,核心功能包括订单管理、库存同步、API 对接第三方物流。最初由三个后端 + 一个全栈(其实就是我)用 Spring Boot 搭起来的,代码结构还算清晰:

/src
  /main/java/com/ourproduct
    ├── controller
    ├── service
    ├── repository
    └── config

听起来挺规范?现实是:所有业务逻辑挤在一个 service 包里,改个库存算法,订单模块可能就炸了。去年双11,因为一个定时任务没加锁,导致库存超卖,运维半夜打电话骂街,我当时真的想砸电脑。

更别提部署了——每次上线都要停服 5 分钟,测试同学天天在群里@我:“你们后端能不能别动不动就 downtime?” 产品经理也学会了甩锅:“技术不行,产品再好也没用。”

痛点总结

  • 耦合严重:改一处,测全站
  • 扩展困难:高峰期 CPU 打满,只能整体扩容,浪费钱
  • 故障扩散:一个接口 OOM,整个服务挂掉
  • 发布痛苦:小改动也要全量回归

第一步:拆!微服务化不是选择,是保命

被线上事故教育三次后,CTO 终于松口:“拆吧,但别影响现有功能。” 于是我们开始了“边飞行边换引擎”的操作。

怎么拆?按业务域!

我们画了 DDD 的限界上下文图(其实是用 Cursor 让 Claude 帮我生成的),最终拆成四个核心服务:

  • order-service:订单创建、状态流转
  • inventory-service:库存扣减、预警
  • integration-service:对接外部物流、支付
  • user-service:用户鉴权、组织管理

关键原则:每个服务独立数据库,禁止跨库 join!以前为了“查询方便”搞的视图关联,现在想想简直是技术债炸弹。

遇到的第一个坑:分布式事务

比如用户下单,要同时扣库存 + 创建订单。在单体里一个 @Transactional 就搞定,现在怎么办?

一开始想用 Seata,但学习成本高,团队没人熟悉。最后妥协方案:本地消息表 + 定时补偿

// order-service 中
@Transactional
public void createOrder(OrderRequest req) {
    Order order = new Order(req);
    orderRepository.save(order);
    
    // 发送“库存扣减”事件到本地消息表
    messageTable.insert(new Message("INVENTORY_DEDUCT", order.getId(), JSON.toJSONString(req)));
}

// 定时任务扫描未发送的消息,调用 inventory-service
@Scheduled(fixedDelay = 5000)
public void retrySendMessages() {
    List<Message> pending = messageTable.findUnsent();
    for (Message msg : pending) {
        try {
            restTemplate.postForObject("http://inventory-service/deduct", msg.payload, Boolean.class);
            messageTable.markAsSent(msg.id);
        } catch (Exception e) {
            // 失败继续重试,最多3次后告警
        }
    }
}

效果:虽然做不到强一致,但通过幂等设计(库存接口加 requestId 去重),最终一致性达标。线上再也没出现过超卖


第二步:拥抱容器化,告别“在我机器上是好的”

拆完服务,部署又成了新噩梦。五个服务,每台机器手动启 jar 包、配环境变量、改端口……运维小哥已经不想理我了。

解决方案:Docker + Kubernetes

我用 Cursor 生成了基础 Dockerfile:

FROM openjdk:17-jdk-slim
COPY target/product-order.jar /app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app.jar"]

然后写 Helm Chart 管理部署:

# values.yaml
replicaCount: 3
image:
  repository: our-registry/order-service
  tag: "v1.2.0"
env:
  DB_URL: "jdbc:postgresql://postgres-order:5432/orderdb"

最大的爽点:开发、测试、生产环境终于一致了!再也不用听测试说:“你本地能跑,CI 上跑不了,是不是你代码有问题?”


第三步:云原生三件套——服务发现、配置中心、链路追踪

微服务一多,IP 管理、配置变更、问题排查成了新痛点。

1. 服务发现:Nacos 替代硬编码 IP

以前调用库存服务,代码里写死 http://192.168.1.10:8081,IP 一变全挂。现在:

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
    return new RestTemplate();
}

// 调用时直接用服务名
restTemplate.getForObject("http://inventory-service/stock/{skuId}", Stock.class, skuId);

配合 Nacos,自动负载均衡,服务上下线无感。

2. 配置中心:动态刷新不用重启

以前改个数据库密码,要重新打包上线。现在用 Nacos Config:

@RefreshScope
@RestController
public class InventoryController {
    @Value("${inventory.threshold:10}")
    private int lowStockThreshold;
}

改配置 → 推送到 Nacos → 应用自动生效。产品经理临时要改库存预警阈值?5 秒搞定,他都惊了

3. 链路追踪:定位慢接口不再靠猜

集成 SkyWalking 后,终于能看到完整调用链:

[order-service] --> [inventory-service] --> [DB]
      |--> [integration-service] --> [External API]

上周发现一个接口平均耗时 2s,查 SkyWalking 发现是 integration-service 调第三方物流超时。立马加熔断 + 降级逻辑,用户体验提升立竿见影

@CircuitBreaker(name = "logisticsApi", fallbackMethod = "fallbackGetTracking")
public TrackingInfo getTracking(String trackingNo) {
    return logisticsClient.get(trackingNo); // 可能超时
}

private TrackingInfo fallbackGetTracking(String trackingNo, Exception e) {
    return TrackingInfo.builder().status("UNKNOWN").build(); // 降级返回默认值
}

数据库设计:从“一库走天下”到分库分表

单体时代,所有表都在一个 PostgreSQL 实例里。拆服务后,每个服务有自己的库,但 order 表数据暴涨到千万级,查询越来越慢。

解决方案

  • tenant_id 分库(多租户场景)
  • order_date 分表(每月一张表)

用 ShardingSphere 实现透明分片:

# application.yml
spring:
  shardingsphere:
    datasource:
      names: ds0, ds1
      ds0:
        jdbc-url: jdbc:postgresql://pg0:5432/orderdb
      ds1:
        jdbc-url: jdbc:postgresql://pg1:5432/orderdb
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..1}.t_order_${202301..202412}
            table-strategy:
              standard:
                sharding-column: order_date
                sharding-algorithm-name: table-inline

效果:单表数据控制在 200 万以内,复杂查询从 5s 降到 200ms。


运维经验:云原生不是终点,可观测性才是

上了 K8s 就万事大吉?天真。真正的挑战在于:如何快速发现并修复问题

我们搭建了完整的可观测性栈:

  • Metrics:Prometheus + Grafana,监控 CPU、内存、JVM GC、HTTP QPS
  • Logs:ELK(Elasticsearch + Logstash + Kibana),统一日志检索
  • Traces:SkyWalking,调用链分析

举个真实案例:某天凌晨报警 CPU 使用率 95%。
→ 查 Prometheus,发现 inventory-service 的 Young GC 频繁
→ 查 Kibana 日志,大量 OutOfMemoryError
→ 查 SkyWalking,某个 SKU 查询接口返回了 10 万条记录
原来是前端传了个空 SKU,后端没做分页限制

修复:加上默认分页 + 参数校验:

@GetMapping("/stock")
public Page<Stock> getStock(@RequestParam(defaultValue = "0") int page,
                            @RequestParam(defaultValue = "20") int size,
                            @RequestParam String sku) {
    if (size > 100) size = 100; // 防刷
    return stockService.findBySku(sku, PageRequest.of(page, size));
}

效果对比:数字不会骗人

指标 单体架构 云原生架构
平均响应时间 850ms 220ms
部署频率 1次/周 10+次/天
故障恢复时间 30min+ <5min
资源利用率 40%(整体扩容) 75%(按需扩缩容)
新人上手时间 2周 3天

最直观的感受:现在产品经理提需求,我敢说“明天就能上”,而不是“得排期两周”。


心得体会:云原生不是银弹,但值得投入

回顾这段旅程,有几个血泪教训:

  1. 不要为了拆而拆:如果业务简单,单体完全够用。我们拆是因为真的扛不住了。
  2. 自动化是生命线:CI/CD、健康检查、自动扩缩容,缺一不可。否则运维成本会爆炸。
  3. 团队能力要跟上:我们每周搞一次“云原生小课堂”,用 Cursor 生成 demo 代码现场调试,效果拔群。
  4. 渐进式演进:先容器化,再服务化,最后上 Service Mesh。一口吃不成胖子。

最后:AI 工具如何加速这个过程?

作为 Cursor 重度用户,我必须吹一波:它让我把精力聚焦在架构设计,而不是 CRUD 模板代码

  • 写 Helm Chart?让 Cursor 根据我的描述生成初版
  • 调 SkyWalking 配置?直接问 Claude 哪些参数最关键
  • 重构旧代码?选中一段逻辑,让 AI 建议如何解耦

它不是替代我,而是让我从“码农”变成“架构师”——至少在心理上是(笑)。


如果你也在经历类似的架构转型,记住:没有完美的方案,只有适合当前阶段的方案。我们现在的系统依然有优化空间(比如还没上 Service Mesh),但比起一年前那个“一碰就碎”的单体,已经是质的飞跃。

技术人的终极浪漫,不就是看着自己写的系统,在千万用户面前稳如老狗吗

(写完这篇,我得去改 product backlog 了——产品经理又发来新需求:“能不能加个 AI 推荐功能?”……算了,先让 Cursor 帮我想想怎么搭向量数据库吧。)

评论 0

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