文章目录

Spring Cloud

1、Spring Cloud 介绍

1.1、架构演进与微服务

架构演进

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

集群与分布式的区别

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

微服务技术分类

功能Dubbo(阿里)Spring CloudSpring Cloud Alibaba(兼容 Dubbo、Spring Cloud)
注册中心Zookeeper、NacosEureka、ConsulEureka、Nacos
服务远程调用Dubbo(Dubbo 协议)Feign(HTTP 协议)Feign、Dubbo
配置中心无Spring Cloud ConfigSpring Cloud Config、Nacos
服务网关无Spring Cloud Gateway、ZuulSpring Cloud Gateway、Zuul
服务监控与保护Dubbo-AdminHystrixSentinel

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 版本
Hoxton2.2.x、2.3.x(Starting with Hoxton SR5)
Greenwich2.1.x
Finchley2.0.x
Edgware1.5.x
Dalston1.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、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