文章目录

Spring Cloud

1、Spring Cloud 介绍

1.1、架构演进与微服务

架构演进

  • 单体架构:一个项目完成所有功能,适合小型项目。优点:部署简单。缺点:耦合度高,性能较差。
  • 垂直架构:将单体架构拆分为多个独立项目,各个项目功能完整互不干扰,本质还是单体架构。缺点:重复功能较多。
  • 分布式架构:根据业务功能拆分一个大型项目,每个模块独立开发,称为一个微服务优点:耦合度低,利于扩展。缺点:部署运维复杂。

集群与分布式的区别

  • 集群:多个相同功能的模块,部署在不同的机器上。
  • 分布式:多个不同功能的模块,组成一个完整项目。

微服务技术分类

功能 Dubbo(阿里) Spring Cloud Spring Cloud Alibaba(兼容 Dubbo、Spring Cloud)
注册中心 Zookeeper、Nacos Eureka、Consul EurekaNacos
服务远程调用 Dubbo(Dubbo 协议) Feign(HTTP 协议) Feign、Dubbo
配置中心 Spring Cloud Config Spring Cloud Config、Nacos
服务网关 Spring Cloud Gateway、Zuul Spring Cloud Gateway、Zuul
服务监控与保护 Dubbo-Admin Hystrix Sentinel

1.2、Spring Cloud 介绍

Spring Cloud 用于快速构建分布式系统。常用组件包括:服务注册与发现、负载均衡、熔断器、网关、配置中心等。

Spring Cloud 官网:https://spring.io/projects/spring-cloud。

Spring Cloud 项目分类

  • Spring Cloud Netflix
    • Eureka:服务注册与发现。
    • Ribbon:负载均衡。
  • Spring Cloud Alibaba:提供微服务开发的一站式解决方案,包括所有必须组件。
    • Nacos:服务注册与发现、负载均衡、配置管理、数据存储等。
  • Spring Cloud OpenFeign:声明式 HTTP 客户端。
  • Spring Cloud Gateway:网关。
  • Spring Cloud Zuul:网关。
  • Spring Cloud Zookeeper:服务注册与发现。

1.3、Spring Cloud 与 Spring Boot

Spring Cloud 与 Spring Boot 的区别

  • Spring Cloud:用于快速构建分布式系统。
  • Spring Boot:用于快速构建 Spring 应用程序(微服务)。

Spring Cloud 与 Spring Boot 的版本兼容关系

下面 Spring Cloud 版本使用 Hoxton.SR12,对应 Spring Boot 版本使用 2.3.7.RELEASE。

Spring Cloud 版本 Spring Boot 版本
Hoxton 2.2.x、2.3.x(Starting with Hoxton SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.x

2、环境搭建与 RestTemplate 远程调用

2.1、环境搭建 - 父项目(springcloud 项目)

<!--打包方式-->
<packaging>pom</packaging>

<!--依赖管理-->
<dependencyManagement>
    <dependencies>
        <!--Spring Cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR12</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--Spring Boot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.7.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--Mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!--Druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.8</version>
        </dependency>
    </dependencies>
</dependencyManagement>

2.2、环境搭建 - 提供者(user 模块)

步骤

  • 一、创建数据库。
  • 二、在 user 中,导入依赖、配置 YML、创建启动类。
  • 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
  • 四、测试。访问 http://localhost:8081/user/1。

示例

  • 一、创建数据库。
create database db_user;
use db_user;
create table user (
    user_id int auto_increment,
    user_name varchar(100),
    user_address varchar(100),
    primary key (user_id)
);
insert into user (user_name,user_address) values
('乔峰','陕西省西安市'),
('虚竹','河南省郑州市'),
('段誉','云南省大理市');
select * from user;
  • 二、在 user 中,导入依赖、配置 YML、创建启动类。
<!--Web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--Mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--MySQL-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
server:
  port: 8081
spring:
  application:
    name: user
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db_user?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=UTF-8
      username: root
      password: test123!
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
@SpringBootApplication
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}
  • 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer userId;
    private String userName;
    private String userAddress;
}
@Repository
@Mapper
public interface UserDao {
    @Select("select * from user where user_id = #{id}")
    User selectById(int id);
}
public interface UserService {
    User selectById(int id);
}
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;

    @Override
    public User selectById(int id) {
        return userDao.selectById(id);
    }
}
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User selectById(@PathVariable("id") int id) {
        return userService.selectById(id);
    }
}
  • 四、测试。访问 http://localhost:8081/user/1。
{
  "userId": 1,
  "userName": "乔峰",
  "userAddress": "陕西省西安市"
}

