当前位置:   article > 正文

SpringCloud(微服务介绍,远程调用RestTemplate,注册中心Nacos,负载均衡Ribbon,环境隔离,进程和线程的区别)【详解】

SpringCloud(微服务介绍,远程调用RestTemplate,注册中心Nacos,负载均衡Ribbon,环境隔离,进程和线程的区别)【详解】

目录

一、微服务介绍

1. 系统架构的演变

1 单体架构

2 分布式服务

3 微服务

2. SpringCloud介绍

SpringCloud简介

SpringCloud版本

3. 小结

二、远程调用RestTemplate【理解】

1. 服务拆分

1 服务拆分原则

2 服务拆分示例

1) 创建父工程

2) 准备用户服务

1. 用户服务的基础代码

2. 启动测试

3) 准备订单服务

1. 订单服务的基础代码

2. 启动测试

2. 远程调用RestTemplate【了解】

1 实现远程调用

说明

实现

测试

2 提供者与消费者

3. 小结

三、注册中心Nacos【重点】

1. 介绍

1 服务治理问题

2 Nacos简介

3 Nacos安装

2. Nacos使用入门

1 添加坐标

2 配置注册中心地址

3 开启服务发现功能

4 功能测试

3. Nacos的原理

1 临时实例与非临时实例

2 Nacos的原理

4. 常见错误

5. 小结

四、负载均衡Ribbon

1. 负载均衡简介

1 什么是负载均衡

2 Ribbon负载均衡

2. Ribbon效果演示

1 搭建用户服务集群

2 订单服务调用用户服务

3. Ribbon实现原理分析

4. Ribbon负载均衡策略

5. 饥饿加载

6. 小结

五、Nacos分组存储与环境隔离

1. Nacos分级存储模型

1 配置实例集群

1 Nacos里实例集群的概念

2 配置实例集群

3 查看配置效果

2 同集群优先访问

1 配置负载均衡策略

2 测试效果

3 Nacos的服务实例权重

1 设置服务实例的权重

2 测试效果

2. namespace环境隔离

1 创建namespace

2 给微服务指定namespace

3 隔离效果

3. 小结

六、进程和线程的区别


一、微服务介绍

本章节学习目标:

  • 什么是微服务,微服务有哪些特征
  • SpringCloud是什么
  • SpringCloud与Dubbo的区别

1. 系统架构的演变

1 单体架构

将业务的所有功能集中在一个项目中开发,打成一个包部署。当网站流量很小时,单体架构非常合适。

单体架构的优缺点如下:

优点:

  • 架构简单

  • 部署成本低

缺点:

  • 耦合度高(维护困难、升级困难)

2 分布式服务

分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。

分布式架构的优缺点:

优点:

  • 降低服务耦合

  • 有利于服务升级和拓展

缺点:

  • 服务有重复代码

  • 服务调用关系错综复杂

  • 服务容错性较差

3 微服务

微服务其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。

因此,可以认为微服务是一种经过良好架构设计的分布式架构方案,微服务以如下特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责。一个服务只做一件事

  • 服务自治:团队独立、技术独立、数据独立,独立部署和交付

  • 面向服务:服务对外暴露统一标准的接口,与语言和技术无关。SpringCloud体系里使用HTTP协议的接口

  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

但是使用了微服务,也会带来一些新的问题(相对于单体架构来说):

  • 增加了系统间的通信成本

  • 增加了数据一致性问题,分布式事务问题等等

  • 服务数量增加,运维压力大

一旦采用微服务系统架构,就势必会遇到这样几个问题:

  • 这么多小服务,如何管理他们的地址?服务治理的问题,可以使用“注册中心”来解决

  • 这么多小服务,他们之间如何通讯?远程调用,可以使用httpclient、RestTemplate、OpenFeign(优雅的远程调用技术)来解决

  • 这么多小服务,客户端怎么访问他们?要使用网关实现统一的对外访问入口,Gateway来解决

  • 这么多小服务,一旦出现问题了,应该如何自处理?要实现服务的隔离防止雪崩,Hystrix或者Sentinel等解决

对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。

2. SpringCloud介绍

微服务的实现方式很多,例如dubbo+zookeeper, SpringCloud等等。那么这两种微服务实现方案有什么区别呢?

微服务技术方案对比

SpringCloud微服务解决方案:不是一个框架,是一系列框架集合,目前包含二十多个框架,还在不断增加中……

SpringCloud简介

官网地址:Spring Cloud

中文文档(非官方):Spring Cloud中文网-官方文档中文版

SpringCloud是Pivotal团队提供的、基于SpringBoot开箱即用的、一站式微服务解决方案的框架体系。目前已成为国内外使用最广泛的微服务框架。

SpringCloud集成了各种微服务功能组件。

其中常见的组件包括:

SpringCloud版本

SpringCloud的版本命名比较特殊,因为它不是一个组件,而是许多组件的集合,它的命名是以A到Z的为首字母的一些单词(其实是伦敦地铁站的名字)组成:

我们在项目中,会使用Hoxton.SR10版本,对应的SpringBoot版本为2.3.x

3. 小结

