SpringCloud

SpringCloud

1、分布式概念

了解分布式和微服务

简单来说,微服务架构风格是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。这些服务围绕业务能力构建并且可通过全自动部署机制独立部署。这些服务共用一个最小型的集中式的管理,服务可用不同的语言开发,使用不同的数据存储技术。

http://blog.cuicc.com/blog/2015/07/22/microservices/

简单的说,微服务是架构设计方式,分布式是系统部署方式,两者概念不同

分布式是啥?

分布式服务顾名思义服务是分散部署在不同的机器上的,一个服务可能负责几个功能,是一种面向SOA架构的,服务之间也是通过 RPC 来交互或者是 webservice 来交互的

逻辑架构设计完后就该做物理架构设计,系统应用部署在超过一台服务器或虚拟机上,且各分开部署的部分彼此通过各种通讯协议交互信息,就可算作分布式部署,生产环境下的微服务肯定是分布式部署的,分布式部署的应用不一定是微服务架构的,比如集群部署,它是把相同应用复制到不同服务器上,但是逻辑功能上还是单体应用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KBZCq8pX-1654935432431)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220410111710679.png)]

微服务是啥?

简单来说微服务就是很小的服务,小到一个服务只对应一个单一的功能,只做一件事

这个服务可以单独部署运行,服务之间可以通过 RPC 来相互交互,每个微服务都是由独立的小团队开发,测试,部署,上线,负责它的整个生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m9r6yT46-1654935432433)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220410111724586.png)]

微服务架构又是啥?

在做架构设计的时候,先做逻辑架构,再做物理架构,当你拿到需求后,估算过最大用户量和并发量后,计算单个应用服务器能否满足需求

如果用户量只有几百人的小应用,单体应用就能搞定,即所有应用部署在一个应用服务器里

如果是很大用户量,且某些功能会被频繁访问,或者某些功能计算量很大,建议将应用拆解为多个子系统,各自负责各自功能,这就是微服务架构

总结

微服务相比分布式服务来说,它的粒度更小,服务之间耦合度更低,由于每个微服务都由独立的小团队负责,因此它敏捷性更高,分布式服务最后都会向微服务架构演化,这是一种趋势, 不过服务微服务化后带来的挑战也是显而易见的,例如服务粒度小,数量大,后期运维将会很难

微服务架构选型

选型依据:

  • 整体解决方案和框架成熟度
  • 社区热度
  • 可维护性
  • 学习曲线

主流微服务框架:

  • 阿里巴巴Dubbo/HSF
  • 京东JSF
  • 新浪微博Motan
  • 当当网Dubbox

微服务框架对比:

SpringCloud概述

分布式微服务架构下的一站式解决方案,是各个微服务架构落地技术的集合体,俗称微服务全家桶

相关资源:

  • 官网地址:https://spring.io/projects/spring-cloud
  • 中文地址:https://springcloud.cc/
  • 中文社区:http://springcloud.cn/

SpringBoot 和 SpringCloud 有啥关系?

  • SpringBoot专注于快速方便的开发单个个体微服务。
  • SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
  • SpringBoot可以离开SpringCloud独立使用开发项目,但是SpringCloud离不开SpringBoot,属于依赖的关系
  • SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架

版本说明

  • 因为 Spring Cloud 不同其他独立项目,它拥有很多子项目的大项目。所以它是的版本是 版本名+版本号 (如Angel.SR6)
  • 版本名:是伦敦的地铁名
  • 版本号:SR(Service Releases)是固定的 ,大概意思是稳定版本。后面会有一个递增的数字。
  • 所以 Brixton.SR5 就是Brixton的第5个Release版本。

版本选择:

  • 我们此次学习使用的版本为: Greenwich SR1
  • 相关API文档: https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html

2、服务注册与发现Eureka

2.1、微服务基础模块设计

商品服务

  1. 查询商品列表

  2. 查询商品详情

订单服务

  1. 创建订单

案例功能:前台调用订单服务,订单服务远程调用商品服务获取商品详情信息,基于该商品信息创建订单

2.2、注册中心的作用

为什么需要注册中心:

远程服务调用在没有注册中心前存在什么问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xnjtnQSW-1654935432438)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220410111749045.png)]

微服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器,url地址维护带来很大问题

注册中心提供服务注册与发现功能,对服务的url地址进行统一管理

  1. 对于服务提供者Provider的作用
    启动的时候向注册中心上报自己的网络信息
  2. 对于服务消费者Consumer的作用
    启动的时候向注册中心上报自己的网络信息,拉取provider的相关网络信息

常见的注册中心:
zookeeper、Eureka、consul、nacos、etcd

2.3、Spring-Cloud Euraka介绍

Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server 并维持心跳连接。这样系统的维护人员就可以 通过Eureka Server来监控 系统中的各个微服务是否正常运行

在服务注册于发现中,有一个注册中心。当服务器启动的时候, 会把当前自己服务器的信息

比如:服务地址(IP:端口)等以别名方式注册到注册中心, 另一个(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通信地址(IP:端口),然后通过RPC框架进行远程方式(Ribbon+RestTemplate)调用接口实现功能

SpringCloud 将它集成在其子项目 spring-cloud-netflix 中,以实现 SpringCloud 的服务发现功能

Eureka架构

参与角色

1、EurekaServer 服务端

Eureka服务端,负责服务发现与管理

  • 注册中心
2、EurekaClient 客户端
  • Provider
    启动的时候向注册中心上报自己的网络信息

  • Consumer
    启动的时候向注册中心上报自己的网络信息,拉取provider的相关网络信息

2.4、案例项目

项目结构

  • springcloud2020
    • cloud-eureka-server8761
    • cloud-product-api ​
    • cloud-provider-product8080 ​
    • cloud-order-api ​
    • cloud-consumer-order8090 ​
    • cloud-consumer-hystrix-dashboard8100 ​
    • cloud-gateway-zuul9000 ​
    • cloud-config-server7000

2.4.1 父项目管理pom依赖

<!--导入springboot项目-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/>
    </parent>
    <!--配置jdk版本与springcloud版本-->
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
    </properties>
    <!--导入springcloud依赖-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <!--公共依赖-->
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

2.4.2 注册中心eureka-server搭建

步骤
  1. 选中父项目右键新建module
  2. 使用Spring Initializr创建SpringBoot项目,选择Cloud Discover->Eureka Server
    这里使用简单的maven项目
  3. 启动类上贴上@EnableEurekaServer注解
  4. 修改application.properties为application.yml文件,添加相关配置信息. https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#spring-cloud-eureka-server
  5. 运行测试,打开浏览器输入http://localhost:8761
新建子项目-cloud-eureka-server8761

pom.xml

<dependencies>
    <!-- 导入springboot-web环境 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 导入springboot-监控依赖【后续图形化监控】 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- Eureka服务端依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

application.yml

server:
  port: 8761
eureka:
  instance:
      hostname: localhost
  client:
      #是否将自己注册进去eureka,false为不注册,true注册
      registerWithEureka: false
      #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
      fetchRegistry: false
      serviceUrl:
          #eureka注册中心地址
          defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

官方文档

https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#spring-cloud-eureka-server

启动类-EurekaMain8761

@SpringBootApplication
@EnableEurekaServer
public class EurekaMain8761 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain8761.class, args);
    }
}