2.3、环境搭建 - 消费者(order 模块)

步骤

  • 一、创建数据库。
  • 二、在 order 中,导入依赖、配置 YML、创建启动类。(注意:使用 User 类需要导入依赖,或存在该实体类)
  • 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
  • 四、测试。访问 http://localhost:8080/order/1。

示例

  • 一、创建数据库。
create database db_order;
use db_order;
create table `order` (
	order_id int auto_increment,
    order_name varchar(100),
    order_price int,
    user_id int,
    primary key (order_id)
);
insert into `order` (order_name,order_price,user_id) values
('苹果 14',4999,1),
('平板',2499,2),
('手表',2999,3);
select * from `order`;
  • 二、在 order 中,导入依赖、配置 YML、创建启动类。(注意:使用 User 类需要导入依赖,或存在该实体类)
<!--User-->
<dependency>
    <groupId>com.feng</groupId>
    <artifactId>user</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

<!--Web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--Mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--MySQL-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!--Lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
server:
  port: 8080
spring:
  application:
    name: order
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db_order?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=UTF-8
      username: root
      password: test123!
mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
  • 三、创建 Pojo 层、Dao 层、Service 层、Controller 层。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private Integer orderId;
    private String orderName;
    private Integer orderPrice;
    private Integer userId;
    private User user;
}
@Repository
@Mapper
public interface OrderDao {
    @Select("select * from `order` where order_id = #{id}")
    Order selectById(int id);
}
public interface OrderService {
    Order selectById(int id);
}
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;

    @Override
    public Order selectById(int id) {
        return orderDao.selectById(id);
    }
}
@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @GetMapping("/{id}")
    public Order selectById(@PathVariable("id") int id) {
        return orderService.selectById(id);
    }
}
  • 四、测试。访问 http://localhost:8080/order/1。
{
  "orderId": 1,
  "orderName": "苹果 14",
  "orderPrice": 4999,
  "userId": 1,
  "user": null
}

2.4、RestTemplate 远程调用

RestTemplate 属于 Spring 项目,用于发起远程调用。HTTP 请求不用关心语言,只需提供 IP、端口、路径、请求参数。

步骤

  • 一、在 order 的启动类中,注入 RestTemplate。(或使用配置类)
  • 二、在 order 的 Service 层方法中,使用 RestTemplate 发起远程调用
  • 三、测试。访问 http://localhost:8080/order/1。

示例

  • 一、在 order 的启动类中,注入 RestTemplate(或使用配置类)。
@SpringBootApplication
public class OrderApplication {
    @Bean
    public RestTemplate resTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
  • 二、在 order 的 Service 层方法中,使用 RestTemplate 发起远程调用
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public Order selectById(int id) {
        // 一、查询订单。
        Order order = orderDao.selectById(id);
        // 二、使用 RestTemplate 发起 HTTP 请求,查询用户。
        String url = "http://localhost:8081/user/" + order.getUserId();
        User user = restTemplate.getForObject(url, User.class);
        // 三、封装 User。
        order.setUser(user);
        // 四、返回。
        return order;
    }
}
  • 三、测试。访问 http://localhost:8080/order/1。
{
  "orderId": 1,
  "orderName": "苹果 14",
  "orderPrice": 4999,
  "userId": 1,
  "user": {
    "userId": 1,
    "userName": "乔峰",
    "userAddress": "陕西省西安市"
  }
}

Spring Cloud Netflix

3、Eureka 服务注册与发现

3.1、Eureka 介绍

Eureka 用于服务注册与发现。核心包括:

  • Eureka Server(服务端):注册中心。用于存储服务信息,监控服务心跳。如果 Eureka Server 在短时间内丢失过多心跳,则会进入自我保护模式,避免因网络故障或实例故障而误删健康实例的注册信息,称为 Eureka Server 自我保护机制。
  • Eureka Client(客户端):服务提供者与消费者。
    • 服务提供者:服务注册。注册自身到 Eureka Server 中,每隔 30 秒发送一次心跳到 Eureka Server。例如:user-service。
    • 服务消费者:服务发现。从 Eureka Server 中获取服务列表,根据负载均衡策略,选取一个服务发起远程调用。例如:order-service。

3.2、注册中心

步骤

  • 一、创建 eureka-server 模块,导入 Eureka Server 依赖。
  • 二、配置 Eureka Server。
  • 三、在启动类中,使用 @EnableEurekaServer 开启服务。
  • 四、测试。访问 http://localhost:8700。

