# SpringCloud
# 从单体到集群再到分布式
早期阶段,单体架构是主流选择,所有功能模块打包在一个应用中,开发简单直接,但是随着业务增长,代码变得臃肿,难以扩展特定功能模块,技术栈单一,难以采用新技术。
为了应对单体架构的性能瓶颈和高可用需求,集群架构应运而生。
实现方式:
- 水平扩展:部署多个相同的单体应用实例
- 通过负载均衡器 (Nginx、F5 等) 分配请求
- 共享数据库或数据库主从复制
但是仍然有缺陷,比如应用本身仍然是单体,业务复杂时扩展不灵活。
此时分布式架构与微服务应运而生,分布式架构通过将系统拆分为多个服务来解决上述问题。
本次学习使用尚硅谷 b 站开放课堂:https://www.bilibili.com/video/BV1UJc2ezEFU
框架(组件)学习与本套课程高度重合,但并不是课程资料的再复写。
相关技术:
- Nacos(注册中心、配置中心)来自 Spring Cloud Alibaba
- OpenFegin(远程调用)来自 Spring Cloud 官方
- Sentinel(异常处理、流控规则、熔断规则)来自 Spring Cloud Alibaba
- Gateway(路由、断言、过滤)来自 Spring Cloud 官方
- Seata(分布式事务)来自 Spring Cloud Alibaba
# Nacos