运行启动类,打开浏览器输入

http://localhost:8761

2.4.3 商品服务接口product-api搭建

步骤
  1. 选中父项目右键新建module
  2. 创建骨架类型为quickstart项目,删除多余的依赖和src/test目录的文件
    (上课为了演示步骤,使用简单的maven项目)
  3. 编写domain类product
新建子项目-cloud-product-api

pom.xml

暂时不用

domain-Product
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
    private Long id;//商品id
    private String name;//商品名称
    private BigDecimal price;//商品价格
    private int stock;//商品库存
}

2.4.4 商品服务product-server搭建

步骤
  1. 选中父项目右键新建module
  2. 使用Spring Initializr创建SpringBoot项目,选择Cloud Discover->Eureka Discover 和 Web->Web
    上课为了演示步骤,使用简单的maven项目
  3. 把application.properties修改成application.yml,并添加配置信息
    https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#netflix-eureka-client-starter
  4. 启动测试,会在Eureka注册中心控制台页面中看到product-server实例
  5. 添加mapper,service,controller类
新建子项目-cloud-provider-product8080

pom.xml

<!--导入springboot-web环境-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--导入springboot-监控依赖【后续图形化监控】-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--Eureka客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--导入cloud-product-api依赖-->
<dependency>
    <groupId>com</groupId>
    <artifactId>cloud-product-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

application.yml

server:
  port: 8080
spring:
  application:
    name: product-server
eureka:
  client:
    #是否将自己注册进去eureka,false为不注册,true注册
    registerWithEureka: true
    #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    serviceUrl:
      #eureka注册中心地址
      defaultZone:  http://localhost:8761/eureka/

官方文档

https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#netflix-eureka-client-starter

启动类

@SpringBootApplication
@EnableEurekaClient
public class ProductMain8080 {
    public static void main(String[] args) {
        SpringApplication.run(ProductMain8080.class,args);
    }
}
业务类 mapper
//这里本应该使用mybatis接口,因为不想将项目弄麻烦,不整合mybatis类,使用class类模拟
@Repository
public class ProductMapper {
    //模拟查询出来的数据
    private static final Map<Long, Product> PRODUCT_MAP = new HashMap<>();
    static {
        PRODUCT_MAP.put(1L, new Product(1L, "小米10", new BigDecimal("1000"), 10000));
        PRODUCT_MAP.put(2L, new Product(2L, "荣耀10", new BigDecimal("2000"), 10000));
        PRODUCT_MAP.put(3L, new Product(3L, "iphone10", new BigDecimal("3000"), 10000));
        PRODUCT_MAP.put(4L, new Product(4L, "vivo10", new BigDecimal("4000"), 10000));
    }
    //模拟数据库查询
    public void save(Product product){
        PRODUCT_MAP.put(PRODUCT_MAP.size()+0L, product);
    }
    public Product get(Long id){
        return PRODUCT_MAP.get(id);
    }
}
service
@Service
public class ProductServiceImpl implements IProductService {
    @Autowired
    private ProductMapper productMapper;
    @Override
    public void save(Product product) {
        productMapper.save(product);
    }
    @Override
    public Product get(Long id) {
        return productMapper.get(id);
    }
}
controller
@RestController
@RequestMapping("products")
public class ProductController {
    @Autowired
    private IProductService productService;
    
    @GetMapping("/get/{id}")
    private Object get(@PathVariable Long id){
        return productService.get(id);
    }
}
测试
  1. 先启动cloud-eureka-server8761

  2. 再启动cloud-provider-product8080

  3. 打开浏览器输入
    http://localhost:8761
    查看服务注册栏
    Instances currently registered with Eureka
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NgqIuIuv-1654935432440)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220410123219368.png)]

  4. 打开浏览器,查询id为1的商品信息
    http://localhost:8080/products/get/1

Eureka 客户端与服务器之间的通信

  1. Register(注册)
    Eureka 客户端将关于运行实例的信息注册到 Eureka 服务器。注册发生在第一次心跳
  2. Renew(更新 / 续借)
    Eureka客户端 需要更新最新注册信息(续借),通过每30秒发送一次心跳。更新通知是为了告诉Eureka服务器实例仍然存活。如果服务器在90秒内没有看到更新,它会将实例从注册表中删除。建议不要更改更新间隔,因为服务器使用该信息来确定客户机与服务器之间的通信是否存在广泛传播的问题
  3. Fetch Registry(抓取注册信息)
    Eureka客户端 从服务器获取注册表信息并在本地缓存。之后,客户端使用这些信息来查找其他服务。通过在上一个获取周期和当前获取周期之间获取增量更新,这些信息会定期更新(每30秒更新一次)。获取的时候可能返回相同的实例。Eureka客户端 自动处理重复信息
  4. Cancel(取消)
    Eureka客户端在关机时向Eureka服务器发送一个取消请求。这将从服务器的实例注册表中删除实例,从而有效地将实例从流量中取出

2.5、SpringCloud Eureka 自我保护机制

概述:

保护模式主要用于一组客户端和 Eureka Server 之间存在网络分区场景下的保护,一旦进入保护模式EurekaServer 将会尝试保护其注册表中的信息,不再删除服务注册表的数据,也就是就算服务断开也不销毁任何服务

Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,但是在保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败,对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。

我们在单机测试的时候很容易满足心跳失败比例在 15 分钟之内低于 85%,这个时候就会触发 Eureka 的保护机制,一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,此时我们可以使用eureka.server.enable-self-preservation=false关闭保护机制,这样可以确保注册中心中不可用的实例被及时的剔除(不推荐)

修改cloud-eureka-server8761的配置文件

server:
  port: 8761
eureka:
  instance:
    hostname: localhost
  client:
    #不向注册中心注册自己
    registerWithEureka: false
    #自己是服务端,不需要检查服务
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    #默认是true,现在关闭自我保护机制,保证不可用服务被删除
    enable-self-preservation: false

修改cloud-provider-product8080的配置文件

默认是等90秒钟(30秒发一次,共三次),eureka服务才删除,如果不想等,可在客户端配置修改90s时间

eureka:
  client:
    #是否将自己注册进去eureka,false为不注册,true注册
    registerWithEureka: true
    #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    serviceUrl:
      defaultZone:  http://localhost:8761/eureka/
  instance:
    #eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
    lease-renewal-interval-in-seconds: 1
    #eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
    lease-expiration-duration-in-seconds: 2

3、微服务调用方式Ribbon

3.1 订单服务接口order-api搭建

步骤

  1. 选中父项目右键新建module
  2. 创建骨架类型为quickstart项目,删除多余的依赖和src/test目录的文件
    (上课为了演示步骤,使用简单的maven项目)
  3. 编写domain类Order

建子项目-cloud-order-api

domain

@Setter
@Getter
public class Order implements Serializable {
    private String orderNo;
    private Date createTime;
    private String productName;
    private BigDecimal productPrice;
    private Long userId;
}

3.2 订单服务order-server搭建