示例

  • 一、创建 eureka-server 模块,导入 Eureka Server 依赖。
<!--Eureka Server-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
  • 二、配置 Eureka Server。
server:
  port: 8700 # 端口号,默认 8761
spring:
  application:
    name: eureka-server # 名称
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8700/eureka/ # 地址,默认 http://localhost:8761/eureka/
  • 三、在启动类中,使用 @EnableEurekaServer 开启服务。
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
  • 四、测试。访问 http://localhost:8700。

3.3、服务注册(eureka-user 模块)

步骤

  • 一、创建 eureka-user 模块,导入 Eureka Client 依赖。
  • 二、配置 Eureka Client。
  • 三、测试。访问 http://localhost:8700。

示例

  • 一、创建 eureka-user 模块,导入 Eureka Client 依赖。
<!--Eureka Client-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 二、配置 Eureka Client。
spring:
  application:
    name: eureka-user
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8700/eureka/
  • 三、测试。访问 http://localhost:8700。

3.4、服务发现(eureka-order 模块)

步骤

  • 一、创建 eureka-order 模块,导入 Eureka Client 依赖。
  • 二、配置 Eureka Client。
  • 三、在 RestTemplate 上,使用 @LoadBalanced 负载均衡注解。
  • 四、在 Service 层的方法中,使用服务提供者的名称发起远程调用。
  • 五、测试。访问 http://localhost:8080/order/1。

示例

  • 一、创建 eureka-order 模块,导入 Eureka Client 依赖。
<!--Eureka Client-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 二、配置 Eureka Client。
spring:
  application:
    name: eureka-order
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8700/eureka/
  • 三、在启动类中,使用 @LoadBalanced 负载均衡注解。
@Bean
@LoadBalanced
public RestTemplate resTemplate() {
    return new RestTemplate();
}
  • 四、在 Service 层的方法中,使用服务提供者的名称发起远程调用。
String url = "http://eureka-user/user/" + order.getUserId();
  • 五、测试。访问 http://localhost:8080/order/1。

3.5、注册中心集群

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/,http://localhost:7003/eureka/

3.6、IDEA 启动多服务

在 IDEA 中,右键 Services 窗口中的服务,点击 Copy Configuration 修改端口号然后启动,以下方式任选其一。

  • VM options:-Dserver.port=8082。
  • Program arguments:–server.port = 8083。

4、Ribbon 负载均衡

4.1、负载均衡原理

Ribbon 属于 Spring Cloud Netflix 项目,用于负载均衡。

负载均衡流程

  • 一、发起请求。order-service 使用服务名称发起请求,http://user-service/user/1 不是真实地址。
  • 二、拉取服务列表,获得真实地址。Ribbon 根据服务名称,从 Eureka Server 中拉取服务列表,获得真实地址(localhost:8081、localhost:8082、localhost:8083)。
  • 三、负载均衡。Ribbon 根据负载均衡策略,选择其中一个服务(localhost:8081)发起远程调用。

负载均衡原理