系统架构的演进:

  • 单体架构:all in one,一切功能模块全部在一个服务里

    好处:简单省钱

    缺点:耦合性强

  • 分布式架构:拆。把一个系统拆分成多个不同的子系统,每个子系统有不同的功能,所有子系统组合起来才是完整系统

    优点:耦合性降低了

    缺点:重复代码

  • 微服务架构:是一种经过良好设计的分布式架构,

    有一些特征

    • 单一职责:每个服务只做一件事,服务的粒度比较细

    • 服务自治:每个服务可以有独立的技术、独立的团队、独立的数据、独立发布部署

    • 面向服务:所有服务要遵循相同的协议暴露访问接口,所有的服务之间才可以互相调用通信

    • 服务保护:要防止某个服务出错,导致调用者也出错,最终出现大面积的崩溃。要做好隔离和保护

    缺点:

    • 通信成本高。因为服务之间需要跨网络进行请求调用

    • 运维成本高。架构比较复杂,运维比较麻烦

    微服务需要解决的问题(微服务的5个核心组件):

    • 注册中心:解决的是服务治理问题,即 服务地址的管理。Nacos

    • 远程调用:解决服务之间的请求调用问题。RestTemplate(今天临时用),Feign

    • 服务保护:防止服务之间出现级联问题导致雪崩。Hystrix,Sentinel

    • 配置中心:解决的是每个服务都有配置文件,配置文件散乱不方便管理的问题。Nacos

    • 服务网关:解决的是 要给客户端提供一个统一的访问入口。SpringCloudGateway

二、远程调用RestTemplate【理解】

本章节学习目标:

  • 理解服务拆分的原则
  • 使用RestTemplate实现远程调用

1. 服务拆分

1 服务拆分原则

这里我们总结了微服务拆分时的几个原则:

  • 职责单一:不同微服务,不要重复开发相同业务

  • 服务自治:微服务数据独立,不要访问其它微服务的数据库

  • 面向服务:微服务可以将自己的业务暴露为接口,供其它微服务调用:有Controller即可

2 服务拆分示例

要求:

  • 有用户服务,提供“根据id查询用户”功能

  • 有订单服务,提供“根据id查询订单”功能

准备:

  • 创建数据库cloud_user,执行脚本《cloud-user.sql》

  • 创建数据库cloud_order,执行脚本《cloud-order.sql》

项目结构:

1) 创建父工程

注意:在开发项目时,不要随意动依赖坐标。一旦依赖出现问题,就可能导致整个项目出问题

注意:每个服务命名时,以英文字母开头,单词中间用横杠连接。不建议用下划线_连接

注意:每个服务的配置文件里,都必须有应用名称spring.application.name

  1. 删除src文件夹

  2. 修改pom.xml导入依赖

  1. <packaging>pom</packaging>
  2. <parent>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-parent</artifactId>
  5. <version>2.3.9.RELEASE</version>
  6. <relativePath/>
  7. </parent>
  8. <properties>
  9. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  10. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  11. <mysql.version>8.0.31</mysql.version>
  12. <mybatisplus.version>3.4.1</mybatisplus.version>
  13. </properties>
  14. <dependencyManagement>
  15. <dependencies>
  16. <!-- mysql驱动 -->
  17. <dependency>
  18. <groupId>mysql</groupId>
  19. <artifactId>mysql-connector-java</artifactId>
  20. <version>${mysql.version}</version>
  21. </dependency>
  22. <!-- MybatisPlus -->
  23. <dependency>
  24. <groupId>com.baomidou</groupId>
  25. <artifactId>mybatis-plus-boot-starter</artifactId>
  26. <version>${mybatisplus.version}</version>
  27. </dependency>
  28. </dependencies>
  29. </dependencyManagement>
  30. <dependencies>
  31. <dependency>
  32. <groupId>org.projectlombok</groupId>
  33. <artifactId>lombok</artifactId>
  34. </dependency>
  35. </dependencies>
2) 准备用户服务
1. 用户服务的基础代码

1) 创建用户模块

在项目上创建Module:user-service

2) 导入依赖

修改pom.xml,添加依赖坐标

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>mysql</groupId>
  8. <artifactId>mysql-connector-java</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>com.baomidou</groupId>
  12. <artifactId>mybatis-plus-boot-starter</artifactId>
  13. </dependency>
  14. </dependencies>

3) 准备配置文件

创建配置文件application.yaml

server:
  port: 8080 #8080端口
spring:
  application:
    name: user-service #应用名称
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///cloud_user
    username: root
    password: root    
logging:
  level:
    com.itheima.user: debug
  pattern:
    dateformat: HH:mm:ss.SSS

4) 准备引导类

  1. package com.itheima.user;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. @SpringBootApplication
  6. @MapperScan("com.itheima.user.mapper")
  7. public class UserApplication {
  8. public static void main(String[] args) {
  9. SpringApplication.run(UserApplication.class, args);
  10. }
  11. }