步骤

  1. 选中父项目右键新建module
  2. 使用Spring Initializr创建SpringBoot项目,选择Cloud Discover->Eureka Discover , Web->Web , Cloud Routing->Robbin
    (上课为了演示步骤,使用简单的maven项目)
  3. 在项目的pom.xml文件添加cloud-product-api的依赖
  4. 添加相关的依赖配置
  5. 把创建订单的功能实现(获取商品信息暂先放下)

新建子项目-cloud-consumer-order8090

pom.xml

<!--导入springboot-web环境-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--导入springboot-监控依赖【后续图形化监控】-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--Eureka客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--导入ribbon依赖,用于远程调用-->
<!--可加可不加, eureka-client客户端默认引入ribbon依赖-->
<!--<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>-->
<!--导入cloud-product-api, cloud-order-api依赖-->
<dependency>
    <groupId>com</groupId>
    <artifactId>cloud-product-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com</groupId>
    <artifactId>cloud-order-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

application.yml

server:
  port: 8090
spring:
  application:
    name: order-server
eureka:
  client:
    #是否将自己注册进去eureka,false为不注册,true注册
    registerWithEureka: true
    #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    serviceUrl:
      defaultZone:  http://localhost:8761/eureka/
  instance:
    #eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
    lease-renewal-interval-in-seconds: 1
    #eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
    lease-expiration-duration-in-seconds: 2

官方文档

https://cloud.spring.io/spring-cloud-netflix/single/spring-cloud-netflix.html#netflix-eureka-client-starter

启动类

@SpringBootApplication
@EnableEurekaClient
public class OrderMain8090 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain8090.class, args);
    }
}
业务类 OrderServiceImpl
@Service
public class OrderServiceImpl implements IOrderService {
    @Override
    public Order save(Long userId, Long productId) {
        Product product = null; //假装通过远程调用 product-server 获取
        Order order = new Order();
        order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
        order.setCreateTime(new Date());
        order.setUserId(userId);
        order.setProductName(product.getName());
        order.setProductPrice(product.getPrice());
        System.out.println("执行保存订单操作");
        return order;
    }
}
OrderController

为了方便测试,使用 GET 请求方式,按理说要使用 POST 请求

@RestController
@RequestMapping("orders")
public class OrderController {
    @Autowired
    private IOrderService orderService;
    
    @GetMapping("/save/{userId}/{productId}")
    public Order save(@PathVariable Long userId,@PathVariable Long productId){
        return orderService.save(userId,productId);
    }
}

思考:如何实现远程调用?

使用目前已学习的技术来实现远程调用

A项目:
​ cloud-provider-product8080

B项目:
​ cloud-consumer-order8090

AB 项目交互 ------> 之前发短信 ----> http协议 -----> RestTemplate

3.3 使用Ribbon实现远程调用(RestTemplate)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZKh4NxcM-1654935432441)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411104352091.png)]

此处不会有 跨域 的错误,是因为 RestTemplate 类就相当于是 浏览器,可以访问任何端口服务

而出现跨域错误是,在项目中,使用 Ajax 的方式,url 地址是跨域请求

  1. 在 OrderMain8090 启动类中添加 RestTemplate 的 bean

    @Bean
    public RestTemplate template(){
        return new RestTemplate();
    }
    
  2. 使用 RestTemplate.getObject 获取远程接口的信息

    @Service
    public class OrderServiceImpl implements IOrderService {
    
        /*
            这种实现方式存在问题:写死 product-server 的ip端口,后续如果product-server做了集群
            那么这种方式就无法使用集群中其他 product-server 服务,比如说端口8081的这个服务
            此时解决方案:使用ribbon组件里面负载均衡(狭义上的理解多个服务平均分配请求)功能
         */
        @Autowired
        private RestTemplate template;
        public static final String PRODUCT_URL = "http://localhost:8080";
    
        @Override
        public Order save(Long userId, Long productId) {
            
            //真实远程获取
            //http请求方式获取 product-server 服务里面的商品信息
            Product product = template.getForObject(
                PRODUCT_URL + "/products/get/" + productId, Product.class
            );
            
            Order order = new Order();
            order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
            order.setCreateTime(new Date());
            order.setUserId(userId);
            order.setProductName(product.getName());
            order.setProductPrice(product.getPrice());
            System.out.println("执行保存订单操作");
            return order;
        }
    }
    
  3. 测试
    http://localhost:8090/orders/save/1/3

3.4 使用Ribbon实现负载均衡

  1. 修改启动类 OrderMain8090

    @SpringBootApplication
    @EnableEurekaClient
    public class OrderMain8090 {
        
        @LoadBalanced //ribbon给restTemplate开启负载均衡访问操作
        @Bean
        public RestTemplate template(){
            return new RestTemplate();
        }
    
        public static void main(String[] args) {
            SpringApplication.run(OrderMain8090.class, args);
        }
    }
    
  2. 修改订单服务 OrderServiceImpl

    //因为要进行负载均衡调用,此时需要的是Eureka中 product-server 服务名
    //因为Eureka中可以通过服务名获取到product-server所有服务集群的ip与端口
    @Autowired
    private RestTemplate template;
    public static final String PRODUCT_URL = "http://PRODUCT-SERVER";
    
  3. 修改 ProductController

    @RestController
    @RequestMapping("products")
    public class ProductController {
        @Autowired
        private IProductService productService;
        
        @Value("${server.port}")
        private String port;
    
        @GetMapping("/get/{id}")
        private Object get(@PathVariable Long id){
            Product product = productService.get(id);
            //防止端口重复拼接
            Product pp = new Product();
            BeanUtils.copyProperties(product,pp);
            //想让order-server服务调用product接口时,知道调用哪一个服务(8080?8081)
            pp.setName(product.getName() + "_" + port);
            return pp;
        }
    }
    
  4. 测试
    1:先启动ProductMain8080
    2:修改application.yml里面端口8081
    再启动ProductMain8080 , 模拟启动了2个Product-server服务

负载均衡策略调整

上面的负载均衡策略是 轮询选择 即两个服务之间循环调用

  1. 修改 cloud-consumer-order8090 配置
    注意:服务的名称需要和代码中的服务名称一致,不然是修改不了负载均衡策略

    server:
      port: 8090
    spring:
      application:
        name: order-server
    eureka:
      client:
        #是否将自己注册进去eureka,false为不注册,true注册
        registerWithEureka: true
        #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
        fetchRegistry: true
        serviceUrl:
          defaultZone:  http://localhost:8761/eureka/
      instance:
        #eureka客户端向服务端发送心跳的时间间隔,单位为秒,默认是30
        lease-renewal-interval-in-seconds: 1
        #eureka服务端收到最后一次心跳等待的时间上限,单位为秒,默认是90,超时剔除
        lease-expiration-duration-in-seconds: 2
    PRODUCT-SERVER:   #必须是provide的服务名
      ribbon:
        NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
    
  2. 其他策略

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZUeGfxL-1654935432442)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220410135825552.png)]

4、微服务调用方式Feign

思考:使用RestTemplate实现远程调用方便吗?

  1. 硬编码
  2. 参数不可控