LoadBalancerInterceptor 类实现了 ClientHttpRequestInterceptor 接口,可以拦截 HTTP 请求(RestTemplate)。发起请求之后:

  • 一、首先调用 LoadBalancerInterceptor 类的 intercept() 方法,根据请求路径(http://user-service/user/1)获得服务名称(user-service)。
  • 二、然后调用 LoadBalancerClient 对象的 execute() 方法,根据服务名称(user-service)获得 ILoadBalancer 对象(DynamicServerListLoadBalancer 包含服务列表)。
  • 三、最后调用 ILoadBalancer 的父类 BaseLoadBalancer 的 chooseServer() 方法。
  • 四、最终调用 IRule 对象的 choose() 方法,实现负载均衡,默认 ZoneAvoidanceRule。

4.2、负载均衡策略

负载均衡策略分类

Ribbon 负载均衡策略由 IRule 接口定义,其实现类就是具体规则。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

内置负载均衡规则类 说明
RoundRobinRule 轮询。默认
WeightedResponseTimeRule 根据服务的响应时间设置权重,再做轮询
ZoneAvoidanceRule 根据服务的 Zone 进行分类,再做轮询
AvailabilityFilteringRule 根据服务的可用性进行筛选,再做轮询
RandomRule 随机

负载均衡策略修改

  • 方式一、注入 IRule。
  • 方式二、配置方式。

示例

  • 方式一、注入 IRule。

在 eureka-order 服务的启动类中,注入 IRule(或使用配置类)。在该服务中调用其他服务都使用随机(全局)。

@Bean
public IRule iRule() {
    return new RandomRule();
}
  • 方式二、配置方式。

在 eureka-order 服务的配置文件中,配置策略名称。在该服务中仅调用 eureka-user 服务使用随机(单体)。

eureka-user:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡策略

4.3、懒加载与饥饿加载

负载均衡默认使用懒加载,在第一次访问时才会创建 LoadBalanceClient,访问时间过长。

开启饥饿加载,则会在项目启动时创建 LoadBalanceClient,降低第一次访问时间。

示例

  • 开启饥饿加载

在 eureka-order 服务的配置文件中,配置以下内容。

ribbon:
  eager-load:
    enabled: true # 开启饥饿加载,默认 false
    clients: eureka-user # 指定服务名称

Spring Cloud Alibaba

5、Nacos 服务注册与发现

5.1、Nacos 注册中心(Nacos 安装)

Nacos 官网:https://www.nacos.io/。

Nacos 下载:https://github.com/alibaba/nacos。

  • nacos-server-1.4.1.tar.gz:Linux 版解压包。
  • nacos-server-1.4.1.zip:Windows 版解压包。

Nacos 安装(Nacos 注册中心)

  • 一、启动 Nacos 注册中心。解压 nacos-server-1.4.1.zip,进入 D:\MyTools\nacos\bin,执行 Windows 命令。
  • 二、测试。访问 http://localhost:8848/nacos/index.html,默认账号/密码:nacos/nacos。
# Windows 命令,-m standalone 表示单机模式运行
startup.cmd -m standalone

5.2、服务注册(user-service 模块)

  • 一、在父项目中,导入 Spring Cloud Alibaba 管理依赖。
  • 二、在 user-service 中,注掉 Eureka 依赖,添加 Nacos 依赖。
  • 三、在 user-service 中,注掉 Eureka 地址,添加 Nacos 地址。
  • 四、测试。在 Nacos 控制台中查看服务列表,访问 http://localhost:8080/user/1。

示例

<!--Spring Cloud Alibaba 管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<!--Nacos 客户端-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848

5.3、服务发现(order-service 模块)

  • 一、在父项目中,导入 Spring Cloud Alibaba 管理依赖。
  • 二、在 order-service 中,注掉 Eureka 依赖,添加 Nacos 依赖。
  • 三、在 order-service 中,注掉 Eureka 地址,添加 Nacos 地址。
  • 四、测试。在 Nacos 控制台中查看服务列表,访问 http://localhost:8080/order/1。

示例

<!--Spring Cloud Alibaba 管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<!--Nacos 客户端-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848

6、Nacos 负载均衡与环境隔离

6.1、服务实例分级存储

如果配置了服务集群,则服务消费者优先访问同集群的服务提供者。

服务实例分级存储模型

  • 一级:服务。例如:user-service、order-service。
  • 二级:集群。例如:北京、上海。
  • 三级:服务实例。例如:在北京机房内部署了 user-service 的服务器。

示例

server:
  port: 8081
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        cluster-name: Beijing # 集群,默认 DEFAULT,表示所有服务都在同一个集群
server:
  port: 8082
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        cluster-name: Beijing # 集群,默认 DEFAULT,表示所有服务都在同一个集群
server:
  port: 8083
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        cluster-name: Shanghai # 集群,默认 DEFAULT,表示所有服务都在同一个集群

6.2、NacosRule 负载均衡

在默认情况下,Nacos 使用 Ribbon 负载均衡(默认轮询)。

另外 Nacos 提供 NacosRule 用于负载均衡,NacosRule 类实现了 AbstractLoadBalancerRule 抽象类。

NacosRule 负载均衡

服务消费者优先访问同集群的服务提供者,随机选择一个。如果同集群找不到,则会访问其他集群,并且会报警告。

步骤

  • 一、在 order-service 中配置集群。
  • 二、在 order-service 中配置 IRule。

示例

  • 一、在 order-service 中配置集群。
server:
  port: 8080
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        cluster-name: Beijing # 集群,默认 DEFAULT,表示所有服务都在同一个集群
  • 二、在 order-service 中配置 IRule。
@Bean
public IRule iRule() {
    return new NacosRule();
}
#user-service:
#  ribbon:
#    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # Nacos 负载均衡策略

order-service 优先访问 Beijing 的 user-service。如果 Beijing 的 user-service 发生故障,则会访问 Shanghai,并且会报警告(发生跨集群调用)。

2022-03-18 16:27:34.235  WARN 7480 --- [nio-8080-exec-1] c.alibaba.cloud.nacos.ribbon.NacosRule   : A cross-cluster call occurs,name = user-service, clusterName = Beijing, instance = [Instance{instanceId='192.168.1.2#8083#Shanghai#DEFAULT_GROUP@@user-service', ip='192.168.1.2', port=8083, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='Shanghai', serviceName='DEFAULT_GROUP@@user-service', metadata={preserved.register.source=SPRING_CLOUD}}]

6.3、权重负载均衡

在实际项目部署过程中,服务器性能存在差异,因此可以调整服务实例权重,权重越高被访问的频率越高。

在 Nacos 控制台中,可以设置服务实例权重。默认 1,通常设置 0~1 之间。例如:设置 0.1 基本不会被访问,设置 0 完全不会被访问。

6.4、Nacos 环境隔离

Nacos 使用 Namespace 用于环境隔离:(Namespace(Group(Service/Data)))。例如可以设置 dev、test 等。环境隔离规则如下:

  • 所有服务的 Namespace 默认都是 public,表示在同一个 Namespace 中,互相可见。
  • 每个 Namespace 都有唯一 ID,不同 namespace 中的服务不可见。

步骤

  • 一、在 Nacoc 控制台中,创建 Namespace,生成命名空间 ID。
  • 二、在 order-service 中,配置 Namespace。
  • 三、测试 。访问 http://localhost:8080/order/1。

示例

  • 一、在 Nacoc 控制台中,创建 namespace,生成命名空间 ID。

  • 二、在 order-service 中,配置 namespace。

spring:
  cloud:
    nacos:
      discovery:
        namespace: 30d8ab5f-eaea-4f3e-aec9-be468793d481 # 命名空间
  • 三、测试。访问 http://localhost:8080/order/1。
java.lang.IllegalStateException: No instances available for user-service

6.5、CAP 原则

在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)这三个要素最多只能同时实现两点,不能兼得。

  • 一致性(Consistency):同一时刻,所有节点的数据必须完全一致。
  • 可用性(Availability):如果部分节点出现故障,集群整体必须能够响应客户端的请求。
  • 分区容错性(Partition Tolerance):如果网络分区(节点之间无法通信),系统能够继续运行。