# 注册中心
# 服务注册
首先进行依赖导入
<!-- nacos 配置中心、注册中心 --> | |
<dependency> | |
<groupId>com.alibaba.cloud</groupId> | |
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> | |
</dependency> |
使用 docker 或者直接运行的方式启动 Nacos
在 Windows 平台直接运行下使用命令:startup.cmd -m standalone(standalone 为使用单机模式)
在不同的服务下编写配置,如订单服务和产品服务:
spring: | |
cloud: | |
nacos: | |
server-addr: 127.0.0.1:8848 | |
application: | |
name: service-order | |
server: | |
port: 8080 |
spring: | |
cloud: | |
nacos: | |
server-addr: 127.0.0.1:8848 | |
application: | |
name: service-product | |
server: | |
port: 9000 |
其中 nacos.server-addr 为 nacos 服务的地址为 127.0.0.1:8848(本地测试)
访问 http://localhost:8848/nacos,在服务管理 - 服务列表可以看到现在已经注册上的服务
服务名 | 分组名称 | 集群数目 | 实例数 | 健康实例数 | 触发保护阈值 | 操作 |
---|---|---|---|---|---|---|
service-order | DEFAULT_GROUP | 1 | 2 | 2 | false | 详情 | 示例代码 | 订阅者 | 删除 |
service-product | DEFAULT_GROUP | 1 | 3 | 3 | false | 详情 | 示例代码 | 订阅者 | 删除 |
# 服务发现
由于使用了 Nacos,服务发现方法的调用存在两套标准,分别是 Spring Cloud 的 DiscoveryClient 和 Nacos 的 NacosServiceDiscovery
@Resource | |
DiscoveryClient discoveryClient; | |
@Resource | |
NacosServiceDiscovery nacosServiceDiscovery; |
下面为测试代码:
/** | |
* spring 标准 discovery 使用 DiscoveryClient | |
*/ | |
@Test | |
public void testDiscoveryClient() { | |
for (String service : discoveryClient.getServices()) { | |
/* | |
循环输出服务列表(服务名) | |
service-order | |
service-product | |
*/ | |
System.out.println(service); | |
// 获取所有实例、输出 IP 与端口号 | |
List<ServiceInstance> instances = discoveryClient.getInstances(service); | |
for (ServiceInstance instance : instances) { | |
System.out.println(instance.getHost() + ":" + instance.getPort()); | |
} | |
} | |
} | |
/** | |
* nacos 标准 discovery 使用 NacosServiceDiscovery | |
*/ | |
@Test | |
public void testNacosServiceDiscovery() throws NacosException { | |
for (String service : nacosServiceDiscovery.getServices()) { | |
// 输出服务列表 | |
System.out.println(service); | |
// 获取所有实例、输出 IP 与端口号 | |
List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service); | |
for (ServiceInstance instance : instances) { | |
System.out.println(instance.getHost() + ":" + instance.getPort()); | |
} | |
} | |
} |
输出:
service-order | |
192.168.25.1:8080 | |
192.168.25.1:8001 | |
service-product | |
192.168.25.1:9000 | |
192.168.25.1:9002 | |
192.168.25.1:9001 |
如果并没有如此多的输出可能是只启动了两个后端服务,还需要多启动几个来模拟分布式。
其次,在实际应用中,这个一般会被进一步封装,其发现的过程是自动进行的。
# 初见,远程调用
现在有一个实例,我们需要一个下单功能,当用户下单后对其商品进行结算这里我们对一些数据做出模拟。
订单实体:
@Data | |
public class Order { | |
private Long id; | |
private BigDecimal totalAmount; | |
private Long userId; | |
private String nickName; | |
private String address; | |
private List<Object> product; | |
} |
商品实体:
@Data
public class Product {
private Long id;
private BigDecimal price;
private String productName;
private int num;
}
其次就是相应和 Controller 与 Service 代码,其较为简单不在此处详细展开,不过我想说一下订单部分的 Service:
@Service | |
public class OrderServiceImpl implements OrderService { | |
@Override | |
public Order createOrder(Long productId, Long userId) { | |
Order order = new Order(); | |
order.setId(1L); | |
// TODO 需要计算 | |
order.setTotalAmount(new BigDecimal("0")); | |
order.setUserId(userId); | |
order.setNickName("Karry.Liu"); | |
order.setAddress("北极"); | |
// TODO 需要远程查询 | |
order.setProduct(null); | |
return order; | |
} | |
} |
由于 Order 与 Product 分别位于两个服务之中,其详细的金额 Amount 与产品详情列表 Product List 我们目前似乎无法获取,那我们应该怎么办呢?这个我们暂时按下不表,我们现在需要解决一个更加棘手的问题。
现在我们有如下项目结构(简略版)
- cloud-demo(基座项目) | |
| | |
| - services(服务层) | |
| | | |
| | - service-order(订单服务)(包含订单实体bean、服务service和控制controller) | |
| | | |
| | - service-product(商品服务)(包含商品实体bean、服务service和控制controller) | |
订单服务无法使用商品bean,反之商品服务无法使用订单bean,因为其每个服务均为独立的项目。 |
当 Order 服务需要 Product 服务时,其在 Order 的代码内一定会存在与 Product 相关的关键字,特别地,由于两个服务之间项链紧密,在 Product 的代码内也许也会出现 Order 相关的关键字。可是两套服务分别维护着自己的 bean(实体对象 / 实体类),在不同的服务之间甚至没有办法使用对方的实体类。
解决方案也很简单,将商品与订单的 Bean 剥离出来,形成一个独立的项目,与 services 等价地位,并在 services 添加 model 依赖。
<!-- 模型依赖 --> | |
<dependency> | |
<groupId>com.KarryCode</groupId> | |
<artifactId>model</artifactId> | |
<version>0.0.1-SNAPSHOT</version> | |
</dependency> |
现在的结构为:
- cloud-demo(基座项目) | |
| | |
| - model(模型层)(包含订单实体bean与商品实体bean) | |
| | |
| - services(服务层) | |
| | | |
| | - service-order(订单服务)(包含服务service和控制controller) | |
| | | |
| | - service-product(商品服务)(包含服务service和控制controller) |
至此,服务之间的实体使用已经被打通。
其次编写远程访问请求模板类 RestTemplate,由于 RestTemplate 是线程安全的,我们可以这样写:
@Configuration | |
public class ProductServiceConfig { | |
@Bean | |
public RestTemplate restTemplate() { | |
return new RestTemplate(); | |
} | |
} | |
// 上拉使他成为一个 Bean |
其次编写远程访问方法:
private Product getProductFromRemote(Long productId) { | |
// 获取商品服务所在的所有机器 IP + 端口 | |
List<ServiceInstance> instances = discoveryClient.getInstances("service-product"); | |
// 获取第一个机器(简单版) | |
ServiceInstance serviceInstance = instances.get(0); | |
// 拼接请求地址 http://192.168.25.1:9000/product/100 | |
String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/product/" + productId; | |
log.info("远程请求: {}", url); | |
// 发送请求(远程) | |
return restTemplate.getForObject(url, Product.class); | |
} |
补充 createOrder 方法
public Order createOrder(Long productId, Long userId) { | |
// 此处提前远程查询 | |
Product productFromRemote = getProductFromRemote(productId); | |
Order order = new Order(); | |
order.setId(1L); | |
// 计算 | |
order.setTotalAmount(productFromRemote.getPrice().multiply(new BigDecimal(productFromRemote.getNum()))); | |
order.setUserId(userId); | |
order.setNickName("Karry.Liu"); | |
order.setAddress("北极"); | |
// 远程查询的结果 | |
order.setProduct(List.of(productFromRemote)); | |
return order; | |
} |
此时双端服务已经打通了,注意到日志:2025-06-24T22:21:11.646+08:00 INFO 3496 — [service-order] [nio-8080-exec-1] c.K.service.impl.OrderServiceImpl : 远程请求: http://192.168.25.1:9000/product/100
{ | |
"id": 1, | |
"totalAmount": 198, | |
"userId": 2, | |
"nickName": "Karry.Liu", | |
"address": "北极", | |
"product": [ | |
{ | |
"id": 100, | |
"price": 99, | |
"productName": "IPhone-100", | |
"num": 2 | |
} | |
] | |
} |
此时我们还有两个问题,一是我们每次只取了第一个服务器,二是这样写太复杂,没有实现负载均衡。
针对此第一个问题将在下一小节中解决,第二个问题将在下一个组件中解决。
# 实现负载均衡 APIs
首先是使用复杂一点的方式,后面将会介绍使用注解的形式。
先引入负载均衡环境依赖。
<!-- 负载均衡 --> | |
<dependency> | |
<groupId>org.springframework.cloud</groupId> | |
<artifactId>spring-cloud-starter-loadbalancer</artifactId> | |
</dependency> |
# 基于方法的负载均衡
将 getProductFromRemote 方法改造为 getProductFromRemoteLoadBalancing:
private Product getProductFromRemoteLoadBalancing(Long productId) { | |
// 获取商品服务所在机器 (负载均衡) | |
ServiceInstance chooseLoadBalancing = loadBalancerClient.choose("service-product"); | |
// 拼接请求地址 http://192.168.25.1:9000/product/100 | |
log.info("服务地址(uri): {}", chooseLoadBalancing.getUri());//http://192.168.25.1:9000 | |
String url = "http://" + chooseLoadBalancing.getHost() + ":" + chooseLoadBalancing.getPort() + "/product/" + productId; | |
log.info("远程请求: {}", url); | |
// 发送请求(远程) | |
return restTemplate.getForObject(url, Product.class); | |
} |
多次请求 http://localhost:8080/create 后观察日志:
服务地址(uri): http://192.168.25.1:9001 | |
远程请求: http://192.168.25.1:9001/product/100 | |
服务地址(uri): http://192.168.25.1:9002 | |
远程请求: http://192.168.25.1:9002/product/100 | |
服务地址(uri): http://192.168.25.1:9000 | |
远程请求: http://192.168.25.1:9000/product/100 |
可以观察到其负载均衡的使用了不同的端口,下面将介绍注解的形式。
# 基于注解的负载均衡
还记得我们之前使用的 RestTemplate 嘛?
@Resource | |
private RestTemplate restTemplate; |
我们可以观察到,无论哪种方法,最终都会是去使用 restTemplate.getForObject (…) 这个方法,如果这个方法自己就可以进行负载均衡呢?我们是不是可以少些一点代码?
改造 ProductServiceConfig 配置类:
@Configuration | |
public class ProductServiceConfig { | |
@LoadBalanced// 使用负载均衡 | |
@Bean | |
public RestTemplate restTemplate() { | |
return new RestTemplate(); | |
} | |
} |
将 getProductFromRemoteLoadBalancing 方法改造为 getProductFromRemoteLoadBalancingWithAnnotation:
private Product getProductFromRemoteLoadBalancingWithAnnotation(Long productId) { | |
String url = "http://service-product/product/" + productId; | |
// 发送请求(远程) | |
return restTemplate.getForObject(url, Product.class); | |
} |
注意到我们的 url 中出现了 service-product,在由于 restTemplate 被追加了 @LoadBalanced 注解,使得整个 restTemplate 自带有负载均衡的能力,url 传过去的时候 service-product 会被自动替换为 IP+HOST 的形式,替换的结果符合负载均衡。
# 配置中心
# 基本用法
引入依赖
<!-- 配置中心 --> | |
<dependency> | |
<groupId>com.alibaba.cloud</groupId> | |
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> | |
</dependency> |
书写导入配置:nacos:service-order.yml
spring: | |
config: | |
import: nacos:service-order.yml | |
cloud: | |
nacos: | |
server-addr: 127.0.0.1:8848 | |
application: | |
name: service-order | |
server: | |
port: 8080 |
在 nacos 中设置名为 service-order.yml
order: | |
timeout: 120s | |
autoConfirm: 7d |
在 controller 加入 @RefreshScope 自动刷新注解
@Value("${order.timeout}") | |
private String orderTimeout; | |
@Value("${order.auto-confirm}") | |
private String orderAutoConfirm; | |
// 获取配置信息 | |
@GetMapping("/getConfig") | |
public String getConfig() { | |
return "order.timeout:" + orderTimeout + " order.autoConfirm:" + orderAutoConfirm; | |
} |
访问后得到回应:order.timeout:120s order.autoConfirm:7d
但是配置中心的依赖导入方法具有广播性,有可能出现其他服务的无法启动问题,因此可以在 yml 中加入:
spring: | |
cloud: | |
nacos: | |
config: | |
import-check: | |
enabled: false |
来禁用检查。
# 无感动态刷新
手动配置很麻烦,通常使用统一导入的方法来实现,创建 OrderProperties:
@Data | |
@Component | |
// 批量获取配置,无需使用 @RefreshScope 即可自动刷新 | |
@ConfigurationProperties(prefix = "order") | |
public class OrderProperties { | |
String timeout; | |
String autoConfirm; | |
} |
@ConfigurationProperties (prefix = “order”) 中 prefix = "order" 获取前缀为 order 的配置,有了 ConfigurationProperties 无需使用 @RefreshScope 即可自动刷新。
改造 OrderController:
@Autowired | |
private OrderProperties orderProperties; | |
// 获取配置信息 | |
@GetMapping("/getConfig") | |
public String getConfig() { | |
return "order.timeout:" + orderProperties.getTimeout() + " order.autoConfirm:" + orderProperties.getAutoConfirm(); | |
} |
# 本地配置与 Nacos 配置冲突时
当本地配置与 Nacos 配置冲突时,优先以 Nacos 中的配置中心为准。
即先导入优先,外部优先。
当:import: nacos:service-order.yml,nacos:common.yml 出现时,仍然是优先以第一次出现的 nacos:service-order.yml 为准。
# 数据隔离
当出现不同环境需要不同配置时,比如 dev 环境、test 环境和 prod 环境,分别需要不同的配置,我们该如何组织?
首先 Nacos 提供了:命名空间 - 组织 - 配置单元的模式,命名空间可以对应到 dev 环境、test 环境和 prod 环境等,组开源对应到不同的微服务比如商品微服务、用户微服务等,配置单元即为具体的详细配置。
首先在 Nacos 命名空间、组织与配置。