此次使用定义Feign接口,使用接口方式解决上述问题

4.1 改造使用Feign方式实现远程调用

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_spring_cloud_openfeign

分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DKkku3v4-1654935432443)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411104418999.png)]

步骤

  1. 在cloud-product-api项目中添加openfeign依赖
  2. 在cloud-product-api项目中添加ProductFeignApi接口
  3. 在cloud-provide-product8080项目中添加 ProductFeignApi 的实现类(本质上就是个Controller),注意要把之前的controller删除掉
  4. 在 cloud-consumer-order8090 项目中的启动类上贴上 @EnableFeignClients 注解
  5. 把之前RestTemplate的远程调用替换成Feign方式调用即可 (注意 cloud-product-api 和 cloud-provide-product8080 中包名的问题)

1、修改 cloud-product-api 项目依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

2、在 cloud-product-api 项目中添加 IProductFeignApi 接口

/**
 *  @FeignClient:feign客户端,用来取代之前ribbon操作
 *      name表示调用服务名
 */
@FeignClient(name = "PRODUCT-SERVER")
public interface IProductFeignApi {
    /**
     *  访问的指定服务的具体接口
     *  此处表示:访问product-server服务的 products/get/id 接口
     */
    @GetMapping("/products/get/{id}")
    Product get(@PathVariable Long id);
}

3、修改 cloud-consumer-order8090 项目中的启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderMain8090 {
    //注释掉ribbon调用方式
    /*@LoadBalanced //ribbon给restTemplate开启负载均衡访问操作
    @Bean
    public RestTemplate template(){
        return new RestTemplate();
    }*/

    public static void main(String[] args) {
        SpringApplication.run(OrderMain8090.class, args);
    }
}

4、修改cloud-consumer-order8090项目中OrderServiceImpl

@Service
public class OrderServiceImpl implements IOrderService {
    //注释掉ribbon的调用方式
    /*//因为要进行负载均衡调用,此时需要的是Eureka中 product-server 服务名
    //因为Eureka中可以通过服务名获取到product-server所有服务集群的ip与端口
    @Autowired
    private RestTemplate template;
    public static final String PRODUCT_URL = "http://PRODUCT-SERVER";*/

    //改用feign接口方式
    @Autowired
    private IProductFeignApi productFeignApi;

    @Override
    public Order save(Long userId, Long productId) {
        //http 请求方式获取 product-server 服务里面的商品信息
        //Product product = template.getForObject(PRODUCT_URL + "/products/get/" + productId, Product.class); //真实远程获取
        //Product product = null; //假装通过远程调用 product-server 获取

        //feign 接口方式获取 product-server 服务里面的商品信息
        Product product = productFeignApi.get(productId);
        Order order = new Order();
        order.setOrderNo(UUID.randomUUID().toString().replace("-",""));
        order.setCreateTime(new Date());
        order.setUserId(userId);
        order.setProductName(product.getName());
        order.setProductPrice(product.getPrice());
        System.out.println("执行保存订单操作");
        return order;
    }
}

改进——使用实现类 ProductFeignClient

在 cloud-provider-product8080 建新类 ProductFeignClient

​ springcloud推崇的不适用controller进行对外提供接口服务,使用feign客户端方式
​ 其实本质还是controller,换一种写法而已,注意要把之前的controller删除掉

@RestController
public class ProductFeignClient implements IProductFeignApi {
    @Autowired
    private IProductService productService;

    @Value("${server.port}")
    private String port;

    @Override
    public Product get(Long id) {
        Product product = productService.get(id);
        Product result = new Product();
        BeanUtils.copyProperties(product,result);
        result.setName(result.getName()+",data from "+port);
        return result;
    }
}

4.2 Feign超时时间设置

源码中默认options中配置的是6000毫秒,但是Feign默认加入了Hystrix,此时默认是1秒超时

我们可以通过修改配置,修改默认超时时间.

1、改动cloud-provider-product8080中 ProductFeignClient 类get方法,睡眠3秒
@Override
public Product get(Long id) {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Product product = productService.get(id);
    Product result = new Product();
    BeanUtils.copyProperties(product,result);
    result.setName(result.getName()+",data from "+port);
    return result;
}
2、改动cloud-consumer-order8090的application.yml中配置,feign有效时间2s测试

会报错,因为睡了 3 秒, java.net.SocketTimeoutException:Read timed out

3、改动cloud-consumer-order8090的application.yml中配置,feign有效时间5s测试
# 设置超时时间
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

4.3 Feign超时重试次数设置

https://github.com/Netflix/ribbon/wiki/Getting-Started#the-properties-file-sample-clientproperties

默认重试 1 次,即会执行 2 次;默认超时时间 1 s

重试设置要谨慎配置:如果请求是添加操作,不能配置重试,除非方法中进行判断,以防出现重复结果

  • # Max number of retries on the same server (excluding the first try)
    超时重试次数,0表示不重试,1表示还会重试一次,一共执行2次
    sample-client.ribbon.MaxAutoRetries=1
  • # Max number of next servers to retry (excluding the first server)
    集群中重试几个服务器,即当前服务器宕机之后,还会访问其他服务器尝试。则一共执行4次
    sample-client.ribbon.MaxAutoRetriesNextServer=1

修改 cloud-consumer-order8090 的配置文件

PRODUCT-SERVER:
  ribbon:
    #ConnectTimeout: 5000
    #ReadTimeout: 5000
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 1

5、服务熔断与降级Hystrix

5.1 熔断与降价概念讲解

降级

抛弃非核心业务,尽量保障核心页面的正常运行 (例如:网络不好时视频清晰度越来越低)

服务器忙,请稍后再试, 不让客户端等待立刻返回一个友好的提示, fallback

引发服务降级:
1>程序运行异常
2>调用超时
3>服务熔断出发服务降级
4>线程池 / 信号量打满也会导致服务降级

熔断

类比保险丝超过最大功率后熔断一样, 服务器达到最大访问处理量之后,拒绝再接受服务,被拒绝的请求直接调用服务降级方法,返回友好提示

一般熔断操作过程:

正常服务访问 ----> 遭遇异常/超时等意外情况,服务降级 ---->

多次请求处理无果进而熔断 ----> 熔断时间到,尝试恢复调用链路

两者之间的联系

java 跟 javascript 关系【没多大关系】

5.2 服务雪崩效应原因和解决思路

产生的原因

服务雪崩

多个服务之间调用的时候,假设微服务A调用微服务B和微服务C, 微服务B和微服务C又调用了其他微服务,这就是所谓的 “扇出” , 如果扇出的链路上某个微服务的调用响应时间过长或者不可用, 对微服务A的调用就会占用越来越多的系统资源,进而引起系统的崩溃, 所谓的 “雪崩效应”

对于高流量的应用来说, 单一的后端依赖可能会导致所有服务器上的资源都在几秒钟内饱和,比失败更悲催的是, 这些应用程序还可能导致服务间的延迟增加, 备份队列,线程和其他系统资源紧张, 导致整个系统发生更多的 级联故障 , 这些都表示需要对故障和延迟进行隔离和管理, 以便单个依赖关系的失败, 不能取消整个应用程序或系统。