三者为什么不能兼得

假如集群中的两个节点 N1 与 N2 之间无法通信,则会出现以下情况:

  • 如果保证一致性,则 N2 需要在数据未同步时拒绝响应,牺牲可用性(CP 模式)。
  • 如果保证可用性,则 N2 需要响应旧数据,牺牲一致性(AP 模式)。
  • 如果放弃分区容错性,则使用单点架构,失去分布式系统的意义(CA 模式)。

6.6、Nacos 与 Eureka 的区别

Nacos 与 Eureka 的相同点

在默认情况下,两者的实例都是临时实例,使用心跳检测模式,如果实例不正常则会被剔除,使用 AP 模式。

Nacos 与 Eureka 的不同点

Nacos 可以设置非临时实例,使用主动检测模式,如果实例不正常不会被剔除,使用 CP 模式。

另外 Nacos 支持服务列表更新的消息推送,服务列表更新更及时。

Nacos 设置非临时实例

spring:
  cloud:
    nacos:
      discovery:
        ephemeral: false # 是否为临时实例,默认 ture。(ephemeral 临时)

7、Nacos 配置管理

7.1、配置创建

在 Nacos 控制台中。建议配置经常需要变更的内容。

示例

  • Data ID:user-service-dev.yaml。
  • Group:DEFAULT_GROUP。
  • 描述:user-service 的开发环境配置文件。
  • 格式:YAML。(目前支持 YAML、Properties)
pattern:
  dateformat: yyyy-MM-dd HH:mm:ss

7.2、微服务配置拉取

配置读取顺序

  • 【项目启动】→→【读取 Nacos 中的配置 】→→【读取本地配置】→→【创建 Spring 容器】→→【注入 Bean】

bootstrap.yml

引导文件,优先级高于 application.yml。

步骤

  • 一、导入 Nacos 配置管理依赖。
  • 二、在 user-service 中创建 bootstrap.yml,属于引导文件,优先级高于 application.yml。
  • 三、在 UserController 中,注入 pattern.dateformat 属性。
  • 四、测试。访问 http://localhost:8081/user/now。

示例

  • 一、导入 Nacos 配置管理依赖。
