后端架构演进:从单体到云原生——一个Cursor重度用户的血泪实践
上周五晚上十点半,我正瘫在沙发上用 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天 |
最直观的感受:现在产品经理提需求,我敢说“明天就能上”,而不是“得排期两周”。
心得体会:云原生不是银弹,但值得投入
回顾这段旅程,有几个血泪教训:
- 不要为了拆而拆:如果业务简单,单体完全够用。我们拆是因为真的扛不住了。
- 自动化是生命线:CI/CD、健康检查、自动扩缩容,缺一不可。否则运维成本会爆炸。
- 团队能力要跟上:我们每周搞一次“云原生小课堂”,用 Cursor 生成 demo 代码现场调试,效果拔群。
- 渐进式演进:先容器化,再服务化,最后上 Service Mesh。一口吃不成胖子。
最后:AI 工具如何加速这个过程?
作为 Cursor 重度用户,我必须吹一波:它让我把精力聚焦在架构设计,而不是 CRUD 模板代码。
- 写 Helm Chart?让 Cursor 根据我的描述生成初版
- 调 SkyWalking 配置?直接问 Claude 哪些参数最关键
- 重构旧代码?选中一段逻辑,让 AI 建议如何解耦
它不是替代我,而是让我从“码农”变成“架构师”——至少在心理上是(笑)。
如果你也在经历类似的架构转型,记住:没有完美的方案,只有适合当前阶段的方案。我们现在的系统依然有优化空间(比如还没上 Service Mesh),但比起一年前那个“一碰就碎”的单体,已经是质的飞跃。
技术人的终极浪漫,不就是看着自己写的系统,在千万用户面前稳如老狗吗?
(写完这篇,我得去改 product backlog 了——产品经理又发来新需求:“能不能加个 AI 推荐功能?”……算了,先让 Cursor 帮我想想怎么搭向量数据库吧。)

评论 0