Spring Cloud Alibaba 在高并发场景下的生产实战手记
去年夏天,我还在用 Vim 调 Swift 代码,为 iOS App 的启动速度优化到 0.1 秒而沾沾自喜。没想到转眼就被老板“亲切关怀”:“老张啊,咱们后端微服务要重构,你不是架构感不错吗?来带一带。”
我当时心里一万只羊驼奔腾——我是写 iOS 的!连 Java 的泛型擦除都还没搞明白呢!但架不住领导画的大饼香,再加上跳槽市场寒冬,只能硬着头皮接下这个“跨端”任务。
结果一查项目需求:基于 Spring Cloud Alibaba 构建高可用、高并发的订单中心,支撑大促峰值 5w QPS。好家伙,这哪是重构,这是直接上战场啊。
为什么选 Spring Cloud Alibaba?
我们组之前用的是纯 Spring Cloud Netflix(Eureka + Hystrix + Zuul),但自从 Netflix 宣布停止维护,加上团队里运维同学天天抱怨 Eureka 注册中心在节点扩容时同步慢得像蜗牛,早就想换了。
调研一圈,Spring Cloud Alibaba(SCA)成了最务实的选择:
- Nacos:服务发现 + 配置中心一体化,支持 AP/CP 切换,比 Eureka 灵活太多
- Sentinel:流量控制、熔断降级比 Hystrix 更细粒度,还带实时监控大盘
- Seata:分布式事务方案,虽然我们暂时没用上,但架构上得预留
- Dubbo 兼容性:公司历史系统多是 Dubbo,SCA 原生支持,迁移成本低
最关键的是——阿里系组件在国内有真实超大流量验证。双11扛得住,我们这点量算啥?
实战第一坑:Nacos 注册中心脑裂
项目刚上线测试环境,就遇到诡异问题:服务列表一会儿有 A 节点,一会儿没了,客户端疯狂报 No instance available。
翻日志发现 Nacos 集群三个节点之间心跳同步异常。后来才知道,我们部署在 Kubernetes 上,Nacos 默认走内网 IP 注册,但 Pod IP 是动态的,一旦滚动更新,旧 IP 就成僵尸节点,集群状态不一致。
解决方案:强制使用固定域名 + hostNetwork 模式(或者用 StatefulSet + Headless Service)。配置如下:
# nacos-peer-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nacos-headless
spec:
clusterIP: None
ports:
- port: 8848
name: server
selector:
app: nacos
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nacos
spec:
serviceName: nacos-headless
replicas: 3
template:
spec:
containers:
- name: nacos
env:
- name: NACOS_SERVERS
value: "nacos-0.nacos-headless:8848 nacos-1.nacos-headless:8848 nacos-2.nacos-headless:8848"
另外,在客户端 application.yml 中显式指定:
spring:
cloud:
nacos:
discovery:
ip: ${HOST_IP} # 通过 Downward API 注入宿主机 IP
port: ${SERVER_PORT}
上线后注册稳定性提升 100%,再也不用半夜被 PagerDuty 叫醒。
Sentinel 流控:从“一刀切”到精细化治理
最初我们对所有接口统一限流:QPS > 1000 就熔断。结果大促预演时,支付回调接口因为第三方响应慢,触发了全局流控,导致用户下单成功但订单状态卡住——差点背锅。
痛定思痛,我们重新设计了 Sentinel 规则体系:
| 接口类型 | 限流策略 | 熔断条件 |
|---|---|---|
| 创建订单 | QPS 限流 2000,排队等待 500ms | 异常比例 > 30% 持续 10s |
| 查询订单详情 | 线程数限流 50 | RT > 800ms 持续 5s |
| 支付回调 | 关联资源流控(依赖支付网关) | 慢调用比例 > 50% |
关键代码(配合注解 + 动态规则加载):
@SentinelResource(
value = "createOrder",
blockHandler = "handleCreateOrderBlock",
fallback = "fallbackCreateOrder"
)
public Order createOrder(OrderRequest req) {
// 业务逻辑
}
// 动态加载规则(从 Nacos 配置中心拉取)
@PostConstruct
public void initFlowRules() {
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource =
new NacosDataSource<>("localhost", "SENTINEL_GROUP", "flow-rules", source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
现在,即使支付网关抖动,也只是回调接口降级,不影响主链路下单。运维同学终于敢在群里发“系统稳如老狗”了。
配置中心:别再把密码写死在 Git 里!
早期开发为了快,数据库密码、Redis 密钥全写在 application-prod.yml 里,然后 commit 到 GitLab。某天安全扫描告警直接飙红,CTO 亲自下场问责。
我们立刻用 Nacos 配置中心接管所有敏感信息,并结合 KMS 加密:
- 所有配置按环境隔离(dev/test/prod)
- 敏感字段用 AES 加密,密钥由 KMS 管理
- 应用启动时自动解密
# Nacos 配置 dataId: order-service-prod.yaml
spring:
datasource:
url: jdbc:mysql://xxx
username: root
password: ENC(AesEncryptedStringHere) # 标记为加密字段
应用端加一个 @ConfigurationProperties 解密器:
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
private String password;
public void setPassword(String password) {
if (password.startsWith("ENC(")) {
this.password = KmsClient.decrypt(password.substring(4, password.length() - 1));
} else {
this.password = password; // dev 环境可明文
}
}
}
从此告别“删库跑路”风险(至少不会因为密码泄露)。
Go?没错,我们在 Java 项目里混进了 Go!
别误会,不是重写后端。而是——用 Go 写了个轻量级 Sidecar,解决 Java 进程内存占用高的问题。
我们的订单服务单实例堆内存 2GB,K8s 资源配额吃紧。但有些边缘功能(比如异步日志上报、指标采集)其实不需要 JVM。
于是,我用周末两天撸了个 Go 程序:
- 监听本地 8081 端口,接收 Java 应用发来的 metrics
- 聚合后批量推送到 Prometheus
- 同时监听 Nacos 配置变更,动态调整采样率
部署方式:同一个 Pod 里跑两个容器(Java 主容器 + Go Sidecar),共享 localhost 网络。
# Dockerfile-go-sidecar
FROM golang:1.21-alpine
WORKDIR /app
COPY . .
RUN go build -o sidecar .
CMD ["./sidecar"]
效果:主 Java 进程内存下降 15%,GC 压力明显减轻。而且 Go 二进制只有 10MB,启动快如闪电。
产品经理看到架构图里冒出个 Go,一脸懵:“这不是后端语言吗?”
我:“现在谁还分前后端啊,能跑就行。”
数据库设计:别让 MySQL 成为瓶颈
高并发下,DB 往往最先扛不住。我们的教训:
- 不要迷信 ORM:MyBatis 虽好,但
SELECT *在大表上就是自杀 - 分库分表要早做:订单 ID 用 Snowflake 算法生成,天然支持分片
- 读写分离必须配:主库写,从库读,但要注意主从延迟
我们最终采用 ShardingSphere-JDBC(SCA 生态兼容),配置如下:
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
url: jdbc:mysql://master-db0
ds1:
url: jdbc:mysql://slave-db0
rules:
readwrite-splitting:
data-sources:
rw_ds:
write-data-source-name: ds0
read-data-source-names: ds1
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..3}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: snowflake-mod
压测显示,QPS 从 3000 提升到 12000+,TP99 从 400ms 降到 90ms。
线上事故复盘:一次“优雅”的雪崩
上周五大促前夜,系统突然全线超时。排查发现:某个下游服务响应变慢 → Sentinel 触发熔断 → 订单服务线程池满 → Tomcat 连接耗尽 → 网关 502。
表面看是下游问题,根因却是线程池配置不合理。默认 Tomcat 最大线程 200,但我们的 Feign 调用都是同步阻塞,一个慢请求占一个线程,200 个慢请求就把服务打挂了。
改进措施:
- Feign + Resilience4j 改为异步非阻塞(反应式编程)
- Tomcat 线程池拆分为:核心业务线程池(高优先级) + 非核心线程池(低优先级)
- 网关层增加“快速失败”策略,避免请求堆积
代码示例(WebFlux + Reactor):
@GetMapping("/order/{id}")
public Mono<Order> getOrder(@PathVariable Long id) {
return orderService.fetchFromCache(id)
.switchIfEmpty(Mono.defer(() -> orderService.fetchFromDb(id)))
.timeout(Duration.ofMillis(300))
.onErrorReturn(FALLBACK_ORDER);
}
现在即使下游挂了,前端也能在 300ms 内收到兜底数据,用户体验不崩。
写在最后:从 iOS 到 SCA,我的跨界感悟
干了两年后端,我反而更理解移动端的设计哲学了——稳定压倒一切。iOS 上一个 ANR 可能丢一个用户,后端一个 5xx 可能让公司损失百万。
Spring Cloud Alibaba 不是银弹,但它提供了一套经过实战检验的“脚手架”。关键在于:你要知道每个组件的边界在哪里,什么时候该自己造轮子。
比如 Sentinel 很强,但如果你的限流维度特别复杂(比如按用户等级 + 商品类目 + 地域组合限流),可能就得自己写 SlotChain。
又比如 Nacos 配置中心方便,但热更新时如何保证配置一致性?我们加了版本号校验 + 回滚机制,才敢在线上用。
所以,别迷信框架。架构的本质,是在约束中寻找最优解。
对了,我现在还是 Vim 党。不过 .vimrc 里多了不少 Java 插件。有时候看着满屏的 {},还是会怀念 Swift 的简洁……
但没办法,成年人的世界,就是一边吐槽,一边把活干漂亮。
本文所有配置和代码均来自真实项目(脱敏处理),已在生产环境稳定运行 6 个月+,支撑过 3 次大促,最高单日订单量 280w+。
如有雷同,纯属同行。

评论 0