<!--Nacos 配置管理-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  • 二、在 user-service 中,创建 bootstrap.yml,注掉 application.yml 中的相同内容。
spring:
  application:
    name: user-service
  profiles:
    active: dev # 环境
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos 地址
      config:
        file-extension: yaml # 配置文件扩展名
  • 三、在 UserController 中,注入 pattern.dateformat 属性。
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/now")
    public String getDate() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }

    @GetMapping("/{id}")
    public User selectById(@PathVariable("id") int id) {
        return userService.selectById(id);
    }
}
  • 四、测试。访问 http://localhost:8081/user/now。

7.3、配置热更新

Nacos 中的配置文件更新后,微服务无需重启就可生效。

  • 方式一、在 @Value 注入变量的类上,使用 @RefreshScope 注解。
  • 方式二、在配置类上,使用 @Component 与 @ConfigurationProperties 注解。

示例

  • 方式一、在 @Value 注入变量的类上,使用 @RefreshScope 注解。
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/now")
    public String getDate() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}
  • 方式二、在配置类上,使用 @Component 与 @ConfigurationProperties 注解。
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}
@Autowired
private PatternProperties properties;

@GetMapping("/now")
public String getDate() {
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}

7.4、多环境配置共享

微服务启动时会从 Nacos 读取多个配置文件。例如:

  • 在 Nacos 控制台中:user-service-dev.yaml。
  • 在 Nacos 控制台中:user-service.yml。
  • 在 user-service 中:application.yml。

优先级顺序 user-service-dev.yaml > user-service.yml > application.yml。

7.5、Nacos 集群(了解)

Spring Cloud OpenFeign

8、Feign 远程调用

8.1、Feign 介绍

Feign 使用声明式的 HTTP 客户端接口,封装远程调用过程,可以代替 RestTemplate。

RestTemplate 与 Feign 的区别

  • RestTemplate 远程调用:URL 参数复杂难以维护,代码耦合度高且不能实现复用。
String url = "http://user-service/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
  • Feign 远程调用:结构优雅,代码耦合度低且可以实现复用。

8.2、Feign 远程调用

步骤

  • 一、在 order-service 中,导入 Feign 依赖
  • 二、在 order-service 中的启动类上,使用 @EnableFeignClients 注解开启 Feign 功能。
  • 三、创建 Feign 接口(声明式的 HTTP 客户端),使用 Spring MVC 注解声明远程调用的信息。包括:服务名称、请求方式、请求路径、请求参数,返回值类型。
  • 四、使用 Feign 接口中的方法发起远程调用,代替 RestTemplate。

示例

  • 一、在 order-service 中,导入 Feign 依赖
<!--Feign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 二、在 order-service 中的启动类上,使用 @EnableFeignClients 注解开启 Feign 功能。
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
    public static void main(String[] args){
        SpringApplication.run(OrderApplication.class, args);
    }
}
  • 三、创建 Feign 接口(声明式的 HTTP 客户端),使用 Spring MVC 注解声明远程调用的信息。包括:服务名称、请求方式、请求路径、请求参数,返回值类型。
@FeignClient("user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User selectById(@PathVariable("id") int id);
}
  • 四、使用 Feign 接口中的方法发起远程调用,代替 RestTemplate。
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;

//    @Autowired
//    private RestTemplate restTemplate;

    @Autowired
    private UserClient userClient;

    @Override
    public Order selectById(int id) {
        // 一、查询订单。
        Order order = orderDao.selectById(id);
        // 二、使用 RestTemplate 发起 HTTP 请求,查询用户。
//        String url = "http://user-service/user/" + order.getUserId();
//        User user = restTemplate.getForObject(url, User.class);
        // 二、使用 Feign 发起远程调用,代替 RestTemplate。
        User user = userClient.selectById(order.getUserId());
        // 三、封装 User。
        order.setUser(user);
        // 四、返回。
        return order;
    }
}

注意:@FeignClient 与 @RequestMapping 冲突,如果同时存在则 Spring 容器启动报错。

@FeignClient("user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User selectById(@PathVariable("id") int id);
}
// Spring 容器启动报错
@FeignClient("user-service")
@RequestMapping("/user")
public interface UserClient {
    @GetMapping("/{id}")
    User selectById(@PathVariable("id") int id);
}

8.3、Feign 远程调用规则

Feign 远程调用规则,要求 FeignClient(消费者)与 Controller(提供者)中方法的请求路径、请求方式、参数列表完全相同,否则会抛出异常。

  • FeignClient(消费者)
@FeignClient("user-service")
public interface UserClient {
    @GetMapping("/user/{id}")
    User selectById(@PathVariable("id") int id);
}
  • Controller(提供者)