准备三层:

  1. //2. 准备实体类User
  2. package com.itheima.user.pojo;
  3. import com.baomidou.mybatisplus.annotation.TableName;
  4. import lombok.Data;
  5. @Data
  6. @TableName("tb_user")
  7. public class User {
  8. private Long id;
  9. private String username;
  10. private String address;
  11. }
  12. -------------
  13. //3. 创建UserMapper
  14. package com.itheima.user.mapper;
  15. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  16. import com.itheima.user.pojo.User;
  17. public interface UserMapper extends BaseMapper<User> {
  18. }
  19. ------------
  20. //4. 创建UserService
  21. package com.itheima.user.service;
  22. import com.itheima.user.mapper.UserMapper;
  23. import com.itheima.user.pojo.User;
  24. import org.springframework.beans.factory.annotation.Autowired;
  25. import org.springframework.stereotype.Service;
  26. @Service
  27. public class UserService {
  28. @Autowired
  29. private UserMapper userMapper;
  30. public User findById(Long id){
  31. return userMapper.selectById(id);
  32. }
  33. }
  34. -----------
  35. //5. 创建UserController
  36. package com.itheima.user.controller;
  37. import com.itheima.user.pojo.User;
  38. import com.itheima.user.service.UserService;
  39. import org.springframework.beans.factory.annotation.Autowired;
  40. import org.springframework.web.bind.annotation.GetMapping;
  41. import org.springframework.web.bind.annotation.PathVariable;
  42. import org.springframework.web.bind.annotation.RequestMapping;
  43. import org.springframework.web.bind.annotation.RestController;
  44. @RestController
  45. @RequestMapping("/user")
  46. public class UserController {
  47. @Autowired
  48. private UserService userService;
  49. @GetMapping("/{id}")
  50. public User findById(@PathVariable("id") Long id) {
  51. return userService.findById(id);
  52. }
  53. }
2. 启动测试
3) 准备订单服务
1. 订单服务的基础代码

1) 创建订单模块

在项目上右键创建Module:order-service

2) 导入依赖

修改pom.xml,添加依赖坐标

  1. <dependencies>
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-web</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>mysql</groupId>
  8. <artifactId>mysql-connector-java</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>com.baomidou</groupId>
  12. <artifactId>mybatis-plus-boot-starter</artifactId>
  13. </dependency>
  14. </dependencies>

3) 配置文件

创建application.yaml

server:
  port: 7070
spring:
  application:
    name: order-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///cloud_order
    username: root
    password: root
logging:
  level:
    com.itheima.order: debug
  pattern:
    dateformat: MM-dd HH:mm:ss.SSS

4) 创建引导类

  1. package com.itheima.order;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. @SpringBootApplication
  6. @MapperScan("com.itheima.order.mapper")
  7. public class OrderApplication {
  8. public static void main(String[] args) {
  9. SpringApplication.run(OrderApplication.class, args);
  10. }
  11. }

准备三层:

  1. //2. 创建实体类Order
  2. //把User类拷贝进来,稍后会用到
  3. //创建Order类
  4. package com.itheima.order.pojo;
  5. import com.baomidou.mybatisplus.annotation.TableField;
  6. import com.baomidou.mybatisplus.annotation.TableName;
  7. import lombok.Data;
  8. @Data
  9. @TableName("tb_order")
  10. public class Order {
  11. private Long id;
  12. private Long userId;
  13. private String name;
  14. private Long price;
  15. private Integer num;
  16. @TableField(exist = false)
  17. private User user;
  18. }
  19. ------------
  20. //3. 创建OrderMapper
  21. package com.itheima.order.mapper;
  22. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  23. import com.itheima.order.pojo.Order;
  24. public interface OrderMapper extends BaseMapper<Order> {
  25. }
  26. -----------
  27. //4. 创建OrderService
  28. package com.itheima.order.service;
  29. import com.itheima.order.mapper.OrderMapper;
  30. import com.itheima.order.pojo.Order;
  31. import org.springframework.beans.factory.annotation.Autowired;
  32. import org.springframework.stereotype.Service;
  33. @Service
  34. public class OrderService {
  35. @Autowired
  36. private OrderMapper orderMapper;
  37. public Order findById(Long id){
  38. Order order = orderMapper.selectById(id);
  39. return order;
  40. }
  41. }
  42. -----------
  43. //5. 创建OrderController
  44. package com.itheima.order.controller;
  45. import com.itheima.order.pojo.Order;
  46. import com.itheima.order.service.OrderService;
  47. import org.springframework.beans.factory.annotation.Autowired;
  48. import org.springframework.web.bind.annotation.GetMapping;
  49. import org.springframework.web.bind.annotation.PathVariable;
  50. import org.springframework.web.bind.annotation.RequestMapping;
  51. import org.springframework.web.bind.annotation.RestController;
  52. @RestController
  53. @RequestMapping("/order")
  54. public class OrderController {
  55. @Autowired
  56. private OrderService orderService;
  57. @GetMapping("/{id}")
  58. public Order findById(@PathVariable("id") Long id) {
  59. return orderService.findById(id);
  60. }
  61. }
2. 启动测试

2. 远程调用RestTemplate【了解】

1 实现远程调用

说明

问题:

  • 在order-service中,查询一个订单OrderOrder中的user对象为空

  • 在user-service中,提供了根据id查询用户的功能

要求:

  • 在查询订单时,把订单关联的用户信息一并查询出来

方案:

  • 使用SpringMVC提供的RestTemplate,可以发起HTTP请求,调用http://localhost:8080/user/{id}对应的接口

  • 具体步骤是:

    • 在order-service里注册一个RestTemplate对象

    • 在OrderService的findById方法中

      • 查询得到订单之后,再根据订单中的userId,使用RestTemplate向http://localhost:8080/user/{id}发请求,查询对应的User对象

      • 把得到的User对象放到Order里

实现

1. 修改OrderApplication

在引导类中注册一个RestTemplate对象

  1. package com.itheima.order;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.web.client.RestTemplate;
  7. @SpringBootApplication
  8. @MapperScan("com.itheima.order.mapper")
  9. public class OrderApplication {
  10. public static void main(String[] args) {
  11. SpringApplication.run(OrderApplication.class, args);
  12. }
  13. @Bean
  14. public RestTemplate restTemplate(){
  15. return new RestTemplate();
  16. }
  17. }

