楼层: 首页/ 软件技术/ Spring Cloud 微服务/ 微服务最佳实践与坑
十

微服务最佳实践与坑

Best Practices & Pitfalls

工具会用只是第一步,怎么拆、怎么管才是真功夫。下面这十条,是从无数线上事故里攒出来的经验,踩过一次就懂了。

主题建议
服务拆分按业务域(DDD)拆,别按技术层拆。一个服务只负责一个业务能力,数据库归它自己,别跨库 join。
API 版本管理接口变更加版本号(/api/v1/user),老版本别随便删,灰度兼容。
同步 vs 异步强一致的实时调用用 Feign;解耦、削峰、通知类用 MQ。别啥都同步。
配置管理所有配置进 Nacos,敏感配置(密码、Key)加密或走配置中心鉴权,别写代码里。
日志规范每个请求带 TraceId,日志级别、格式统一,生产别开 DEBUG。
安全网关统一验 JWT/OAuth2 token,服务之间别裸调;内网通信也加鉴权。
容器化每个服务打 Docker 镜像,K8s 编排;别把环境差异赌在"我电脑上能跑"。

服务怎么拆:别按技术层拆

新手最容易犯的错:把工程分成 controller、service、dao 三个"服务"——这不是微服务,这是把一个单体拆碎了重新粘回去。正确的拆法是按业务域拆:用户相关的归用户服务,订单相关的归订单服务,一个服务对应一个业务能力、一个数据库。

原则意思
单一业务能力一个服务只干一件事,能一句话说清它是干嘛的。
数据库独占每个服务有自己的库,别跨库 join。要数据走接口。
独立部署改这个服务不用动别的服务,能单独发版。
别拆太细三个人维护 20 个微服务是灾难。先粗拆,业务跑起来再细拆。

服务间通信:什么时候同步、什么时候异步

这是设计微服务时最常纠结的问题。判断标准只有一条:这个调用的结果,需不需要立刻拿来返回给用户?

场景选哪种原因
下单查用户信息Feign 同步下单响应里要带用户名,必须立刻拿到。
下单后发短信MQ 异步用户不关心短信有没有发出去,别让下单接口等。
扣库存Feign 同步库存够不够直接决定下单成不成。
下单后推 BI 报表MQ 异步报表晚几秒没事,解耦最重要。

API 版本管理:别让老用户骂街

接口上线后总会改。直接改老接口,老版本 App 就崩了。行业做法是把版本号放进 URL(/api/v1/user、/api/v2/user),老版本接口留着给老 App 用,新版本新开一套。网关层可以按路径前缀路由到不同服务或不同 Controller。别随便删老版本,至少留到 95% 用户升级完。

同一个 Controller 里支持两个版本(思路示例)

@RestController @RequestMapping("/api") public class UserController { // 老版本:返回字段少 @GetMapping("/v1/user/{id}") public UserV1 v1(@PathVariable Long id) { ... } // 新版本:多了手机号字段,老 App 不调这个 @GetMapping("/v2/user/{id}") public UserV2 v2(@PathVariable Long id) { ... } }

容器化:一个服务一个 Docker 镜像

微服务动辄十几个,每台机器部署一个太浪费。Docker 把每个服务连同它的依赖打成一个镜像,丢到任何机器上都能跑。K8s 负责编排——挂了自动重启、流量大了自动扩容。学习阶段用 Docker Compose 一键起 Nacos + Redis + 你的服务,比本地一个个起省事。

一个 Spring Boot 服务的 Dockerfile(通用模板)

# 用一个带 JDK 17 的基础镜像(Spring Boot 3 要 JDK 17+) FROM eclipse-temurin:17-jre # 把打好的 jar 包复制进镜像,起个别名 COPY target/user-service.jar app.jar # 容器启动时跑这个命令 ENTRYPOINT ["java", "-jar", "/app.jar"]

docker-compose.yml:一键起 Nacos + Redis + 三个业务服务

version: "3.8" services: nacos: image: nacos/nacos-server:v2.3.0 # 以官方最新稳定版为准 environment: MODE: standalone ports: - "8848:8848" user-service: build: ./user-service ports: - "8081:8081" depends_on: [nacos] # order-service、product-service 类似,照着抄

安全:JWT 统一鉴权

用户登录成功后,服务端发一个 JWT(JSON Web Token)字符串给前端。之后前端每次请求都把这个 token 放在 Authorization 头里。网关统一校验签名和有效期,通过了才放行;下游服务不用再查库,解析 token 就能拿到用户 ID。无状态、可扩展,是微服务鉴权的事实标准。

网关里校验 JWT 的思路(伪代码,实际用 spring-security 或 jjwt 库)

// 1. 前端请求头:Authorization: Bearer eyJhbGciOi...(一段很长的字符串) // 2. 网关过滤器里取出 Bearer 后面那串,用密钥验签 // 3. 验签通过,从 token 里解出 userId,塞进请求头转发给下游 // 4. 验签失败或过期,直接 401 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String header = exchange.getRequest().getHeaders().getFirst("Authorization"); if (header == null || !header.startsWith("Bearer ")) { return unauthorized(exchange); } String token = header.substring(7); // 验签 + 解析出 userId,以官方 jjwt 库用法为准 String userId = jwtUtil.verifyAndGetUserId(token); if (userId == null) return unauthorized(exchange); // 把 userId 传给下游服务 ServerHttpRequest mutated = exchange.getRequest().mutate() .header("X-User-Id", userId).build(); return chain.filter(exchange.mutate().request(mutated).build()); }

JWT 之上的 OAuth2:第三方登录那套

别把 JWT 和 OAuth2 混为一谈:JWT 是一种 token 格式,OAuth2 是一套授权协议("用微信/Google 账号登录这个网站")。企业内部服务间鉴权用 JWT 就够;要做"第三方授权登录""开放平台给合作伙伴发令牌"才上 OAuth2。

OAuth2 角色人话
Resource Owner用户本人(同意把自己数据授权给第三方 App)。
Client第三方应用(要拿用户数据的那个 App)。
Authorization Server授权服务器,发 token(Spring Authorization Server 干这个)。
Resource Server存用户数据的服务,拿着 token 来验,验对了才给数据。

论一句话选型

内部微服务鉴权 → JWT 就够,网关验签。要"让别的公司/应用接入你的开放平台、用户点同意授权" → 上 OAuth2 授权码模式,配合 Spring Authorization Server。别一上来就 OAuth2,内部场景它是杀鸡用牛刀。

六大常见坑(面试也常问)

坑 1:版本不匹配

Spring Cloud、Spring Boot、Spring Cloud Alibaba 三者版本不对应,启动时 NoSuchMethodError 满天飞。去官方 Wiki 查 Release Train 表,照抄。

坑 2:Nacos 连接超时

报 connect timed out。先看 Nacos 起没起、端口对不对、防火墙/安全组放没放 8848。本地连公司 Nacos 要走 VPN 的话,确认 VPN 连上了。

坑 3:Feign 超时默认太短

调慢接口必超时,显式配 connectTimeout 和 readTimeout,别用默认值赌。

坑 4:网关路由不生效

查 uri 里服务名拼写、StripPrefix 数、有没有误引 spring-boot-starter-web。

坑 5:Sentinel 规则重启就没

控制台规则默认在内存,必须持久化到 Nacos。

坑 6:分布式事务回滚失败

AT 模式回滚失败多是脏写或隔离级别问题,TC 必须集群 + 持久化。