@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/{id}")
    public User selectById(@PathVariable("id") int id) {
        return userService.selectById(id);
    }
}

Feign 的最佳实践

  • 方式一、为 FeignClient 与 Controller 定义统一的父接口作为标准。
  • 方式二、将 FeignClient 抽取为独立模块,并将与接口有关的 Pojo、Feign 的配置类都放在这个模块中,提供给消费者使用。

8.4、方式一、继承

一般情况,不建议在消费者与提供者之间共享接口,因为会造成紧耦合,且 Spring MVC 不支持这种方式(@GetMapping、@PathVariable 等注解不能被继承)。

  • 统一的父接口
public interface UserAPI {
    @GetMapping("/user/{id}")
    User selectById(@PathVariable("id") int id);
}
  • FeignClient(消费者)
@FeignClient("user-service")
public interface UserClient extends UserAPI {// 继承父接口
}
  • Controller(提供者)
@RestController
@RequestMapping("/user")
public class UserController implements UserAPI {// 实现父接口
    @GetMapping("/{id}")
    public User selectById(@PathVariable("id") int id) {
        return userService.selectById(id);
    }
}

8.5、方式二、抽取

  • 一、创建 feign-api 模块,导入 Feign 依赖。
  • 二、将 order-service 中的 UserClient、User 放到该模块中。
  • 三、在 order-service 中,导入 feign-api 依赖。

Spring Cloud Gateway

9、Gateway 网关

9.1、介绍

Spring Cloud 网关

  • Spring Cloud Gateway:基于 Spring 5 中的 WebFlux,属于响应式编程,性能更好。
  • Spring Cloud Zuul:基于 Servlet,属于阻塞式编程。

网关功能

  • 身份认证与权限校验。
  • 服务路由、负载均衡。
  • 请求限流。

流程

【请求】→→→→【Gateway】→→→→【Nacos】→→→→【微服务】

9.2、Gateway 网关(gateway 模块)

步骤

  • 一、在 gateway 中,导入 Gateway、Nacos 客户端依赖,创建启动类。
  • 二、在 gateway 中,配置 Gateway、Nacos。
  • 三、测试。启动并访问 http://localhost:10010/order/1。

示例

  • 一、在 gateway 中,导入 Gateway、Nacos 客户端依赖。
<!--Gateway-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--Nacos 客户端-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
  • 二、在 gateway 中,配置 Gateway、Nacos。