2. 修改OrderService

  • 使用RestTemplate发HTTP请求,查询对应的用户

  1. package com.itheima.order.service;
  2. import com.itheima.order.mapper.OrderMapper;
  3. import com.itheima.order.pojo.Order;
  4. import com.itheima.order.pojo.User;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.cloud.client.ServiceInstance;
  7. import org.springframework.cloud.client.discovery.DiscoveryClient;
  8. import org.springframework.stereotype.Service;
  9. import org.springframework.web.client.RestTemplate;
  10. import java.util.List;
  11. @Service
  12. public class OrderService {
  13. @Autowired
  14. private OrderMapper orderMapper;
  15. @Autowired
  16. private RestTemplate restTemplate;
  17. public Order findById(Long id) {
  18. Order order = orderMapper.findById(id);
  19. //拼接url地址
  20. String url = "http://localhost:8080/user/" + order.getUserId();
  21. //使用RestTemplate向这个地址发请求,查询用户
  22. User user = restTemplate.getForObject(url, User.class);
  23. //把查询的结果设置到order对象里
  24. order.setUser(user);
  25. return order;
  26. }
  27. }
测试

2 提供者与消费者

在服务调用关系中,会有两个不同的角色:

  • 服务提供者:一次业务中,被其它微服务调用的服务。

  • 服务消费者:一次业务中,调用其它微服务的服务。

但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。

如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?

  • 对于A调用B的业务而言:A是服务消费者,B是服务提供者

  • 对于B调用C的业务而言:B是服务消费者,C是服务提供者

因此,服务B既可以是服务提供者,也可以是服务消费者。

3. 小结

如果拉取nacos的地址

  1. package com.itheima.order.service;
  2. import com.itheima.order.mapper.OrderMapper;
  3. import com.itheima.order.pojo.Order;
  4. import com.itheima.order.pojo.User;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Service;
  7. import org.springframework.web.client.RestTemplate;
  8. @Service
  9. public class OrderService {
  10. @Autowired
  11. private OrderMapper orderMapper;
  12. @Autowired
  13. private RestTemplate restTemplate;
  14. public Order findById(Long id){
  15. Order order = orderMapper.selectById(id);
  16. // String url = "http://localhost:8080/user/"+order.getUserId();
  17. //访问路径的写法: http://服务名/资源路径
  18. String url = "http://user-service/user/"+order.getUserId();
  19. //使用RestTemplate直接发起请求:RestTemplate已经具备负载均衡能力了
  20. User user = restTemplate.getForObject(url, User.class);
  21. order.setUser(user);
  22. return order;
  23. }
  24. }

远程调用:RestTemplate技术,简单了解,体验微服务架构下的功能实现

服务提供者:提供服务、供其它人调用的一方

服务消费者:使用服务、调用其它服务的一方

三、注册中心Nacos【重点】

  • 能安装启动Nacos
  • 能够使用Nacos作为注册中心

1. 介绍

1 服务治理问题

目前已经可以实现微服务之间的调用,但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:

  • 一旦服务提供者地址变化,就需要手工修改代码

  • 一旦服务变得越来越多,人工维护调用关系困难

这时候就需要通过注册中心动态的实现服务治理

服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现

  • 服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。

  • 服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问。

2 Nacos简介

国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。

Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。

3 Nacos安装

  1. 下载:https://github.com/alibaba/nacos/releases,下载zip格式的程序包。

    可以直接使用资料里提供的程序包

  2. 安装:

    免安装,直接解压到一个不含中文、空格、特殊字符的目录里

  3. 启动:

    使用cmd切换到nacos的bin目录里

    执行命令:startup.cmd -m standalone,以单机模式启动nacos

  4. 进入管理界面

    打开浏览器输入地址 http://localhost:8848/nacos

    默认帐号:nacos,密码:nacos

2. Nacos使用入门

Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。

主要差异在于:

  • 依赖坐标不同

  • 配置参数不同

1 添加坐标

1) 父工程锁定SpringCloudAlibaba的依赖版本

在父工程pom.xml的dependencyManagement中添加SpringCloudAlibaba的版本锁定

  1. <!--SpringCloudAlibaba-->
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  5. <version>2.2.6.RELEASE</version>
  6. <type>pom</type>
  7. <scope>import</scope>
  8. </dependency>

父工程最终的坐标如下:

  1. <modules>
  2. <module>user-service</module>
  3. <module>order-service</module>
  4. </modules>
  5. <packaging>pom</packaging>
  6. <parent>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-parent</artifactId>
  9. <version>.RELEASE</version>
  10. <relativePath/>
  11. </parent>
  12. <properties>
  13. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  14. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  15. <java.version>1.8</java.version>
  16. <maven.compiler.source>8</maven.compiler.source>
  17. <maven.compiler.target>8</maven.compiler.target>
  18. <mysql.version>5.1.47</mysql.version>
  19. <mybatisplus.version>3.4.1</mybatisplus.version>
  20. </properties>
  21. <dependencyManagement>
  22. <dependencies>
  23. <!--SpringCloudAlibaba-->
  24. <dependency>
  25. <groupId>com.alibaba.cloud</groupId>
  26. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  27. <version>2.2.6.RELEASE</version>
  28. <type>pom</type>
  29. <scope>import</scope>
  30. </dependency>
  31. <!-- springCloud -->
  32. <dependency>
  33. <groupId>org.springframework.cloud</groupId>
  34. <artifactId>spring-cloud-dependencies</artifactId>
  35. <version>Hoxton.SR10</version>
  36. <type>pom</type>
  37. <scope>import</scope>
  38. </dependency>
  39. <!-- mysql驱动 -->
  40. <dependency>
  41. <groupId>mysql</groupId>
  42. <artifactId>mysql-connector-java</artifactId>
  43. <version>${mysql.version}</version>
  44. </dependency>
  45. <dependency>
  46. <groupId>com.baomidou</groupId>
  47. <artifactId>mybatis-plus-boot-starter</artifactId>
  48. <version>${mybatisplus.version}</version>
  49. </dependency>
  50. </dependencies>
  51. </dependencyManagement>
  52. <dependencies>
  53. <dependency>
  54. <groupId>org.projectlombok</groupId>
  55. <artifactId>lombok</artifactId>
  56. </dependency>
  57. </dependencies>

