文章目录
- 一、任务与拆分:单体到底哪里疼
- 二、环境准备:先把 Nacos 跑起来
- 三、服务注册:两个工程都把自己报上去
- 四、OpenFeign:把 HTTP 调用写成「本地方法」
- 4.1 引依赖、开注解
- 4.2 三个必踩的坑,提前绕开
- 五、Gateway:给所有服务装一个门卫
- 六、熔断降级:别让一次库存抖动拖垮整个商城
- 七、落地清单:上线前把这张表过一遍
聊微服务架构的文章大多停在画框图。这篇换个路子:先给一个真实任务——用户下单后要扣库存,从一个最普通的单体写起,沿着痛点一步步拆成微服务,每一步拆分都对应 Spring Cloud 里一个具体的组件。
最后你会得到一套能跑的代码:Nacos 注册中心 + OpenFeign 服务调用 + Gateway 网关,Spring Boot 3.2 + Spring Cloud 2023 版本线,附每个组件最容易踩的坑。
版本前置说明:Spring Boot 3.x 要求 JDK 17+。版本搭配按官方对照表走,别自由组合——这是微服务环境问题的一大来源。
| 组件 | 版本 |
|---|---|
| JDK | 17+ |
| Spring Boot | 3.2.x |
| Spring Cloud | 2023.0.x |
| Spring Cloud Alibaba | 2023.0.1.x |
| Nacos Server | 2.3.x |
一、任务与拆分:单体到底哪里疼
先写单体版。一个 Spring Boot 工程,两个 Service:
@ServicepublicclassOrderService{@AutowiredprivateStockServicestockService;publicStringcreateOrder(Stringsku,intcount){stockService.deduct(sku,count);// 进程内直接调用orderMapper.insert(buildOrder(sku,count));return"ok";}}跑起来没问题。疼在后面:
- 发布耦合。库存逻辑改一行,订单模块也得跟着重新部署。大促前只想扩容库存服务的机器?做不到,整个应用一起扩。
- 故障扩散。库存模块一个死循环把线程占满,订单接口跟着超时——它们共享同一个进程的线程池。
- 技术栈锁死。想给库存模块换 Go、给报表模块换个存储,不可能,全在一个进程里。
拆分方案:按业务能力拆成 order-service 和 stock-service 两个独立进程,订单服务通过 HTTP/RPC 调库存服务。拆完立刻冒出四个新问题,每个问题对应一个 Spring Cloud 组件:
| 新问题 | 组件 |
|---|---|
| stock-service 部署了 3 台,order 怎么知道调哪台? | Nacos注册中心 + 负载均衡 |
| 跨进程 HTTP 调用,代码怎么写得像本地方法一样? | OpenFeign声明式调用 |
| 外部请求从哪进来、怎么转发、谁做统一鉴权? | Gateway网关 |
| 服务挂了/变慢了,怎么不让故障顺着调用链扩散? | 熔断降级(Sentinel / Resilience4j) |
下面按这个顺序逐个落地。
二、环境准备:先把 Nacos 跑起来
Nacos 承担注册中心和配置中心两个角色,是整个体系的「通讯录」。
下载安装(官网下载 zip 即可,要求已装 JDK 17+):
# Windows(Git Bash / CMD)startup.cmd-mstandalone# macOS / Linuxshstartup.sh-mstandalone-m standalone是单机模式,本地开发够用。启动后访问http://localhost:8848/nacos(默认账号密码 nacos/nacos,生产环境第一件事改密码)。
生产部署提醒一句:单机模式数据默认存在内嵌 Derby 里,重启不丢但不好迁移,生产切 MySQL 存储(conf/application.properties里配spring.datasource.platform=mysql),并至少跑主备。
三、服务注册:两个工程都把自己报上去
两个工程的 pom 依赖结构相同,以 stock-service 为例:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.5</version></parent><properties><spring-cloud.version>2023.0.1</spring-cloud.version><spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>启动类加@EnableDiscoveryClient(新版其实可以不加,显式写出来更清楚):
@SpringBootApplication@EnableDiscoveryClientpublicclassStockApplication{publicstaticvoidmain(String[]args){SpringApplication.run(StockApplication.class,args);}}application.yml:
server:port:9001# order-service 用 9002spring:application:name:stock-service# 注册到 Nacos 的服务名,调用方就靠这个名字找你cloud:nacos:discovery:server-addr:127.0.0.1:8848stock-service 提供接口:
@RestController@RequestMapping("/stock")publicclassStockController{// 内存模拟库存,演示用;真实场景在数据库里用乐观锁扣减privatefinalMap<String,Integer>stockMap=newConcurrentHashMap<>(Map.of("iphone",100,"macbook",50));@PostMapping("/deduct")publicStringdeduct(@RequestParamStringsku,@RequestParamintcount){stockMap.compute(sku,(k,v)->{if(v==null||v<count){thrownewIllegalStateException("库存不足: "+sku);}returnv-count;});return"deducted "+sku+" x"+count;}}两个工程先后启动,到 Nacos 控制台「服务列表」里能看到order-service和stock-service各一条记录,注册这步就算通了。
验证自愈能力:把 stock-service 再起一个实例(IDEA 里 Copy Configuration 改端口为 9011,或--server.port=9011启动),服务列表里 stock-service 变成 2 个实例。杀掉一个,约 15~30 秒后 Nacos 摘除该实例——调用方无感。这就是注册中心买来的第一笔保险。
四、OpenFeign:把 HTTP 调用写成「本地方法」
4.1 引依赖、开注解
order-service 加依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>启动类上@EnableFeignClients。然后定义「接口式客户端」:
// name 要和对方注册的服务名完全一致@FeignClient(name="stock-service",path="/stock")publicinterfaceStockClient{@PostMapping("/deduct")Stringdeduct(@RequestParam("sku")Stringsku,@RequestParam("count")intcount);}调用侧就跟用本地 Service 一样:
@ServicepublicclassOrderService{@AutowiredprivateStockClientstockClient;publicStringcreateOrder(Stringsku,intcount){Stringresult=stockClient.deduct(sku,count);// 实际发起了 HTTP 调用orderMapper.insert(buildOrder(sku,count));returnresult;}}Feign 帮你做了三件事:按服务名从 Nacos 拿可用实例列表 → LoadBalancer 挑一台 → 组装 HTTP 请求。服务扩容缩容,调用方一行代码不用动。
4.2 三个必踩的坑,提前绕开
坑一:@RequestParam必须写 value。Feign 接口里@RequestParam String sku不写"sku",编译后参数名丢失(没开-parameters),请求发出去就是sku=null,对面直接 400。接口里所有参数注解都显式写名字,一劳永逸:
Stringdeduct(@RequestParam("sku")Stringsku,@RequestParam("count")intcount);坑二:超时没配,默认太短。Feign 底层默认连接 10s / 读 60s(随版本有差异),库存接口一旦变慢,订单线程会被拖着等。显式配置:
spring:cloud:openfeign:client:config:default:connect-timeout:2000# 毫秒read-timeout:5000logger-level:basic# 调试期开 basic,生产改 none坑三:把 Feign 当本地方法,忘了它有网络。createOrder里如果stockClient.deduct抛超时异常,订单记录就不能写——扣库存失败必须中断下单流程,这个 try/catch 的语义要想清楚,不能无脑吞异常返回"下单成功"。
五、Gateway:给所有服务装一个门卫
微服务拆多了之后,前端不该知道后面有 8 个服务 8 个端口。网关统一收口:路由转发、鉴权、限流都在这一层做。
新建 gateway 工程:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>server:port:8000spring:application:name:gateway-servicecloud:nacos:discovery:server-addr:127.0.0.1:8848gateway:routes:-id:order-routeuri:lb://order-service# lb:// 表示走负载均衡,从注册中心解析predicates:-Path=/api/order/**filters:-StripPrefix=1# 转发前去掉第一段 /api-id:stock-routeuri:lb://stock-servicepredicates:-Path=/api/stock/**filters:-StripPrefix=1前端此后只认http://gateway:8000/api/order/**,网关按路径拆给对应服务。
一个必须澄清的点:Gateway 基于 WebFlux(Netty),不能和spring-boot-starter-web共存,同一个 pom 里同时引了直接起不来。网关工程里别顺手引 web 依赖。
网关层的鉴权过滤器示意(完整 JWT 校验逻辑见下一篇系统文章):
@ComponentpublicclassAuthFilterimplementsGlobalFilter,Ordered{@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){Stringtoken=exchange.getRequest().getHeaders().getFirst("Authorization");if(token==null||token.isBlank()){exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);returnexchange.getResponse().setComplete();}returnchain.filter(exchange);}@OverridepublicintgetOrder(){return-100;}// 数值越小越先执行}六、熔断降级:别让一次库存抖动拖垮整个商城
调用链是 order → stock。某天 stock 变慢(比如 DB 锁表),order 的线程会排队等它超时,线程池迅速占满,order 也跟着不可用——故障顺着调用链爬上来了,这叫雪崩。
解法是熔断:统计出某下游错误率过高后,短时间内直接走降级逻辑,不再发真实请求,给下游喘息窗口。
用 Resilience4j(不额外引 Alibaba 组件,依赖更轻):
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId></dependency>@FeignClient(name="stock-service",path="/stock",fallback=StockFallback.class)// 声明降级类publicinterfaceStockClient{@PostMapping("/deduct")Stringdeduct(@RequestParam("sku")Stringsku,@RequestParam("count")intcount);}@ComponentpublicclassStockFallbackimplementsStockClient{@OverridepublicStringdeduct(Stringsku,intcount){// 降级策略按业务定:这里选择「下单失败」,也可以改成延迟扣减、入队列thrownewBizException("库存服务繁忙,请稍后重试");}}降级逻辑一定要是业务决策而不是技术兜底:下单场景宁可报「稍后再试」,也不能悄悄跳过扣库存——那是要对账到半夜的。
七、落地清单:上线前把这张表过一遍
| 事项 | 说明 |
|---|---|
| 版本对齐 | Boot / Cloud / Alibaba / Nacos 四个版本查官方对照表,别自由组合 |
| Nacos 生产部署 | 切 MySQL 存储、改默认密码、开鉴权,别用单机 Derby 裸奔 |
| Feign 超时 | 每个客户端显式设置,别吃默认值 |
| 降级语义 | 每个降级逻辑过一遍业务,明确「失败了用户看到什么、数据会不会不一致」 |
| 幂等 | 跨进程调用会重试,扣库存、支付回调必须幂等(唯一业务 ID 去重) |
| 链路追踪 | 至少把 traceId 透传到日志里(Micrometer Tracing + Zipkin 是当前默认组合) |
最后一句话总结拆分的原则:微服务拆的不是代码,是发布节奏、扩容策略和故障隔离。服务数量跟着业务边界走,两个人三个服务,运维成本比收益大——先拆对最疼的那一块,比画一张八个服务的大图有用得多。