所以, 通常你发现一个模块下某个服务失败后, 这时候这个模块依赖接受流量, 然后这个有问题的模块还调用其他某款,这样就会发生级联故障, 或者叫雪崩

解决方案:超时机制

如果我们加入超时机制,例如 2s ,那么超过 2s 就会直接返回了,那么这样就在一定程度上可以抑制消费者资源耗尽的问题

解决方案:服务限流

通过线程池+队列的方式或者通过信号量的方式。比如商品评论比较慢,最大能同时处理10个线程,队列待处理5个,那么如果同时20个线程到达的话,其中就有5个线程被限流了,其中10个先被执行,另外5个在队列中

解决方案:服务熔断

当依赖的服务有大量超时时,在让新的请求去访问根本没有意义,只会无畏的消耗现有资源,比如我们设置了超时时间为 1s ,如果短时间内有大量请求在 1s 内都得不到响应,就意味着这个服务出现了异常,此时就没有必要再让其他的请求去访问这个服务了,这个时候就应该使用熔断器避免资源浪费

解决方案:服务降级

有服务熔断,必然要有服务降级。

所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自己准备一个本地的 fallback(回退)回调,返回一个缺省值

例如:(备用接口 / 缓存 / mock数据),这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强,当然这也要看适合的业务场景

技术实现

熔断机制

为应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错,不可用或者响应时间太长时,会进行服务降级,进而熔断节点微服务的调用,快速返回错误的响应信息

当检测到该节点微服务调用响应正常,恢复调用链路

在springcloud架构里, 熔断机制通过Hystrix实现, Hystrix会监控微服务调用状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,会启动熔断机制

5.3 Hystrix简介

是什么?

hystrix对应的中文名字是“豪猪”

是一个用于处理分布式系统的延时和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,通过熔断隔离的方式,以提高分布式系统的弹性

https://github.com/Netflix/Hystrix

https://github.com/Netflix/Hystrix/wiki

为啥要用?

在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接缓慢,资源繁忙,暂时不可用,服务脱机等

当依赖阻塞时,大多数服务器的线程池就出现阻塞(BLOCK),影响整个线上服务的稳定性,在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败。高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险。

解决方案:对依赖做隔离

能做啥?

提供了熔断、隔离、Fallback、cache、监控等功能

实现原理

断路器

本身是一种开关装置,当某个服务单元发生故障之后,通过断路器监控(类似熔断保险丝), 向调用方返回一个符合预期的,可处理的备选响应(FallBack),而不是长时间等待或者抛出调用方法无法处理异常,这样就保证服务调用方的线程不会被长时间、不必要的占用, 从而避免故障在分布式系统中蔓延,乃至雪崩

  1. 正常请求时,正常处理
  2. 在一个时间窗内(默认10s),请求处理异常超过一定比例(默认50%),执行熔断
  3. 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级 fallback ,通过断路器,实现自动的发现错误, 并将主逻辑切换为降级逻辑,减少响应延迟的效果
  4. 原来的主逻辑如何恢复(hystrix 会自动恢复)

当断路器打开, 对主逻辑进行熔断后, hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑;

当休眠时间窗到期, 断路器将进入半开状态,释放一次请求到原来的主逻辑上;

如果此次请求正常返回,那么断路器将继续闭合, 主逻辑恢复

如果这次请求依然有问题, 断路器继续进行打开状态, 休眠时间窗重新计时

大神论文:https://martinfowler.com/bliki/CircuitBreaker.html

5.4 修改 cloud-consumer-order8090 项目

只要有请求接收,都可以进行降级操作

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_circuit_breaker_hystrix_clients

步骤

  1. 在cloud-consumer-order8090添加 hystrix 依赖
  2. 在启动类中添加 @EnableCircuitBreaker 注解
  3. 在最外层添加熔断降级的处理
    在order-server中的控制器中添加 @HystrixCommand(fallbackMethod = "saveFail") 注解
    (注意 fallbackMethod 需要和原方法一样的签名)

1、依赖pom.xml

<!-- hystrix依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

2、启动类OrderMain8090

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableCircuitBreaker  //或者 @EnableHystrix
public class OrderMain8090 {
    //......
}    

3、接口OrderController

@RestController
@RequestMapping("orders")
public class OrderController {
    @Autowired
    private IOrderService orderService;

    //降级配置,一旦此方法异常/超时,就会执行fallbackMethod指定的降级方法
    @HystrixCommand(fallbackMethod = "saveFallback")
    @GetMapping("/save/{userId}/{productId}")
    public Order save(@PathVariable Long userId,@PathVariable Long productId){
        return orderService.save(userId,productId);
    }

    //降级方法
    //注意:方法签名跟映射接口一样,仅仅是方法名不一样
   public Order saveFallback(@PathVariable Long userId,@PathVariable Long productId){
        System.out.println("走降级方法....");
        return new Order() ;
    }

}

4、测试

修改 cloud-provider-product8080类 ProductFeignClient,模拟出现异常

多次请求观察打印信息

5.5 熔断降级服务异常报警通知

熔断后发短信或邮寄进行提示,提醒运维人员紧急系统维护

需求:异步短信提示频率20秒内

添加 redis 依赖

<!--redis依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置文件

spring:
  redis:
    host: 127.0.0.1

修改 cloud-consumer-order8090 的接口 OrderController 中 saveFallback 降级方法

//降级方法
//注意:方法签名跟映射接口一样,仅仅是方法名不一样
public Order saveFallback(@PathVariable Long userId,@PathVariable Long productId){
    System.out.println("走降级方法....");
    //通知运维人员或者程序员赶紧过来修复
    //发短信或发邮件
    //redis限制频率
    
    //启动一个线程,让线程执行发短信逻辑
    new Thread(()->{
        String redisKey = "order-save";
        String value = stringRedisTemplate.opsForValue().get(redisKey);
        if(StringUtils.isEmpty(value)){
            System.out.println("order下订单服务失败,请查找原因.");
            stringRedisTemplate.opsForValue().set(redisKey,"save-order-fail",20, TimeUnit.SECONDS);
        }else{
            System.out.println("已经发送过短信");
        }
    }).start();
    return new Order();
}

5.6 Feign集成Hystrix

因为 cloud-product-api 使用的是 Feign 的方式,在 cloud-product-api 里面如果出现异常/超时,也需要熔断,name就需要 Feign 集成 Hystrix

1、在cloud-product-api中新建降级方法实现类ProductFeignHystrix

注意点:
1. 自定义类实现 IProductFeignApi 接口
2. 贴 @Component 交给spring容器管理

/**
 *  实现 IProductFeignApi 接口目的是为了指定哪些方法需要进行降级处理
 *  该类用于product-server服务对外提供接口进行降级保护
 */
@Component
public class ProductFeignHystrix implements IProductFeignApi {
    //IProductFeignApi接口里面get方法的降级方法
    @Override
    public Product get(Long id) {
        System.out.println("走降级方法了。。ProductFeignHystrix。。");
        Product product = new Product();
        product.setName("降级方法:默认对象");
        return product;
    }
}

2、修改cloud-product-api中IProductFeignApi接口