2) 微服务添加nacos的服务发现依赖坐标

在用户服务和订单服务中添加nacos的服务发现包的依赖坐标:

  1. <dependency>
  2. <groupId>com.alibaba.cloud</groupId>
  3. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  4. </dependency>

最终依赖如下:

  1. <dependencies>
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.boot</groupId>
  8. <artifactId>spring-boot-starter-web</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>mysql</groupId>
  12. <artifactId>mysql-connector-java</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>com.baomidou</groupId>
  16. <artifactId>mybatis-plus-boot-starter</artifactId>
  17. </dependency>
  18. </dependencies>

2 配置注册中心地址

修改微服务的application.yaml,配置nacos的地址

spring:
  application:
    name: order-service #应用名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

用户服务的最终配置文件

server:
  port: 8080 #8080端口
spring:
  application:
    name: user-service #应用名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///cloud_user?useSSL=false
    username: root
    password: root
logging:
  level:
    com.itheima.user: debug
  pattern:
    dateformat: HH:mm:ss.SSS

订单服务的最终配置文件

server:
  port: 7070
spring:
  application:
    name: order-service #应用名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///cloud_order?useSSL=false
    username: root
    password: root
logging:
  level:
    com.itheima.user: debug
  pattern:
    dateformat: HH:mm:ss.SSS
mybatis:
  configuration:
    map-underscore-to-camel-case: true

3 开启服务发现功能

修改所有微服务的引导类,在引导类上添加注解@EnableDiscoveryClient

用户服务最终的引导类

  1. package com.itheima.user;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  6. @EnableDiscoveryClient
  7. @SpringBootApplication
  8. @MapperScan("com.itheima.user.mapper")
  9. public class UserApplication {
  10. public static void main(String[] args) {
  11. SpringApplication.run(UserApplication.class, args);
  12. }
  13. }

订单服务最终的引导类

  1. package com.itheima.order;
  2. import org.mybatis.spring.annotation.MapperScan;
  3. import org.springframework.boot.SpringApplication;
  4. import org.springframework.boot.autoconfigure.SpringBootApplication;
  5. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
  6. import org.springframework.cloud.client.loadbalancer.LoadBalanced;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.web.client.RestTemplate;
  9. @EnableDiscoveryClient
  10. @SpringBootApplication
  11. @MapperScan("com.itheima.order.mapper")
  12. public class OrderApplication {
  13. public static void main(String[] args) {
  14. SpringApplication.run(OrderApplication.class, args);
  15. }
  16. @Bean
  17. public RestTemplate restTemplate(){
  18. return new RestTemplate();
  19. }
  20. }

4 功能测试

验证服务是否注册到Nacos

  1. 先启动Nacos,再启动用户服务和订单服务

  2. 打开Nacos控制台,访问http://localhost:8848/nacos,查看注册的服务信息

验证远程调用是否成功

  1. 修改Order服务的引导类,给RestTemplate对象添加 @LoadBalanced

  1. @Bean
  2. @LoadBalanced
  3. public RestTemplate restTemplate(){
  4. return new RestTemplate();
  5. }

  2. 修改OrderService类,根据服务名进行远程调用

  1. @Service
  2. public class OrderService {
  3. @Autowired
  4. private OrderMapper orderMapper;
  5. @Autowired
  6. private RestTemplate restTemplate;
  7. public Order findById(Long id){
  8. Order order = orderMapper.selectById(id);
  9. //使用RestTemplate,向名称为user-service的服务发起远程调用,得到结果
  10. User user = restTemplate.getForObject("http://user-service/user/" + order.getUserId(), User.class);
  11. order.setUser(user);
  12. return order;
  13. }
  14. }

  3. 打开浏览器,访问http://localhost:7070/order/101

3. Nacos的原理

1 临时实例与非临时实例

从Nacos1.0开始就提供了一个配置参数:spring.cloud.nacos.discovery.ephemeral,用于设置微服务实例是临时实例还是非临时实例

  • 值为true:微服务是临时实例。当微服务长时间不能向Nacos续约时,Nacos会剔除微服务实例的信息

  • 值为false:微服务是非临时实例。即使微服务长时间不能向Nacos续约,Nacos也仅仅是将服务实例标记为不健康状态,而不会剔除服务实例的信息

两者的作用是:

  • 临时实例:适用于应对流量突增的情况,当流量洪峰时,增加服务实例数量;当流量高峰过去以后,服务实例停止,就自动从Nacos里剔除了

  • 非临时实例:适用于服务的保护。当Nacos发现短时间有大量微服务不健康时,实际可能是Nacos的网络波动等情况,而微服务实例其实是正常的。 这时Nacos仍然保留服务信息,供消费者进行拉取