server:
  port: 10010 # 端口,默认 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848
    gateway: # 网关
      routes: # 路由
        - id: user-service # 路由 ID,自定义只要唯一即可
          # uri: http://localhost:8081 # 路由目标地址,固定地址写法
          uri: lb://user-service # 路由目标地址,服务名称写法,lb 表示负载均衡
          predicates: # 断言,判断 HTTP 请求是否符合路由规则
            - Path=/user/** # 路径匹配
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
  • 三、测试。启动并访问 http://localhost:10010/order/1。

9.3、路由断言工厂

网关路由可以配置的内容包括:

  • 路由 ID:路由唯一标识。
  • URI:路由目标地址。
  • Predicates:路由断言,判断是否符合要求。
  • Filters:路由过滤器,处理请求与响应。

11 种路由断言工厂(Route Predicate Factory)

详见 https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories。

工厂 说明 示例
After 必须在某个时间点之后 - After=2017-01-20T17:42:47.789-07:00[America/Denver]
Before 必须在某个时间点之前 - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
Before 必须在两个时间点之间 - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookie 必须包含某些 Cookie - Cookie=chocolate, ch.p
Header 必须包含某些 Header - Header=X-Request-Id, \d+
Host 必须访问某个 Host(域名) - Host=**.somehost.org, **.anotherhost.org
Method 请求方式必须指定方式 - Method=GET, POST
Path 请求路径必须符合指定规则 - Path=/red/{segment}, /blue/{segment}
Query 请求参数必须包含指定参数 - Query=green
RemoteAddr 请求者的 IP 必须是指定范围 - RemoteAddr=192.168.1.1/24
Weight 权重处理

示例

predicates:
  - Path=/order/**
  - After=2017-01-20T17:42:47.789-07:00[America/Denver]

9.4、路由过滤器

GatewayFilter(路由过滤器,或网关过滤器),用于对进入网关的请求与微服务返回结果进行处理。

31 种路由过滤器工厂

详见 https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories。

工厂 说明
AddRequestHeader 为请求添加一个请发头
RemoveRequestHeader 从请求中移除一个请求头
AddResponseHeader 为响应结果添加一个响应头
RemoveResponseHeader 从响应结果中移除一个响应头

示例

  • 一、在 gateway 中,配置过滤器。
server:
  port: 10010 # 端口,默认 8080
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos 地址,默认 localhost:8848
    gateway:
      routes:
        - id: user-service # 路由唯一 ID,只要唯一即可,建议使用服务名称
          uri: lb://user-service # 路由目标地址,lb 表示负载均衡,服务名称写法(uri: http://localhost:8081 # 路由目标地址,固定地址写法)
          predicates: # 断言,判断 HTTP 请求是否符合路由规则
            - Path=/user/** # 路径匹配
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
            - After=2017-01-20T17:42:47.789-07:00[America/Denver] # 必须在某个时间点之后
          filters: # 过滤器
            - AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头
      default-filters: # 默认过滤器
        - AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头
  • 二、在 order-service 中,修改方法。
@GetMapping("/{id}")
public Order selectById(@PathVariable("id") int id,
                        @RequestHeader(name = "truth", required = false) String s) {
    System.out.println(s);
    return orderService.selectById(id);
}

9.5、全局过滤器

GlobalFilter(全局过滤器),用于对进入网关的请求与微服务返回结果做处理。

GlobalFilter 与 GatewayFilter 的区别

  • GlobalFilter:使用代码,处理逻辑可以自定义。实现 GlobalFilter 接口即可。
  • GatewayFilter:使用配置,处理逻辑固定。

需求

定义全局过滤器,拦截请求,判断请求参数中是否存在 authorization 且值为 admin,是则放行,否则拦截。

示例

  • 一、在 gateway 中,定义全局过滤器。
//@Order(-1)// 全局过滤器执行顺序,值越小则优先级越高。(等价于实现 Ordered 接口)
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 一、获取请求参数。
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> params = request.getQueryParams();
        // 二、获取请求参数中的 authorization 的值。
        String auth = params.getFirst("authorization");
        // 三、判断值是否为 admin。
        if ("admin".equals(auth)) {
            // 四、是,放行。
            return chain.filter(exchange);
        }
        // 五、否,拦截请求并设置状态码。
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        return exchange.getResponse().setComplete();
    }

    @Override
    public int getOrder() {
        return -1;
    }
}
  • 二、测试。访问 http://localhost:10010/order/1 与 http://localhost:10010/order/1?authorization=admin。

9.6、过滤链执行顺序

进入网关的请求,会经过三种过滤器:当前路由 Filter、DefaultFilter、GlobalFilter。这三种过滤器会被合并到一个过滤链(集合)中,排序后依次执行。

过滤链执行顺序

  • 一、每个过滤器都必须指定一个 int 类型的 order 值,值越小则优先级越高。
  • 二、GlobalFilter 使用 @Order 注解或实现 Ordered 接口指定 order 值。
  • 三、当前路由 Filter、DefaultFilter 的 order 值由 Spring 指定,默认按照声明顺序从 1 递增。
  • 四、如果过滤器的 order 值相同,则按照顺序 DefaultFilter > 当前路由 Filter > GatewayFilter。
          filters: # 过滤器
            - AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头 1
            - AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头 2
            - AddRequestHeader=truth, you are a good person!!! # 为请求添加一个请发头 3
      default-filters: # 默认过滤器
        - AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头 1
        - AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头 2
        - AddRequestHeader=truth, you are a very good person!!! # 为请求添加一个请发头 3

9.7、跨域问题处理(了解)

发起请求时域名不同,称为跨域。包括:

  • 域名不同:www.taobao.com、www.taobao.org、www.jd.com、miaosha.jd.com。
  • 端口不同:localhost:8081、localhost:8082、localhost:8083。

注意:跨域属于浏览器禁止请求的发起者与服务端发生跨域 Ajax 请求,请求被浏览器拦截。order-service 到 user-service 不是跨域。

解决方案:配置 CORS。

spring:
  cloud:
    gateway:
      # CORS 跨越配置
      globalcors: # 全局 CORS
        add-to-simple-url-handler-mapping: ture # 解决 Options 请求被拦截的问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许的 URL
              - http://localhost:8090
            allowedMethods: # 允许的 Ajax 请求方式
              - GET
              - POST
              - DELETE
              - PUT
              - OPTIONS
            allowedHeader: * # 允许的头信息
            allowedCredentials: true # 是否允许 Cookie
            maxAge: 360000 # 有效期,单位秒
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