同一个社区团购业务,单体版 1 个应用,微服务版 6 个服务。怎么拆?服务怎么互相发现?请求怎么路由?跨服务调用怎么降级?本文用 3 个真实微服务项目的代码,把 Spring Cloud 的基础设施层从头到尾讲透。
第一章:从单体到微服务的拆分决策
1.1 单体的痛点
1 2 3 4 5 6
| community-group-buying-system (单体) ├── 15 个 Controller ├── 14 个 DAO ├── 1 个数据库 ├── 1 个 Redis └── 2 个前端
|
当业务增长后:
- 商品模块改代码要重新部署整个应用 → 影响用户登录
- 订单高峰期打满线程池 → 商品浏览也卡死
- 团购逻辑和商品逻辑耦合在同一代码库 → 团队协作冲突
1.2 拆分方案
1 2 3 4 5 6 7 8 9 10 11 12
| 单体 1 个DB 微服务 5 个DB ┌──────────────┐ ┌──────────────┐ │ springboot │ ─────> │ cgb_user_db │ ← 用户服务(8001) │ 2c1hu │ ├──────────────┤ │ │ │ cgb_product │ ← 商品服务(8002) └──────────────┘ ├──────────────┤ │ cgb_groupbuy │ ← 团购服务(8003) ├──────────────┤ │ cgb_order │ ← 订单服务(8004) ├──────────────┤ │ cgb_content │ ← 内容服务(8005) └──────────────┘
|
1.3 7 模块结构
1 2 3 4 5 6 7 8
| community-group-buying-microservices/ ├── cgb-common # 公共模块(Feign客户端、MQ消息体、认证、工具类) ├── cgb-gateway # API网关 (端口8000) ├── cgb-user-service # 用户服务 (端口8001, 库cgb_user) ├── cgb-product-service # 商品服务 (端口8002, 库cgb_product) ├── cgb-groupbuy-service # 团购服务 (端口8003, 库cgb_groupbuy) ├── cgb-order-service # 订单服务 (端口8004, 库cgb_order) └── cgb-content-service # 内容服务 (端口8005, 库cgb_content)
|
1.4 父 POM 版本管理
1 2 3 4 5 6 7
| <properties> <spring-boot.version>3.4.1</spring-boot.version> <spring-cloud.version>2024.0.0</spring-cloud.version> <spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version> <seata.version>2.2.0</seata.version> <rocketmq-spring.version>2.3.1</rocketmq-spring.version> </properties>
|
1.5 单体 vs 微服务代码对比
单体版(一个方法内完成,本地事务):
1 2 3 4 5 6 7
| @Transactional(rollbackFor = Exception.class) public void createOrder(OrdersEntity entity) { ProductEntity product = productDao.selectById(entity.getProductId()); productDao.decreaseStock(entity.getProductId(), entity.getQuantity()); ordersDao.insert(entity); yonghuDao.addPoints(entity.getUserId(), entity.getTotalPrice().doubleValue()); }
|
微服务版(跨服务调用 + 分布式事务 + 异步消息):
1 2 3 4 5 6 7 8
| @GlobalTransactional(name = "cgb-create-order", rollbackFor = Exception.class) public void createOrder(OrdersEntity entity) { var stockResult = feignProductService.decreaseStock( entity.getProductId(), entity.getQuantity()); if (stockResult.getCode() != 0) throw new EIException("库存扣减失败"); ordersDao.insert(entity); sendOrderStatusMessage(entity, MQTopics.TAG_ORDER_CREATED); }
|
1.6 拆分成本评估
| 维度 |
单体 |
微服务 |
变化 |
| 部署 |
1个JAR |
6个JAR + 中间件 |
↑↑↑ |
| 运维 |
1台服务器 |
6台+Nacos+Seata+RocketMQ |
↑↑↑ |
| 开发效率 |
改代码全量部署 |
按服务独立部署 |
↓ |
| 扩展性 |
整体扩容 |
按需扩容 |
↓ |
结论:微服务用运维复杂度换取开发灵活性和扩展性。团队 < 5 人、QPS < 1000,单体是更好的选择。
第二章:Nacos 服务注册与配置中心
2.1 Nacos 双重角色
1 2 3 4 5 6 7 8 9 10
| ┌─────────────────┐ │ Nacos │ 服务注册 ────> │ 服务列表 │ <──── 服务发现 │ cgb-user:8001 │ │ cgb-product:8002│ │ │ 配置推送 ────> │ 配置存储 │ <──── 配置拉取 │ common-redis │ │ cgb-order.yml │ └─────────────────┘
|
2.2 服务注册
1 2 3 4
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
1 2 3 4 5 6 7 8
| spring: cloud: nacos: server-addr: ${NACOS_ADDR:127.0.0.1:8848} discovery: namespace: cgb-dev group: CGB_GROUP service: cgb-order-service
|
2.3 命名空间 vs 分组
1 2 3 4 5 6 7 8 9 10 11 12 13
| Nacos ├── namespace: cgb-dev │ ├── CGB_GROUP │ │ ├── cgb-user-service (8001) │ │ ├── cgb-product-service (8002) │ │ └── cgb-order-service (8004) │ └── SHARED_GROUP │ └── (共享配置) │ ├── namespace: cgb-test │ └── (测试环境服务) │ └── namespace: public (默认)
|
- 命名空间:物理隔离(dev/test/prod 互不可见)
- 分组:逻辑分类(同一环境内的不同业务组)
2.4 共享配置(多服务复用)
1 2 3 4 5 6 7 8 9 10 11 12 13
| spring: data: redis: host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379} database: 0 timeout: 10s lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0
|
2.5 从 Nacos 导入配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| spring: cloud: nacos: config: namespace: cgb-dev group: CGB_GROUP file-extension: yml shared-configs: - data-id: common-redis.yml group: SHARED_GROUP refresh: true - data-id: common-mybatis.yml group: SHARED_GROUP refresh: true refresh-enabled: true import: - optional:nacos:cgb-order-service.yml?group=CGB_GROUP&refreshEnabled=true
|
2.6 动态配置刷新
1 2 3 4 5 6
| @RestController @RefreshScope public class OrderController { @Value("${order.timeout:300}") private int orderTimeout; }
|
在 Nacos 控制台修改 order.timeout 的值,不重启应用即可生效。
第三章:Spring Cloud Gateway 网关
3.1 网关核心职责
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 客户端请求 │ ▼ ┌──────────────────────────────────────┐ │ API Gateway │ │ 1. 路由:路径 → 服务映射 │ │ 2. 鉴权:JWT验证 │ │ 3. 限流:Sentinel │ │ 4. 跨域:CORS │ │ 5. 用户信息透传 │ └──────────┬───────────────────────────┘ │ ┌──────┼──────┐ ▼ ▼ ▼ user product order
|
3.2 路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| spring: cloud: gateway: routes: - id: cgb-user-service uri: lb://cgb-user-service predicates: - Path=/user/** filters: - StripPrefix=1 - id: cgb-product-service uri: lb://cgb-product-service predicates: - Path=/product/** filters: - StripPrefix=1
|
路由流程:/user/login → StripPrefix=1 去掉 /user → 转发到 cgb-user-service 的 /login。
3.3 双前缀路由(钢铁工厂项目)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| spring: cloud: gateway: routes: - id: user-service-v1 uri: lb://user-service predicates: - Path=/api/v1/login,/api/v1/admin/**,/api/v1/user/** filters: - StripPrefix=2
- id: user-service uri: lb://user-service predicates: - Path=/login,/admin/**,/user/**
|
3.4 全局跨域配置
1 2 3 4 5 6 7 8 9 10 11
| spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowed-origins: "*" allowed-methods: "*" allowed-headers: "*" allow-credentials: true max-age: 3600
|
3.5 JWT 鉴权过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| @Component public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired private RedisTemplate<String, Object> redisTemplate;
private static final List<String> WHITE_LIST = Arrays.asList( "/login", "/api/v1/login", "/files/download", "/api/v1/files/download", "/uniapp/getObtainInformationDataList", "/hello", "/actuator" );
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getURI().getPath();
if (isWhiteListed(path)) return chain.filter(exchange);
String token = request.getHeaders().getFirst("token"); if (StrUtil.isBlank(token)) { token = request.getQueryParams().getFirst("token"); } if (StrUtil.isBlank(token)) return unauthorized(exchange);
String audience = JWT.decode(token).getAudience().get(0); String[] split = audience.split("-"); String userId = split[0]; String role = split[1];
String password = (String) redisTemplate.opsForValue() .get("token:password:" + audience); if (password == null) return unauthorized(exchange);
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(password)).build(); verifier.verify(token);
String clientIp = getClientIp(request); String storedIp = (String) redisTemplate.opsForValue().get("token:" + audience); if (storedIp == null || !clientIp.equals(storedIp)) { return unauthorized(exchange); }
ServerHttpRequest mutatedRequest = request.mutate() .header("X-User-Id", userId) .header("X-User-Role", role) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); }
@Override public int getOrder() { return -1; } }
|
3.6 用户信息透传原理
1 2 3 4 5 6 7
| 客户端 网关 下游服务 │── 请求(Token) ────────>│ │ │ │ 解析Token获取userId/role │ │ │ 添加 X-User-Id Header ──────>│ │ │ 添加 X-User-Role Header ─────>│ │ │ │ request.getHeader("X-User-Id") │<── 响应 ──────────────│<─────────────────────────────│
|
下游服务不需要再次解析 JWT——网关统一鉴权后,通过自定义 Header 传递用户信息。
第四章:OpenFeign 声明式调用
4.1 Feign 客户端接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @FeignClient( name = "cgb-product-service", // 目标服务名 contextId = "product", // 上下文ID(防Bean名冲突) fallbackFactory = FeignProductServiceFallbackFactory.class // 降级工厂 ) public interface FeignProductService {
@GetMapping("/shangpin/internal/productDetail") R<?> getProductDetail(@RequestParam("id") Long id);
@PostMapping("/shangpin/internal/decreaseStock") R<?> decreaseStock(@RequestParam("id") Long id, @RequestParam("quantity") Integer quantity);
@PostMapping("/shangpin/internal/increaseStock") R<?> increaseStock(@RequestParam("id") Long id, @RequestParam("quantity") Integer quantity); }
|
像调本地方法一样调远程接口:
1 2 3 4 5 6 7 8 9 10
| @Service public class OrderServiceImpl { @Autowired private FeignProductService feignProductService;
public void createOrder(Long productId, Integer quantity) { R<?> result = feignProductService.decreaseStock(productId, quantity); if (result.getCode() != 0) throw new RuntimeException("库存扣减失败"); } }
|
4.2 Fallback 降级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Slf4j @Component public class FeignProductServiceFallbackFactory implements FallbackFactory<FeignProductService> {
@Override public FeignProductService create(Throwable cause) { log.error("商品服务调用失败", cause); return new FeignProductService() { @Override public R<?> decreaseStock(Long id, Integer quantity) { return R.fail("商品服务暂不可用,扣减库存失败"); } @Override public R<?> increaseStock(Long id, Integer quantity) { return R.fail("商品服务暂不可用,回补库存失败"); } @Override public R<?> getProductDetail(Long id) { return R.fail("商品服务暂不可用"); } }; } }
|
降级触发场景:服务宕机、超时、HTTP 500、Sentinel 熔断器打开。
4.3 内部接口鉴权
微服务的 /internal/** 接口只允许服务间调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @Component public class InternalAuthFeignInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { template.header(InternalAuthConstants.HEADER_NAME, InternalAuthConstants.TOKEN); } }
@Component public class InternalAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, ...) { if (request.getRequestURI().contains("/internal/")) { String token = request.getHeader(InternalAuthConstants.HEADER_NAME); if (!InternalAuthConstants.isValid(token)) { response.setStatus(401); response.getWriter().write("内部接口无权访问"); return; } } filterChain.doFilter(request, response); } }
|
4.4 超时配置
1 2 3 4 5 6 7 8 9 10
| spring: cloud: openfeign: client: config: default: connect-timeout: 3000 read-timeout: 10000 cgb-product-service: read-timeout: 5000
|
第五章:多语言微服务
5.1 Java + Python + TypeScript 架构
1 2 3 4 5 6 7 8 9 10 11 12 13
| ┌─────────────┐ │ Vue 3 前端 │ (TypeScript) └──────┬──────┘ ┌──────▼──────┐ │ API Gateway │ (Java + Spring Cloud Gateway) └──────┬──────┘ ┌──────▼──────┐ │ Nacos │ ← 服务注册中心 └──┬──────┬───┘ ┌────────▼┐ ┌─▼────────────┐ │ Java │ │ Python │ │ user-svc │ │ demo-service │ └──────────┘ └──────────────┘
|
5.2 Python 服务注册到 Nacos
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| from flask import Flask, jsonify import requests, threading, time
app = Flask(__name__) NACOS_ADDR = "localhost:8848" SERVICE_NAME = "python-demo-service"
def register_to_nacos(): data = { "serviceName": SERVICE_NAME, "ip": "127.0.0.1", "port": 5000, "groupName": "DEFAULT_GROUP" } requests.post(f"http://{NACOS_ADDR}/nacos/v1/ns/instance", data=data)
def heartbeat(): """每5秒发送心跳""" while True: try: requests.put( f"http://{NACOS_ADDR}/nacos/v1/ns/instance/beat", data={"serviceName": SERVICE_NAME, "ip": "127.0.0.1", "port": 5000}) except: pass time.sleep(5)
@app.route("/api/demo/data") def get_data(): return jsonify({"data": [1, 2, 3], "source": "python-flask"})
if __name__ == "__main__": register_to_nacos() threading.Thread(target=heartbeat, daemon=True).start() app.run(host="0.0.0.0", port=5000)
|
5.3 网关统一路由
1 2 3 4 5 6 7 8 9 10
| spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: [Path=/api/users/**] - id: python-demo-service uri: lb://python-demo-service predicates: [Path=/api/demo/**]
|
关键点:lb://python-demo-service 从 Nacos 获取 Python 服务的实例列表并负载均衡。网关不关心服务用什么语言写。
总结
| 组件 |
作用 |
关键配置 |
| Nacos |
服务注册 + 配置中心 |
spring.cloud.nacos.discovery/config |
| Gateway |
路由 + 鉴权 + 透传 |
routes + GlobalFilter |
| OpenFeign |
声明式调用 + 降级 |
@FeignClient + fallbackFactory |
| 内部鉴权 |
/internal/** 接口保护 |
InternalAuthFeignInterceptor |
| 多语言 |
Java + Python 混合 |
Nacos 统一注册 |
核心思想:微服务的基础设施层解决”服务在哪里”(Nacos)、”请求怎么到”(Gateway)、”服务怎么调”(Feign)三个问题。语言差异被 HTTP + JSON 标准化了。