/**
 *  @FeignClient:feign客户端,用来取代之前ribbon操作
 *      name表示调用服务名
 *      fallback表示降级方法所在类
 */
@FeignClient(name = "PRODUCT-SERVER", fallback = ProductFeignHystrix.class)
public interface IProductFeignApi {
    /**
     *  访问的指定服务的具体接口
     *  此处表示:访问product-server服务的 products/get/id 接口
     */
    @GetMapping("/products/get/{id}")
    Product get(@PathVariable Long id);
}

3、修改cloud-consumer-order8090的配置文件

默认是关闭的,需要手动开启一下

feign:
  hystrix:
    enabled: true

结果

当 product-server 的 get 方法出现异常时,执行了降级方法

又因为此时 cloud-product-api 里面的降级方法返回了一个正常的 product 类对象,所以 consumer-server 里面没有异常,就不会执行此降级方法

5.7 超时时间调整

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zFxmpwJG-1654935432445)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220412140358586.png)]

现学组件有 ribbon feign hystrix 都有超时控制,该如何选择?

推荐方案:
1. feign 或者 hystrix > 正常调用业务耗时
2. hystrix > feign

因为 ribbon 和 feign 的超时设置会冲突

PRODUCT-SERVER:  # ribbon
  ribbon:
    ConnectTimeout: 2000
    ReadTimeout: 2000
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 0

feign:          # feign
  hystrix:
    enabled: true #Feign集成Hystrix
  client:
    config:
      default:
        connectTimeout: 4000
        readTimeout: 4000

hystrix:       # hystrix 
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000

5.8 断路器Dashboard监控仪表盘

除了隔离依赖服务调用以为,Hystrix还提供了准实时的调用监控(Hystrix DashBoard) Hystrix会持续的几率所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求,多少是成功的,多少是失败的等, Netflix通过Hystrix-metrics-event-stream项目实现对上面指标的监控。SpringCloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面

新建子项目-cloud-consumer-hystrix-dashboard8100

1、pom.xml

<!-- hystrix监控仪 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!--Eureka客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2、application.yml

测试的时候,无法将 Dashboard 注入 Eureka 注册中心,会报错

registerWithEureka: false 但是不影响其他操作

server:
  port: 8100
  
eureka:
  instance:
    hostname: localhost
  client:
    #是否将自己注册进去eureka,false为不注册,true注册
    registerWithEureka: true
    #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

3、给所有需要监控的服务(8080/8090)加依赖

<!--导入springboot-监控依赖【后续图形化监控】-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

4、给所有需要监控的服务(8080/8090)监控配置

#hystrix监控配置
management:
  endpoints:
    web:
      exposure:
        include: ["health","info","hystrix.stream"]

5、启动类-DashBoard8100

@SpringBootApplication
@EnableHystrixDashboard
public class DashBoard8100 {
    
    //请求数据获取路径
    @Bean
    public ServletRegistrationBean servletRegistrationBean(){
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean  = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
    
    public static void main(String[] args) {
        SpringApplication.run(DashBoard8100.class, args);
    }
}

6、测试

运行启动类,打开浏览器输入 http://localhost:8100/hystrix

然后在页面中的地址栏输入框输入:需要监控服务的地址 http://localhost:启动端口/actuator/hystrix.stream

此次以cloud-consumer-order8090为例子
http://localhost:8090/actuator/hystrix.stream

参数讲解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n9zhiw0L-1654935432446)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411110228969.png)]

6、微服务网关Zuul

6.1 Zuul简介

是什么?

Zuul 网关是系统的唯一对外的入口,介于客户端和服务器端之间的中间层,处理非业务功能 提供路由请求、鉴权、监控、缓存、限流等功能

Zuul包含了对请求的 路由过滤 两个最主要的功能:

其中路由能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一的入口的基础,而过滤器功能则负责对请求的处理过程进行干预,是实现请求验证、服务聚合等功能的基础。Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获取其他微服务的信息,也既以后的访问微服务都是通过Zuul跳转后获得

注意:Zuul服务最终还是会注册进Eureka

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7lZaRkXe-1654935432453)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411114338009.png)]

能干嘛?

  1. 验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
  2. 审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
  3. 动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
  4. 压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
  5. 负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
  6. 静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
  7. 多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。

提供 = 代理 + 路由 + 过滤三大功能

主流的网关

  1. zuul
    Netflix开源的微服务网关,和Eureka,Ribbon,Hystrix等组件配合使用.

  2. kong
    由Mashape公司开源的,基于Nginx的API gateway

  3. nginx+lua
    是一个高性能的HTTP和反向代理服务器,lua是脚本语言,让Nginx执行Lua脚本,并且高并发、非阻塞的处理各种请求

  4. GateWay

6.2 网关项目zuul-server搭建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CLqb8fkm-1654935432455)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220412140428568.png)]

步骤

  1. 创建SpringBoot项目,选择Cloud Discover->Eureka Discover , Cloud Rounting -> Zuul 依赖
    (下面是使用maven)
  2. 添加application.yml配置文件并添加相关的配置信息.
  3. 在启动类上贴上 @EnableZuulProxy 注解

新建子项目-cloud-gateway-zuul9000

1、pom.xml

<!--Eureka客户端-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--Zuul依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

2、application.yml

server:
  port: 9000
spring:
  application:
    name: zuul-server
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

3、启动类-ZuulMain9000

@SpringBootApplication
@EnableZuulProxy
public class ZuulMain9000 {
    public static void main(String[] args) {
        SpringApplication.run(ZuulMain9000.class, args);
    }
}

测试

  1. 不使用网关访问,直接访问订单
    http://localhost:8090/orders/save/1/1
  2. 使用网关
    访问订单
    http://localhost:9000/order-server/orders/save/1/1

6.3 相关配置

1、定义路由规则

可以自定义路由规则

可以通过网关实现网络隔离

因为前面要使用网关,就要在地址里面加入想要访问的 服务名称,较麻烦。自定义路由规则

修改cloud-gateway-zuul9000 的application.yml文件
server:
  port: 9000