2 Nacos的原理

4. 常见错误

启动微服务时,可能会报以下错误:

出错的原因,通常是Nacos缓存问题:

  • 在使用Nacos的时候,每次有服务注册到Nacos,Nacos会把服务的信息缓存起来。缓存的数据在Nacos软件的data文件夹里

  • 下次再使用Nacos的时候,如果 之前注册服务的ip地址 和 这一次注册服务的ip地址不同,就会报这个错

解决的方法是:

  • 清理掉Nacos里的data文件夹,再启动Nacos,启动微服务。 就没有缓存了,就不会报这个错了

5. 小结

注册中心是什么,有什么用?解决了服务治理问题

  • 维护所有活跃的服务地址列表

  • 监控所有服务的健康状态

注册中心的模式:

  • 服务注册:每个微服务启动时,会把自己的地址信息上报给注册中心;

    然后注册中心会监控服务的健康状态:通过心跳续约的方式,或者通过主动探测模式

  • 服务发现:当微服务需要远程调用其它服务时,从注册中心里拉取地址列表信息,再发起远程调用

注册中心的技术:

  • Eureka:NetFlix网飞的注册中心技术。功能比较简单,管理界面也比较简陋

  • Nacos:Alibaba的注册中心技术。功能丰富强大,有管理界面可以进行服务的管理

使用Nacos作为注册中心:

  1. 安装启动Nacos:startup.cmd -m standalone ,或者用我们提供的cmd脚本

  2. 每个微服务需要整合Nacos

    添加依赖:先添加SpringCloudAlibaba的依赖版本锁定,再给每个微服务添加nacos-discovery依赖坐标

    修改配置:修改每个微服务的application.yaml,其中要有

    • spring.application.name, 配置服务名

    • spring.cloud.nacos.discovery.server-addr,配置Nacos的地址,写成 localhost:8848

    修改引导类:修改每个微服务的引导类,添加@EnableDiscoveryClient

  3. 启动微服务,微服务会自动把自己的地址信息上报给Nacos。

    打开Nacos管理界面 http://localhost:8848/nacos,帐号:nacos,密码:nacos

    找到服务管理,可以查看已注册的服务信息列表

Nacos的原理了解:

  • Nacos的服务实例分为两种:临时实例,非临时实例

    临时实例,当服务不健康时,Nacos会剔除服务信息

    非临时实例, 当服务不健康时,Nacos仅仅是标记为不健康状态,并不会剔除服务信息

  • 服务注册:微服务一启动,就会立即自动注册到Nacos里

  • 服务健康状态的监控:

    如果是临时实例,需要由微服务每5秒一次向Nacos发送心跳续约;如果15秒没有心跳,服务信息会被Nacos标记为不健康;30秒以后会把服务信息剔除掉

    如果是非临时实例,Nacos会每20秒主动探测一次微服务的状态。如果不正常,也仅仅是标记不健康,不会剔除信息

四、负载均衡Ribbon

本章节学习目标:

  • 理解负载均衡的概念与分类
  • 能够使用ribbon实现负载均衡

1. 负载均衡简介

1 什么是负载均衡

通俗的讲,负载均衡就是将负载(工作任务,访问请求)分摊到多个操作单元上进行执行。

根据负载均衡发生位置的不同,一般分为服务端负载均衡客户端负载均衡

  • 服务端(提供者)负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。

  • 客户端(消费者)负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求。

我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。

2 Ribbon负载均衡

Ribbon是NetFlix提供的客户端负载均衡工具,它有助于控制HTTP和TCP客户端的行为。

Ribbon提供了多种负载均衡算法,包括轮询、随机等等,同时也支持自定义负载均衡算法。

它不需要独立部署,而是几乎存在于SpringCloud的每个组件中,包括Nacos也已经引入了Ribbon。

  • 当使用RestTemplate发起远程调用时,需要给RestTemplate对象添加@LoadBalanced注解

  • 当使用OpenFeign发起远程调用时,不需要做任何额外配置,就具备负载均衡效果

2. Ribbon效果演示

在刚刚的案例中,order-service要调用user-service,而user-service只有一个服务实例。但是实际环境中为了保证高可用,通常会搭建集群环境。比如user-service搭建了集群,这就需要实现对用户服务的负载均衡效果

我们以订单中调用用户服务为例,来演示负载均衡的问题

基础服务代码:

  • 注册中心:Nacos

  • 用户服务:提供findById功能

  • 订单服务:提供findById功能,并通过RestTemplate远程调用 用户服务。

1 搭建用户服务集群

1) 复制用户服务的配置文件

复制用户服务的配置文件,准备多个不同后缀名的配置环境application-环境标识.yaml

application-8080serv.yaml:设置为8080端口

server:
  port: 8080
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848    
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///cloud_user?useSSL=false
    username: root
    password: root
logging:
  level:
    com.itheima.user: debug
  pattern:
    dateformat: HH:mm:ss.SSS

application-8081serv.yaml:设置为8081端口

server:
  port: 8081
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848    
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///cloud_user?useSSL=false
    username: root
    password: root
    
logging:
  level:
    com.itheima.user: debug
  pattern:
    dateformat: HH:mm:ss.SSS

2) 设置启动链接

设置步骤略,参考前边euraka集群的配置

给用户服务准备多个启动链接,一个激活8080serv配置文件,一个激活8081serv配置文件

