Spring cloud

Spring Cloud 微服务架构全解(上):拆分、注册、网关与调用

2026-07-15 #Spring cloud#微服务

同一个社区团购业务,单体版 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()); // Feign远程调用
if (stockResult.getCode() != 0) throw new EIException("库存扣减失败");
ordersDao.insert(entity);
sendOrderStatusMessage(entity, MQTopics.TAG_ORDER_CREATED); // MQ异步消息
}

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 # 命名空间隔离(dev/test/prod)
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
# Nacos中 data-id: common-redis.yml | group: SHARED_GROUP
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 # lb://服务名(负载均衡)
predicates:
- Path=/user/** # 路径匹配
filters:
- StripPrefix=1 # 去掉第一级前缀
- id: cgb-product-service
uri: lb://cgb-product-service
predicates:
- Path=/product/**
filters:
- StripPrefix=1

路由流程/user/loginStripPrefix=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:
# 管理前端 /api/v1 前缀路由
- id: user-service-v1
uri: lb://user-service
predicates:
- Path=/api/v1/login,/api/v1/admin/**,/api/v1/user/**
filters:
- StripPrefix=2 # /api/v1/admin/list → /admin/list

# 小程序直连路由(无前缀)
- id: user-service
uri: lb://user-service
predicates:
- Path=/login,/admin/**,/user/**
# 无 StripPrefix,直接透传

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();

// 1. 白名单
if (isWhiteListed(path)) return chain.filter(exchange);

// 2. 提取Token
String token = request.getHeaders().getFirst("token");
if (StrUtil.isBlank(token)) {
token = request.getQueryParams().getFirst("token");
}
if (StrUtil.isBlank(token)) return unauthorized(exchange);

// 3. 解析Token
String audience = JWT.decode(token).getAudience().get(0);
String[] split = audience.split("-");
String userId = split[0];
String role = split[1];

// 4. 从Redis获取密码验证签名
String password = (String) redisTemplate.opsForValue()
.get("token:password:" + audience);
if (password == null) return unauthorized(exchange);

// 5. 验证JWT签名
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(password)).build();
verifier.verify(token);

// 6. IP绑定验证
String clientIp = getClientIp(request);
String storedIp = (String) redisTemplate.opsForValue().get("token:" + audience);
if (storedIp == null || !clientIp.equals(storedIp)) {
return unauthorized(exchange);
}

// 7. ★ 用户信息透传给下游微服务
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
// Feign拦截器:自动注入内部Token
@Component
public class InternalAuthFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header(InternalAuthConstants.HEADER_NAME, InternalAuthConstants.TOKEN);
}
}

// 服务端过滤器:验证内部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 # 连接超时3秒
read-timeout: 10000 # 读超时10秒
cgb-product-service:
read-timeout: 5000 # 商品服务5秒

第五章:多语言微服务

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 # Java服务
predicates: [Path=/api/users/**]
- id: python-demo-service
uri: lb://python-demo-service # Python服务
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 标准化了。

评论
分享