spring:
  application:
    name: zuul-server
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
zuul:
  #忽略匹配,即: order-server这种格式请求路径忽略掉
  # 访问 http://localhost:9000/order-server/orders/save/1/1 会报错404
  ignoredPatterns: /*-server/**
  routes:
    #定制路由匹配规则
    order-server-route:
      #凡是请求路径中带有/order前缀的转发到order-server进行处理
      #简单的理解:配置前:http://localhost:9000/order-server/orders/save/1/1
      #           配置后:http://localhost:9000/order/orders/save/1/1
      path: /order/**
      serviceId: order-server
      
    product-server-route:
      path: /product/**
      serviceId: product-server

2、Cookie请求头的问题

默认情况,网关会把 “Cookie”、“Set-Cookie”、“Authorization” 这三个请求头过滤掉,下游的服务是获取不到这几个请求头的

如果不需要过滤这个请求头,可以修改过滤的集合的值

这个属性直接设置:sensitiveHeaders

修改cloud-gateway-zuul9000的 application.yml文件
server:
  port: 9000
spring:
  application:
    name: zuul-server
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/
zuul:
  #配置为空,表示什么都不过滤
  sensitiveHeaders:
  #忽略匹配,既: order-server这种格式请求路径忽略掉
  ignoredPatterns: /*-server/**
  routes:
    #定制路由匹配规则
    order-server-route:
      #凡是请求路径中带有/order前缀的转发到order-server进行处理
      #简单的理解:配置前:http://localhost:9000/order-server/orders/get/1/1
      #           配置后:http://localhost:9000/order/orders/get/1/1
      path: /order/**
      serviceId: order-server
    product-server-route:
      path: /product/**
      serviceId: product-server

6.4 zuul流程分析

https://github.com/Netflix/zuul/wiki/How-it-Works

核心类:ZuulServlet 是一个servlet

Zuul 的多个功能 是在各种 filters 里面实现的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4AHzDY3G-1654935432456)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411123117193.png)]

6.5 自定义Zuul过滤器实现登录鉴权

做登录判断,如果带了 token,表示登录了

没有token 表示没登录返回

步骤

在 cloud-gateway-zuul9000 项目里自定义类 AuthZuulFilter

注意,必须交给 Spring 管理

AuthZuulFilter

/**
 *  鉴权过滤器
 *      做登录判断,如果带了token,表示已登录,没有token表示没登录返回
 */
@Component
public class AuthZuulFilter extends ZuulFilter {

    //过滤器类型:指定当前是前置过滤器
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    //过滤器序号(优先级):优先级越小,越先执行
    @Override
    public int filterOrder() {
        return 1;
    }

    //是否执行鉴权过滤:true表示当前请求要执行鉴权操作,false表示当前请求不执行
    // 指定穿过zuul网关的请求过滤规则,true表示当前请求满足拦截条件
    @Override
    public boolean shouldFilter() {
        //做登录判断,如果带了token,表示已登录,没有token表示没登录返回
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        String token = request.getHeader("token");
        return StringUtils.hasLength(token);
    }