3) 启动两个用户服务

  1. 启动两个用户服务,启动后在nacos界面上可看到用户服务有两个实例

  2. 再启动订单服务

2 订单服务调用用户服务

在浏览器上访问http://localhost:7070/order/101,多次刷新访问,发现多个用户服务都被调用到了,而且是轮询的方式。

3. Ribbon实现原理分析

@LoadBalanced注解

注解的文档中说明了:如果使用@LoadBalanced标记一个RestTemplate或者WebClient对象,表示将会配置使用一个LoadBanalcerClient对象

LoadBalancerClient源码

LoadBalancerClient有一个子类RibbonLoadBalancerClient,它使用Ribbon实现了负载均衡调用

IRule源码

在上一步的getServer方法代码如下:

继续跟进去,发现是由rule对象挑选了一个目标服务地址

这个IRule是什么呢

4. Ribbon负载均衡策略

1) 负载均衡策略介绍

IRule是负载均衡策略的顶级接口,它的每个实现类就是一个我们可以选择使用的负载均衡策略

Ribbon内置了多种负载均衡策略,这些策略的顶级接口是com.netflix.loadbalancer.IRule,它的常用实现有:(可参考上一章节里的IRule类图)

2) 修改负载均衡策略

配置文件方式

我们可以通过修改配置文件来调用Ribbon的负载均衡策略,只要修改调用者的配置文件即可:

服务名:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

例如,在订单服务的application.yaml里添加如下配置:

#调用用户服务时,使用指定的负载均衡策略
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

代码@Bean方式

我们修改订单服务的配置类(或引导类),添加:

  1. @Bean
  2. public IRule randomRule(){
  3. return new RandomRule();
  4. }

注意事项

  1. 使用配置文件方式,只能设置 调用某一服务时指定负载均衡策略,而不是全局的策略;

    使用@Bean方式,配置的是全局的负载均衡策略

  2. 实际开发中,通常使用默认的负载均衡策略,不需要修改

5. 饥饿加载

在我们的示例代码里,订单服务里必须要先拉取到用户服务的地址列表,创建LoadBanalcerClient对象,然后才可以发起远程调用。而Ribbon有两种策略:

  • 懒加载:即第一次访问目标服务时,才会去创建LoadBalanceClient对象,会导致第一次请求时间比较长

  • 饥饿加载:当服务启动时,就立即拉取服务列表,并创建好LoadBalancerClient对象;这样的话,即使第一次远程调用时也不会花费很长时间

Ribbon默认采用懒加载方式,如果我们需要修改成饥饿加载的话,以订单服务为例,可以在订单服务的配置文件里增加如下配置:

ribbon:
  eager-load:
    enabled: true #开启饥饿加载
    clients: #要饥饿加载的服务列表
      - user-service

6. 小结

负载均衡有两种方式:

  • 提供者一方实现负载均衡:nginx。调用者是无感知的,适用于 整个服务端的最前端,面向客户端请求时的负载均衡

  • 消费者一方实现负载均衡:ribbon。调用者负责实现负载均衡的效果,适用于微服务之间互相调用时

Ribbon负载均衡:NetFlix提供的,已经被SpringCloud里大量组件内置了。

  • 使用Ribbon,不需要额外添加依赖,也不需要额外的配置,默认就有负载均衡效果

  • 默认的负载均衡策略是:轮询

Ribbon支持的负载均衡策略,常见的:

  • RoundRobinRule:轮询策略

  • RandomRule:随机策略

  • RetryRule:重试策略

如果要调整负载均衡策略:在消费者一方进行设置

在配置文件里设置

目标服务名:
 ribbon:
  NFLoadBalancerRuleClassName: 负载均衡策略的全限定类名

在代码里使用@Bean设置:在配置类或者在引导类里添加

@Bean
public IRule rule(){
    return new 负载均衡策略类名();
}

Ribbon的饥饿加载:

  • 如果没有开启饥饿加载,默认情况下:Ribbon在第一次发起远程调用时,从Nacos里拉取地址列表。通常第一次访问耗时比较长

  • 可以开启饥饿加载:在消费者一方开启,修改配置文件

ribbon:
  eager-load:
      enabled: true #开启饥饿加载
      clients: #在服务启动时,立即加载哪些服务的地址列表
        - 目标服务名
        - 目标服务名

五、Nacos分组存储与环境隔离

  • 理解Nacos的分级存储
  • 理解Nacos的环境隔离

1. Nacos分级存储模型

1 配置实例集群

1 Nacos里实例集群的概念

一个服务可以有多个实例,例如我们的user-service,可以有很多实例,例如:

  • 127.0.0.1:8081

  • 127.0.0.1:8082

  • 127.0.0.1:8083

在大型项目里,为了满足异地容灾的需要,通常将这些实例部署在全国各地的不同机房,Nacos将同一机房内的实例称为一个集群cluster。例如:

  • 127.0.0.1:8081,在上海机房

  • 127.0.0.1:8082,在上海机房

  • 127.0.0.1:8083,在杭州机房

也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。

当本集群内不可用时,才访问其它集群。例如:杭州机房内的order-service应该优先访问同机房的user-service。

2 配置实例集群

配置语法:只要修改配置文件,增加如下设置

spring:
  cloud:
    nacos:
      discovery:
        cluster-name: HZ #设置当前服务实例所属集群名称为HZ

1) 准备配置文件