这里已经创建了 dev 环境、test 环境和 prod 环境,下面创建详细的组与配置
然后改造 yml 配置文件
spring: | |
profiles: | |
active: dev | |
cloud: | |
nacos: | |
server-addr: 127.0.0.1:8848 | |
config: | |
namespace: ${spring.profiles.active:dev} | |
application: | |
name: service-order | |
server: | |
port: 8080 | |
--- | |
spring: | |
config: | |
activate: | |
on-profile: dev | |
import: | |
- nacos:common.yml?group=order | |
- nacos:database.yml?group=order | |
--- | |
spring: | |
config: | |
activate: | |
on-profile: test | |
import: | |
- nacos:common.yml?group=order | |
- nacos:database.yml?group=order | |
--- | |
spring: | |
config: | |
activate: | |
on-profile: prod | |
import: | |
- nacos:common.yml?group=order | |
- nacos:database.yml?group=order |

具体地:
namespace: ${spring.profiles.active:dev} |
这里主要负责的时从项目到 Nacos 时我们应该选择哪套命名空间,是 Nacos 的命名空间
而对于:
profiles: | |
active: dev |
主要负责的是要激活哪套分片配置,是 on-profile: dev 还是 on-profile: test,还是 on-profile: prod,这里指的是项目的配置分片
此时只需要切换不同的 active: dev,即可完成不同环境的配置切换
# Nacos 总结
来自尚硅谷课堂:https://www.bilibili.com/video/BV1UJc2ezEFU