    /**
     *  鉴权操作逻辑
     *  执行前提:shouldFilter()返回true时
     *  表示实现过滤拦截逻辑
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        //有token,已登录的情况,需要鉴权
        System.out.println("已登录,执行鉴权");

        //不放行,不转发
        requestContext.setSendZuulResponse(false);
        //设置响应状态码401
        requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        try {
            //设置响应内容
            requestContext.getResponse().getWriter().write("no unauthorized");
        } catch (IOException e) {
            e.printStackTrace();
        }
        //不携带数据
        return null;
    }
}

下面是存放在 cookie 的情况

@Component
public class AuthZuulFilter extends ZuulFilter {
    //指定是前置过滤器
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }
    //定义过滤器优先级,越小越优先级越高
    @Override
    public int filterOrder() {
        return 1;
    }
    //指定穿过zuul网关的请求过滤规则,true表示当前请求满足拦截条件
    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        if(request.getRequestURI().indexOf("/order/")>=0){
            return true;
        }
        return false;
    }
    //当shouldFilter为true,执行该方法
    //表示实现过滤拦截逻辑
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        String cookie = request.getHeader("Cookie");
        if(StringUtils.isEmpty(cookie)){
            cookie = request.getParameter("Cookie");
        }
        if(StringUtils.isEmpty(cookie)){
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            try {
                requestContext.getResponse().getWriter().write("no unauthorized");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

7、链路追踪组件Sleuth&Zipkin

为什么需要链路追踪

在微服务框架中,一个由客户端发起的请求在后端系统中经过多个不同的服务节点调用,协同生产最后的请求结果,每一个前端请求会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败

那该如何解决呢?

  • sleuth : 链路追踪器
  • zipkin:链路分析器(可视化)

7.1 Sleuth工作流程

sleuth 记录的是一次请求链路(请求经过哪些服务,哪些类)

一条链路有唯一标识(Trace ID), 每个经过一个链路(服务)使用Span来标识不同请求(记录请求相关信息), 各个span间使用 parent ID 关联

详细流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGqPhHk7-1654935432457)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411141949378.png)]

简化流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gQ0xiU2K-1654935432458)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411141733725.png)]

7.2 集成链路追踪组件Sleuth

Sleuth是一个专门用于记录链路数据的开源组件

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#sleuth-adding-project

步骤

  1. 在cloud-provide-product8080 和cloud-consumer-order8090 中添加 sleuth 依赖
  2. 在需要写日志的类上贴 @Slf4j
    然后再cloud-consumer-order8090、cloud-provide-product8080中打印日志

修改 cloud-consumer-order8090

1、pom.xml

springcloud 默认集成了 sleuth + zipkin, 导一个即可

<!--springcloud 默认集成了 sleuth + zipkin, 导一个即可-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
2、修改类

贴上日志标签

@Slf4j  //lombok.extern.slf4j.Slf4j
public class OrderController {
    
    //降级配置,一旦此方法异常/超时,就会执行fallbackMethod指定的降级方法
    @HystrixCommand(fallbackMethod = "saveFallback")
    @GetMapping("/save/{userId}/{productId}")
    public Order save(@PathVariable Long userId,@PathVariable Long productId){
        log.info("OrderController..save..");
        return orderService.save(userId,productId);
    }
    
    //......
    
}
@Slf4j
public class OrderServiceImpl implements IOrderService {

    @Override
    public Order save(Long userId, Long productId) {
    	log.info("OrderController..save..");
        
        //......
}}

修改cloud-provider-product8080

1、pom.xml

springcloud 默认集成了 sleuth + zipkin, 导一个即可

<!--springcloud 默认集成了 sleuth + zipkin, 导一个即可-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
2、修改类
//贴上日志标签
@Slf4j
public class ProductController {
	//记录调用日志
	log.info("ProductController.get....");
    
//贴上日志标签
@Slf4j
public class ProductServiceImpl implements IProductService {
	//记录调用日志
	log.info("ProductController.get....");

日志参数讲解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t2koqRIc-1654935432459)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220412140546248.png)]

日志格式:

[order-server,c323c72e7009c077,fba72d9c65745e60,false]

  1. order-server,spring.application.name的值
  2. c323c72e7009c077,sleuth生成的Trace ID,用来标识请求链路,一条请求链路中包含一个Trace ID,多个Span ID
  3. fba72d9c65745e60,spanID 基本的工作单元,获取元数据,如发送一个http
  4. false,是否要将该信息输出到 zipkin 服务中来收集和展示

7.3 什么是Zipkin

zipkin是Twitter基于google的分布式监控系统Dapper(论文)的开发源实现,zipkin用于跟踪分布式服务之间的应用数据链路,分析处理延时,帮助我们改进系统的性能和定位故障。

官网:https://zipkin.io/

与sleuth集成原理

原理图1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c9NBeh3s-1654935432460)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411144935556.png)]

原理图2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4rgVvjrk-1654935432461)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411145000567.png)]

同类产品

相关阅读:https://www.zhihu.com/question/27994350

  1. CAT
    由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控
  2. Pinpoint
    由韩国团队naver团队开源,针对大规模分布式系统用链路监控,使用Java写的工具
  3. SkyWalking
    2015年由个人吴晟(华为开发者)开源 , 2017年加入Apache孵化器。
    针对分布式系统的应用性能监控系统,特别针对微服务、cloud native和容器化(Docker, Kubernetes, Mesos)架构, 其核心是个分布式追踪系统

7.4 Zipkin+Sleuth整合

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_sleuth_with_zipkin_via_http

步骤

  1. 启动zipkin服务,我们直接使用的是jar的方式

  2. 需要在product-server和order-server中的配置文件中添加zipkin地址

    spring:
      zipkin:
        base-url: http://localhost:9411
      sleuth:
        sampler:
          probability: 1
    

1、启动zipkin客户端

zipkin 是一个java客户端工具,直接使用 java -jar 方式启动即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YtzNiThV-1654935432462)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411145308013.png)]

2、修改cloud-consumer-order8090的application.yml

spring:
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      #采用率,介于0到1之间, 1表示全部收集
      probability: 1

3、修改cloud-provider-product8080的application.yml

spring:
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      #采用率,介于0到1之间, 1表示全部收集
      probability: 1

测试

访问 http://localhost:9411

8、分布式配置中心Config

配置中心的作用和好处

统一管理配置快速切换各个环境的配置

在微服务体系中,服务的数量以及配置信息的日益增多,比如各种服务器参数配置、各种数据库访问参数配置、各种环境下配置信息的不同、配置信息修改之后实时生效等等,传统的配置文件方式或者将配置信息存放于数据库中的方式已无法满足开发人员对配置管理的要求,如:

  • 安全性:配置跟随源代码保存在代码库中,容易造成配置泄漏
  • 时效性:修改配置,需要重启服务才能生效
  • 局限性:无法支持动态调整:例如日志开关、功能开关

所以,一套集中式的,动态的配置管理设施是必不可少的

Springcloud 提供了 config 组件来解决这种问题

相关产品

  1. 百度的disconf
    https://github.com/knightliao/disconf
  2. 阿里的diamand
    https://github.com/takeseem/diamond
  3. 携程Apollo
    https://github.com/ctripcorp/apollo

8.1 配置中心工作流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-32lhZ3Uw-1654935432463)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220411150631634.png)]

是什么

SpringCloud config 为微服务架构中的微服务提供 集中化的外部配置 支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置

怎么玩

SpringCloud config 分服务端和客户端两部分

  • ​ 服务端也称为分布式配置中心,它是一个独立的微服务应用, 用来连接配置服务器并为客户端提供配置信息, 加密/解密信息等访问接口
  • ​ 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容, 并在启动时候从配置中心获取和加载配置信息。配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具类方便管理和访问配置内容

8.2 搭建config-server项目

步骤

  1. 创建SpringBoot项目,选择Cloud Discover->Eureka Discover , Cloud Config -> Config Server 依赖
    (这里使用maven项目)
  2. 在启动类中贴上**@EnableConfigServer**注解
  3. 添加application.yml并设置相关的配置

1、搭建git+config的分布式配置中心

就是先在 码云 上新建仓库并初始化好

这里的 git 服务器可以是:gitlab、github、码云等

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_spring_cloud_config_server

去码云中注册账号,创建自己的项目

2、新建-cloud-config-center7000子项目

pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.yml

server:
  port: 7000
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          #在gitee中新建的仓库路径,注意是地址栏上的
          uri: https://gitee.com/xxx/cloud-config
          #码云账号
          username: wangyifei@wolfcode.cn
          #码云账号秘密
          password: dafei666
          label: master

eureka:
  client:
    #是否将自己注册进去eureka,false为不注册,true注册
    registerWithEureka: true
    #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    serviceUrl:
      defaultZone:  http://localhost:8761/eureka/

启动类-ConfigMain7000

@SpringBootApplication
@EnableConfigServer
public class ConfigMain7000 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigMain7000.class, args);
    }
}

测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zWnmjt1V-1654935432464)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220412140752701.png)]

在码云master分支上新建配置文件order-dev.yml

server:
  port: 8888

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HCHXLJUX-1654935432465)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220412140823186.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sFm059lX-1654935432465)(C:\Users\ASUS\AppData\Roaming\Typora\typora-user-images\image-20220412140851598.png)]

在码云dafei分支上新建配置文件order-dev.yml

server:
  port: 9999

启动服务-ConfigMain7000

分别访问地址

  • http://localhost:7000/order-dev.yml
  • http://localhost:7000/master/order-dev.yml
  • http://localhost:7000/dafei/order-dev.yml

拓展-请求路径读取规则

/{application}/{profile}/{label}
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml  # 上面使用的是这种规则,建议使用这种
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

application: 配置文件名,一般是服务名
profile: 环境 dev  test  prod等
label:git 分支

8.3 分布式配置中心客户端

https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_spring_cloud_config_client

步骤

  1. 在cloud-consumer-order8090中添加config-client的依赖
  2. 修改对应服务的配置文件,把application.yml 改为 bootstrap.yml
  3. 把其他配置都放入到git服务器期中管理

1、以cloud-consumer-order8090项目为例子

修改pom.xml

<!-- 分布式配置中心客户端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

2、修改原先的application.yml为bootstrap.yml

bootstrap.yml

spring:
  application:
    name: order-server
  cloud:
    config:
      label: master  #分支名称
      name: order    #配置文件名称
      profile: dev   #读取后缀名称:
      uri: http://localhost:7000
      #上述4个综合: http://localhost:7000/master/order-dev.yml
eureka:
  client:
    #是否将自己注册进去eureka,false为不注册,true注册
    registerWithEureka: true
    #是否从eureka抓取注册信息,单点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    serviceUrl:
      defaultZone:  http://localhost:8761/eureka/

boostrap 与 application 区别:

  1. application.yml 是用户级别的资源配置项
    bootstrap.yml 是系统级别,优先级更高
  2. Spring cloud 会创建一个 Bootstrap Context 作为 Spring 应用的Application Context 的父上下文,初始化的时候, BootstrapContext负责从外部加载配置属性并解析,这2个上下文共享一个从外部的Environment
  3. Bootstrap 属性有高优先级, 默认情况下, 它们不会被本地配置覆盖,Bootstrap context 跟 Application Context 有着不同的约定, 所以新增一个bootstrap.yml文件保证Bootstrap Context 跟 Application Context 配置分离
  4. 要将Client 模块下的application.yml 改成bootstrap.yml,这是很关键的,因为bootstrap.yml是必application.yml先加载的, bootstrap.yml优先级高与application.yml

3、修改码云上master分支的order-dev.yml

bootstrap.yml 没配置那些, order-dev.yml必须配置

一般来说, bootstrap.yml 配置那些不经常改动的, 码云 order-dev.yml 配置经常改动的

server:
  port: 8090
spring:
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      #采用率,介于0到1之间, 1表示全部收集
      probability: 1
PRODUCT-SERVER:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 0
feign:
  hystrix:
    enabled: true
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

测试

启动 eureka 服务器

启动 config 服务器

启动 order 服务器

观察 order 端口是不是8090(从码云上加载而来)

更新码云上配置,服务动态实时更新数据需要:Bus数据总线+RocketMQ消息中间件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值