我们以用户服务为例,启动多个用户服务实例,分别为:

  • localhost:8081,属于BJ集群

  • localhost:8082,属于BJ集群

  • localhost:8083,属于HZ集群

2) 准备启动链接

创建三个启动链接,分别激活这三个配置文件

创建步骤,略

3 查看配置效果

打开Nacos的控制台,找到user-service服务,查看详情

查看服务实例列表

2 同集群优先访问

在划分了实例集群之后,我们期望集群内的服务优先调用集群内部的服务实例,这样会有更高的响应速度。而默认的负载均衡策略并不能实现这种效果,因此Nacos提供了一个新的负载均衡策略:NacosRule

1 配置负载均衡策略

我们以订单服务为例,假如订单服务属于BJ集群,那么它最好优先调用BJ集群内的用户服务。而实现的方式是:

  1. 给订单服务设置集群:BJ

  2. 给订单服务设置负载均衡策略:NacosRule

最终配置如下:

server:
  port: 7070
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        cluster-name: BJ #订单服务属于BJ集群
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///cloud_order?useSSL=false
    username: root
    password: root
logging:
  level:
    com.itheima.user: debug
  pattern:
    dateformat: HH:mm:ss.SSS
mybatis:
  configuration:
    map-underscore-to-camel-case: true
user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则

2 测试效果

1) 同集群调用

  1. 启动所有用户服务和订单服务

  2. 打开浏览器多次访问http://localhost:7070/order/101,发现只有8081和8082的用户服务被访问了

2) 跨集群调用

  1. 关闭8081和8082用户服务

  2. 稍等一会,再访问http://localhost:7070/order/101,发现8083服务被调用到了,但是idea报了一个警告:A cross-cluster call occurs,意思是 出现了一次跨集群调用

11:57:47.731  WARN 5024 --- [nio-7070-exec-8] c.alibaba.cloud.nacos.ribbon.NacosRule   : A cross-cluster call occurs,name = user-service, clusterName = BJ, instance = [Instance{instanceId='192.168.1.107#8083#HZ#DEFAULT_GROUP@@user-service', ip='192.168.1.107', port=8083, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='HZ', serviceName='DEFAULT_GROUP@@user-service', metadata={preserved.register.source=SPRING_CLOUD}}]

3 Nacos的服务实例权重

实际部署中,服务器设备性能往往是有差异的,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。

但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。

1 设置服务实例的权重

在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:

2 测试效果
  1. 启动所有用户服务和订单服务

  2. 打开浏览器访问http://localhost:7070/order/101,发现8081被访问到的频率更高

2. namespace环境隔离

一个项目的部署运行,通常需要有多个环境,例如:开发环境、测试环境、生产环境。不同的环境之间的配置不同、部署的代码不同,应当是相互隔离的。

比如:项目已经部署到生产环境、正式运行起来了了,后来开发了新的功能,要部署到测试环境进行测试。那么测试环境的微服务,一定要调用测试环境的目标服务,而不能调用到生产环境上的服务。

Nacos提供了namespace来实现环境隔离功能

  • nacos中可以有多个namespace,namespace下可以有group、service等

  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

1 创建namespace

  1. 打开“命名空间”管理界面,点击“创建命名空间”

2.填写空间的id、名称和描述,点击确定

2 给微服务指定namespace

目前我们的所有微服务都没有指定namespace,所以默认使用的都是public名称空间。

现在我们把order-service指定名称空间为dev,和user-service不在同一命名空间内。那么订单服务和用户服务之间就是隔离的,不能调用。

  • 用户服务user-service:不指定名称空间,还使用默认的public命名空间

  • 订单服务order-service:修改配置文件,指定名称空间为dev

然后重启订单服务

3 隔离效果

1) 查看nacos控制台

打开Nacos控制台,可以看到

用户服务在public命名空间下

订单服务在dev命名空间下

2) 隔离效果测试

打开浏览器访问http://localhost:7070/order/101,发现报错了,服务不可用

3. 小结

Nacos的分级存储模型:服务-->多个集群-->多个服务实例

  • 把多个服务实例,划分到不同的集群里。比如:一批服务实例属于BJ集群,一批服务实例属于SH集群

  • 把BJ集群的所有服务全部部署到北京机房;把SH集群的所有服务全部部署到上海机房

好处:

  • 实现灾备。比如北京机房出现问题,还有上海机房可以提供服务

  • 实现同集群优先访问。访问了北京机房的订单服务, 订单服务调用同机房(同集群)的用户服务

给微服务设置所属的集群:

  • 修改微服务的配置文件,设置 spring.cloud.nacos.discovery.cluster-name=集群名称

给微服务设置负载均衡策略:同集群优先访问:

  • 修改消费者的配置文件,设置负载均衡策略为 NacosRule

给微服务设置权重值:

  • 在Nacos里直接设置权重值,不需要重启服务、不需要修改配置文件。

  • 权重值越大,被访问到的机率越高

多个服务实例之间想要实现绝对的隔离:使用Nacos的namespace

  • 相同namespace的服务实例之间,是可以互相发现、互相调用的

  • 不同namespace的服务实例之间,是绝对隔离的,不可能互相发现、更不可能互相调用

  • 做法:

    1. 在Nacos里创建namespace命名空间。需要设置命名空间的id、名称、描述

    2. 修改微服务的配置文件,添加参数 spring.cloud.nacos.discovery.namespace=命名空间的id

六、进程和线程的区别

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/501877
推荐阅读
相关标签
  

闽ICP备14008679号