SpringCloud学习

SpringCloud 学习笔记

前言

什么是微服务

将软件的各个功能细分为众多微小的服务,不同服务之间按照某种规范(http协议)通过接口进行通信,不同服务之间可以使用不同的编程语言,不同的存储方式,提高了开发效率,但也提高了运维成本。

AKF拆分原则

将一个系统有三个扩展维度:

x轴:也就是水平复制,多运行几个实例,成功集群加负载均衡的模式。(整体复制)

y轴:数据分区,在不同地方使用相互独立的集群,使用不同的数据分区。(同样是复制,但是数据存在不同的地方)

z周:微服务拆分,将一个整体拆分成不同的业务(不同的功能之间相互独立,通过接口通信,然后在服务的基础上进行水平复制或者数据分区的扩展)

SpringCloud和Dobbo

Springcloud接口规范比较松散,采用http协议,编程难度较小,带宽占用较大。Dubbo采用二进制传输,性能更好,难度也更大

前后端分离

前端和后端只通过JSON格式的数据进行交流,前端可以在不同设备上进行渲染的同时,使用同样的数据

无状态服务

请求的url与url的上下文无关

SpringCloud 作用

将服务拆分为微服务后,会有很多微小的项目,SpringCloud用来统一管理这些项目

SpringCloude第一代SpringCloud第二代
网关Spring Cloud ZuulSpringCloud GateWay
注册中心Eureka,Consul,ZooKeeper阿里 Nacos,拍拍货Radar
配置中心SpringCloud Config阿里Nacos,携程Apoilo,随行付Config keeper
负载均衡RibbonSpring Cloud LoadBalancer
熔断器Hystrix阿里sentinel

版本说明

按照伦敦地铁站名称首字母大写排序
Build-xxx 开发版
M 里程碑版,功能大致完成后成为候选版
RC 候选发布版,错误更正后成为正式版
SR 正式发布版,经过全面测试后变为稳定版
GA 稳定版

2.1.0 希腊字母
2:主版本号,功能架构有较大变化时更改
1:次版本号,有部分内容更改时的版本号
0:小变动
base 设计阶段
alpha 初级版本 bug较多
belta 修复了严重的bug
Gamma 修复了绝大部分bug
release 代表最终版

注册中心Eureka

注册中心的作用

注册中心的作用是服务注册和服务发现

服务注册:将服务的名称和地址添加到注册中心(张三把名字告诉我,我把他的名字和电话号码记在通讯录中)

服务发现:根据服务名称找到服务地址,并进行调用(根据张三的名字在通讯录中查找到电话号码并拨打电话)

服务提供者将可以使用的服务注册到注册中心,并与注册中心建立心跳

服务使用者从注册中心获取服务列表,得知远程调用的途径

服务使用者对服务提供者进行远程调用

常见的注册中心

image-20220117014949750

创建eureka注册中心

记得别写成euraka了

创建父工程

创建一个maven项目,直接进入

配置父工程的pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xxxx</groupId>
    <artifactId>euraka-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>euraka-server</module>
        <module>eureka-server02</module>
    </modules>

    <properties>
        <maven.compiler.source>13</maven.compiler.source>
        <maven.compiler.target>13</maven.compiler.target>
    </properties>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.2.4.RELEASE</version>
    </parent>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    </dependencies>
    </dependencyManagement>
</project>

这里使用

springboot 2.2.4.RELEASE(parent工程)

spring-cloud Hoxton.SR1

创建子项目eureka-server

创建一个new module,创建maven工程,选择maven-archetype-quickstart

配置pom:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <groupId>com.xxxx</groupId>
  <artifactId>euraka-server</artifactId>
  <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>com.xxxx</groupId>
        <artifactId>euraka-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
   <modelVersion>4.0.0</modelVersion>
  <dependencies>
<!--    neiflix euraka server-->
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-server</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-test</artifactId>
          <scope>test</scope>
          <exclusions>
              <exclusion>
                  <groupId>org.junit.vintage</groupId>
                  <artifactId>junit-vintage-engine</artifactId>
              </exclusion>
          </exclusions>
      </dependency>
  </dependencies>
</project>

删除无用代码,然后进行配置

标签申明父工程

调整项目结构,使其和springboot项目一致,然后创建启动类:

package com.xxxx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer//以注册中心的方式启动
@SpringBootApplication
public class EurakaServerApplication {
    public static void main(String[] args)
    {
        SpringApplication.run(EurakaServerApplication.class);
    }
}

在创建的resources目录下,创建yml配置文件:

server:
  port: 8761

spring:
  application:
    name: euraka-server

eureka:
  instance:
    hostname: localhost #主机名
  client:
    register-with-eureka: false #是否把自己注册到注册中心,默认true,单节点需要关闭,否则报错refuse:connect
    fetch-registry: false #从注册中心获取配置
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

打开http://localhost:8761/看到eureka的界面代表创建成功

至此单节点配置中心创建完成

高可用(多结点)配置

多节点配置中心只需要在父工程下创建一个一模一样的eureka项目即可,只是yml有所区别

server:
  port: 8761

spring:
  application:
    name: euraka-server

eureka:
  instance:
    hostname: eureka01 #主机名
  client:
    service-url:
      defaultZone: http://localhost:8762/eureka/
server:
  port: 8762

spring:
  application:
    name: euraka-server

eureka:
  instance:
    hostname: eureka02 #主机名
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

因为是多节点配置中心,所以需要在启动时将自己注册进去,但是其实是相互注册的过程,子项目直接通过url相互注册,形成闭环,也就形成了一个集群,这里使用不同的端口以及使用localhost是因为这两个集群在同一个机器上,如果在不同机器上,localhost即可改成局域网,端口可以不用改。

打开http://localhost:8761/和http://localhost:8762/都能看到两个注册中心已经被注册

使用ip名称

在instant下面加上:

prefer-ip-address: true #使用ip显示服务名
instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号

搭建服务提供者(service-provider)

创建一个spring项目,pom与注册中心的pom类似,只是需要将server改成client

spring-cloud-starter-netflix-eureka-client

完整的pom如下:(整合了mybatis和swagger)

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xxxx</groupId>
  <artifactId>service-provider</artifactId>
  <parent>
    <artifactId>euraka-demo</artifactId>
    <groupId>com.xxxx</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <dependencies>
    <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.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.22</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
      
      
      
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>io.github.swagger2markup</groupId>
      <artifactId>swagger2markup</artifactId>
      <version>1.3.3</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-test</artifactId>
    </dependency>
      
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
    </dependency>
      <dependency>
          <groupId>org.apache.poi</groupId>
          <artifactId>poi</artifactId>
          <version>4.0.1</version>
      </dependency>
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>4.0.1</version>
    </dependency>
      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>2.0.6</version>
      </dependency>
    <!--MyBatis配置-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.2.0</version>
    </dependency>
    <!--MySQL数据库配置-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.microsoft.sqlserver</groupId>
      <artifactId>mssql-jdbc</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
      <version>5.1.41</version>
    </dependency>
  </dependencies>
</project>

所谓服务提供者就是提供各种接口供消费者调用,所以可以直接按照一个springboot 项目来编写,也就是编写controller层,service层,mapper层,提供能使用的接口即可。

启动类:

package com.xxxx;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

//因为配置了Eureka-client所以默认配置了@EnableEurekaClient
@SpringBootApplication
@MapperScan("com.xxxx.mapper")//扫描mapper层,否则会找不到
public class ServiceProviderApplication {
    public static void main(String[] args)
    {
        SpringApplication.run(ServiceProviderApplication.class);
    }
}

补充:RestTemplate用法

带参的get请求
    public static void main(String[] args) {
        String url = "https://dev.citconpay.com/payment/pay";
        // 请求头
        HttpHeaders requestHeaders = new HttpHeaders();
        // OAuth 2.0认证
        requestHeaders.add("Authorization", "5753BC63A2FAC434FFD2");
        //Param 封装url
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)//将参数放到url里面
                .queryParam("amount", "1")
                .queryParam("currency", "USD")
                .queryParam("vendor", "generic")
                .queryParam("reference", "83847328742384247832");
        //HttpEntity
        HttpEntity<MultiValueMap> requestEntity = new HttpEntity<MultiValueMap>(requestHeaders);
        //get
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.exchange(builder.build().encode().toUri(), HttpMethod.GET, requestEntity, String.class);
        System.out.println(responseEntity.getBody());
    }
RestTemplate带参的post请求
    public static void main(String[] args) {
        String url = "https://dev.citconpay.com/payment/pay";
        // 请求头
        HttpHeaders requestHeaders = new HttpHeaders();
        // OAuth 2.0认证
        requestHeaders.add("Authorization", "5753BC63A2FAC434FFD2");
        //body
        MultiValueMap<String, Object> body= new LinkedMultiValueMap<>();//参数放到请求体
        body.add("amount", "1");
        body.add("currency", "USD");
        body.add("vendor", "generic");
        body.add("reference", "83847328742384247832");
        //HttpEntity
        HttpEntity<MultiValueMap> requestEntity = new HttpEntity<MultiValueMap>(body,requestHeaders);
        //get
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
        System.out.println(responseEntity.getBody());
    }
返回一个实体类的列表
private List<Product> selectProductListByDiscoveryClient(){
        StringBuffer sb=null;
        //获取服务列表
        List<String> serviceIds=discoveryClient.getServices();
        if(CollectionUtils.isEmpty(serviceIds))//集合工具类的一个方法
            return null;
        //从service-provider获取服务列表
        List<ServiceInstance> serviceInstances=discoveryClient.getInstances("service-provider");
        ServiceInstance si=serviceInstances.get(0);
        sb=new StringBuffer();
        sb.append("http://"+si.getHost()+":"+si.getPort()+"/product/list");
        //ResponseEntity 返回的数据
        ResponseEntity<List<Product>> response=restTemplate.exchange(//封装成这个类的列表
           sb.toString(),
           HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>(){}
        );
        return response.getBody();
    }

创建服务消费者(service-provider)——DiscoveryClient

创建一个springboot项目

服务提供者和服务消费者相对于注册中心都是client,所以pom文件一致,而yml略有区别

yml:

server:
  port: 9090

spring:
  application:
    name: server-consumer

eureka:
  client:
    register-with-eureka: false #这里创建的是一个纯粹的消费者,所以这里不将其注册到注册中心
    registry-fetch-interval-seconds: 10 #拉取服务列表的时间间隔,默认30s
    service-url:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8761/eureka/

##注册中心
#eureka:
#  instance:
#    prefer-ip-address: true #使用ip显示服务名
#    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
#  client:
#    service-url:   #注册中心的url都写上
#      defaultZone: http://localhost:8761/eureka/,http://localhost:8761/eureka/

这里使用的是纯粹的消费者,不提供任何服务,因而不需要把自己注册进注册中心,如果提供服务则使用下面那个配置

编写业务代码
controller
package com.xxxx.controller;

import com.xxxx.bean.Order;
import com.xxxx.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController//@controller+@responsebody
@RequestMapping("/order")
public class OrderController {
    @Resource
    private OrderService orderService;

    @GetMapping("/{id}")//路径参数id
    public Order selectOrderById(@PathVariable("id") int id)//获取路径参数id
    {
        return orderService.selectOrderById(id);
    }
}

这里将参数id放入url,可以用这种写法

bean

product

package com.xxxx.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data   //配置get和set
@NoArgsConstructor  //配置没有参数的构造方法
@AllArgsConstructor //配置包含全部参数的构造方法
public class Product implements Serializable {//序列化,方便io存储
    private int id;
    private String productName;
    private int productNum;
    private Double productPrice;
}

order

package com.xxxx.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {
    private int id;//主键
    private String orderNo;//请求编号
    private String orderAddress;//请求位置(中国)
    private Double totalPrice;//总价格
    private List<Product> productList;//商品列表
}

order表示一次请求的信息

service
package com.xxxx.service;

import com.xxxx.bean.Order;

public interface OrderService {
    Order selectOrderById(int id);//根据主键查询id
}
serviceImpl
package com.xxxx.serviceImpl;

import com.xxxx.bean.Order;
import com.xxxx.bean.Product;
import com.xxxx.service.OrderService;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.lang.reflect.ParameterizedType;
import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {
    @Resource
    RestTemplate restTemplate;
    @Resource
    DiscoveryClient discoveryClient;

    @Override
    public Order selectOrderById(int id) {
        return new Order(id,"order-001","中国",31994D,selectProductListByDiscoveryClient());
    }

    private List<Product> selectProductListByDiscoveryClient(){
        StringBuffer sb=null;

        //获取所有注册到注册中心的服务列表
        List<String> serviceIds=discoveryClient.getServices();
        if(CollectionUtils.isEmpty(serviceIds))//集合工具类的一个方法
            return null;
        //根据名称为service-provider提供的服务列表
        List<ServiceInstance> serviceInstances=discoveryClient.getInstances("service-provider");
        ServiceInstance si=serviceInstances.get(0);
        sb=new StringBuffer();
        sb.append("http://"+si.getHost()+":"+si.getPort()+"/product/list");//远程调用
        //ResponseEntity 返回的数据
        ResponseEntity<List<Product>> response=restTemplate.exchange(//封装成这个类的列表
           sb.toString(),
           HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>(){}
        );
        return response.getBody();
    }
}

返回的信息包括主键(就直接返回了……),地区(中国),总价格(这里没有计算),商品列表(需要远程调用来获取)

远程调用时:

StringBuffer sb:用于设置路由,线程安全的字符串处理类

List serviceIds :获取服务名称列表,判断是否为空

List serviceInstances:根据服务名称获取提供的服务信息,但其实只是为了获取主机名和端口号

ServiceInstance si:获取主机名和端口号

ResponseEntity<List> response:获取返回的数据,数据在body中

启动类
package com.xxxx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

//默认开启EnableEurekaClient 以注册中心使用者启动
@SpringBootApplication
public class ServiceConsumerApplication
{
    @Bean//产生一个一个bean并交给spring容器管理,要用的时候采用依赖注入的方式使用
    public  RestTemplate restTemplate(){
        return new RestTemplate();
    }
    public static void main( String[] args )
    {
        SpringApplication.run(ServiceConsumerApplication.class,args);
    }
}

@Bean注解可以向spring提供一个实体类,方便后续注入(其实new一个也可以,这样可能性能更好)

创建服务消费者(service-provider)——loadBalancerClient

在serviceImpl中修改远程调用服务的方式

package com.xxxx.serviceImpl;

import com.xxxx.bean.Order;
import com.xxxx.bean.Product;
import com.xxxx.service.OrderService;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.lang.reflect.ParameterizedType;
import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {
    @Resource
    RestTemplate restTemplate;
    @Resource
    DiscoveryClient discoveryClient;
    @Resource
    LoadBalancerClient loadBalancerClient;//cloud的负载均衡器

    @Override
    public Order selectOrderById(int id) {
        //return new Order(id,"order-001","中国",31994D,selectProductListByDiscoveryClient());
        return new Order(id,"order-001","中国",31994D,selectProductListByLoadBalancerClient());
    }

    private List<Product> selectProductListByDiscoveryClient(){
        StringBuffer sb=null;

        //获取所有注册到注册中心的服务列表
        List<String> serviceIds=discoveryClient.getServices();

        if(CollectionUtils.isEmpty(serviceIds))//集合工具类的一个方法
            return null;
        //根据名称为service-provider提供的服务列表
        List<ServiceInstance> serviceInstances=discoveryClient.getInstances("service-provider");
        ServiceInstance si=serviceInstances.get(0);
        sb=new StringBuffer();
        sb.append("http://"+si.getHost()+":"+si.getPort()+"/product/list");//远程调用
        //ResponseEntity 返回的数据
        ResponseEntity<List<Product>> response=restTemplate.exchange(//封装成这个类的列表
           sb.toString(),
           HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>(){}
        );
        return response.getBody();
    }
    private List<Product> selectProductListByLoadBalancerClient(){//使用负载均衡器
        StringBuffer sb=null;
        //根据服务名称获取服务
        ServiceInstance si=loadBalancerClient.choose("service-provider");//获取相关信息
        if(null==si)
            return null;
        sb=new StringBuffer();
        sb.append("http://"+si.getHost()+":"+si.getPort()+"/product/list");
        //ResponseEntity 返回的数据
        ResponseEntity<List<Product>> response=restTemplate.exchange(//封装成这个类的列表
                sb.toString(),
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>(){}
        );
        return response.getBody();
    }
}

LoadBalancerClient loadBalancerClient :使用cloud(ribbon)的负载均衡器,使得代码更加简洁,性能也更好

直接用choose函数获取指定服务的信息,然后拼接url进行调用

创建服务消费者(service-provider)——@LoadBalanced

启动类:

package com.xxxx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

//默认开启EnableEurekaClient 以注册中心使用者启动
@SpringBootApplication
public class ServiceConsumerApplication
{
    @Bean//产生一个一个bean并交给spring容器管理,要用的时候采用依赖注入的方式使用
    @LoadBalanced//负责均衡注解,让RestTemplate拥有负载均衡的能力,但是也失去了原生的能力
    public  RestTemplate restTemplate(){
        return new RestTemplate();
    }
    public static void main( String[] args )
    {
        SpringApplication.run(ServiceConsumerApplication.class,args);
    }
}

在给spring容器中添加RestTemplate类时,加上@LoadBalanced注解,使其获得负载均衡的能力

private List<Product> selectProductListByLoadBalancerAnnotation(){
        ResponseEntity<List<Product>> response=restTemplate.exchange(
                "http://service-provider/product/list",
                HttpMethod.GET,
                null,//没有参数
                new ParameterizedTypeReference<List<Product>>() {}
        );
        return response.getBody();
    }

然后再impl类中,代码也会变得极为简单。加上注解后RestTemplate类的方法被重写,url的第一项应当被设置为服务的名称(serviceId),后面接上请求的路由,即可直接调用服务。

加上注解后,原来的url功能发生了改变,因为前两种方法不能用这里spring容器中的RestTemplate类对象,可以使用自己new出来的对象。

image-20220111193838094

CAP原则

C:Consistency 一致性,指多个数据源时,请求返回的数据应当是一致的

A:Availability 可用性,指快速响应用户的请求

P:Partition-Tolerance 分区容错性,指对数据源进行分布式扩展和部署

CAP原则是这三者不可兼得,只能满足其中的两个优点。大致原因是如果一个应用既满足的分布式P,又要保证数据的一致性,就要在多个数据源之间进行一致性处理,这样势必会花费一定的时间而降低可用性(用户体验)

CA:不采用分布式,一般是小型的单节点应用,后期不会进行扩展,只使用一个数据源也就避免了一致性处理。

CP:需要数据高度一致的大型应用,例如银行转账应用,数据一致性要求大于用户的体验。

AP:对一致性要求没有那么高的应用,在请求数据的时候不做一致性处理,在后面再做容错处理。(例如秒杀系统,可以先让用户加入购物车,哪怕此时已经没有商品,在结账的时候再给出响应提示)

Eureka安全性保护

微服务默认每30s向注册中心发生一次心跳包来检测微服务是否正常,如果90s都未收到心跳包,并且没有开启安全保护,注册中心就会移除这个微服务,但是有时是因为网络波动而没能及时收到心跳包,这时候删除微服务是不正确的,所以有了安全保护。

Eureka默认开启安全保护,开启安全保护时,如果没能收到心态,注册中心不会将其删除,而实保留下来并给出一行红色的提示信息,提示你检测微服务是否正常运作。

如果向关闭安全性保护,则需要在yml中添加如下配置文件:

eureka:
  server:
    enable-self-preservation: false #关闭安全性保护
    eviction-interval-timer-in-ms: 60000 #每6s清理一次

和之前的合在一起即为:

server:
  port: 8761

spring:
  application:
    name: euraka-server

eureka:
  server:
    enable-self-preservation: false #关闭安全性保护
    eviction-interval-timer-in-ms: 60000 #每6s清理一次
  instance:
    hostname: eureka01 #主机名
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:
      defaultZone: http://localhost:8762/eureka/

此时会提示错误信息:

THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

表示此时安全性检测已经被关闭

优雅停服

所谓优雅停服就是同时终止服务并从注册中心中删除这个服务

在pom中添加依赖:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

actuator用于安全检测,这里用actuator进行优雅停服

在服务提供者(service-provider)的yml中添加如下配置:

#度量健康指标与健康检测
management:
  endpoints:
    web:
      exposure:
        include: shutdown #开启 shutdown 端点访问
  endpoint:
    shutdown:
      enabled: true       #开启shutdown优雅停服

与前面合起来:

server:
  port: 7070

spring:
  application:
    name: service-provider
  datasource:
    name: document
    url: jdbc:mysql://localhost:3306/document
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml  #配置映射文件
  type-aliases-package: com.example.demo.bean #配置实体类

#注册中心
eureka:
  instance:
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:   #注册中心的url都写上
      defaultZone: http://localhost:8761/eureka/,http://localhost:8761/eureka/

#度量健康指标与健康检测
management:
  endpoints:
    web:
      exposure:
        include: shutdown #开启 shutdown 端点访问 ,也可以使用'*',表示开启全部端点
  endpoint:
    shutdown:
      enabled: true       #开启shutdown优雅停服

因为shutdown是一个比较危险的端口,一旦别人知道了你的ip地址和端口就可以通过shutdown来关闭这个服务,因而actuator默认不开启这个端口,默认开始health,也可以使用 * 来开启全部端点。开启后,shutdown仍然是关闭的状态,所以需要在endpoit中显式地开启这个端点。

然后使用例如postman发生如下请求:

http://localhost:7070/actuator/shutdown

请求方法为post,即可实现优雅停服。(localhost可以改为service-provider的ip)

这样即可在注册中心开启安全保护的同时,将停服的微服务从列表中删除。

安全认证(spring-security)

并不是所有用户都能随意拉取服务,所以这里使用spring-security进行拦截

在eureka-server中添加pom配置:

<!--      spring安全认证-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  </dependencies>

添加后,就会对所有请求进行拦截,并会配置上默认的登录界面

我们设置上访问的用户名和密码:

spring:
  #安全认证
  security:
    user:
      name: root
      password: 123456

然后加注册到注册中心时,加上用户名和密码进行访问

整合到一起后为:

server:
  port: 8761

spring:
  application:
    name: euraka-server
  #安全认证
  security:
    user:
      name: root
      password: 123456

eureka:
  instance:
    hostname: eureka01 #主机名
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:
      defaultZone: http://root:123456@localhost:8762/eureka/

8762端口的eureka-server也做同样的修改:

server:
  port: 8762

spring:
  application:
    name: euraka-server
  #安全认证
  security:
    user:
      name: root
      password: 123456

eureka:
  instance:
    hostname: eureka02 #主机名fer-ip-address
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:
      defaultZone: http://root:123456@localhost:8761/eureka/

这样就可以在输入用户名密码后登入到注册中心,但此时会无法拉取服务,这是因素csrf认为PUT,DELETE,POST请求不安全而进行了全面的拦截,因而我们需要编写配置文件,为eureka进行放行:

第一种方式:只放行/eureka/**的url

在com.xxxx目录下创建config包,并编写配置类:

package com.xxxx.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        super.configure(http);//访问eureka控制台和/actuator是的安全控制
        http.csrf().ignoringAntMatchers("/eureka/**");//为所有/eureka/**的请求放行
    }
}
第二种方式:禁用csrf

这样会禁用掉csrf而给eureka放行

package com.xxxx.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception{//只对/eureka/**放行
        super.configure(http);//访问eureka控制台和/actuator是的安全控制
        http.csrf().ignoringAntMatchers("/eureka/**");//为所有/eureka/**的请求放行
    }
//    @Override
//    protected void configure(HttpSecurity http) throws Exception{//禁用csrf
//        //注意,如果直接disable的话会把安全认证也禁用掉
//        http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic();
//    }
}

验证界面也会发生变化

注意,这里的安全验证只限制了访问了eureka的权限,而没有限制访问微服务的权限,如果已经知道了url则可以绕过注册中心进行remote call,这时候需要提供微服务的那一方提供安全验证。

Ribbon负载均衡

负载均衡

Ribbon的作用就是在service consumer调用service provider的服务时,使用负载均衡的方式来选择使用哪个provider的接口。负载均衡的策略有很多,例如随机选择或者选择最小并发量,使得各个结点的压力比较均衡。

负载均衡有两种方式:

集中式负载均衡

使用一个单独的硬件(F5)或者软件(Nigix)作为负载均衡器来选择使用哪个服务提供者

image-20220116195009130

进程内负载均衡

将负载均衡的进程集成到服务消费者中,Ribbon使用这种方式

image-20220116195200786

Ribbon负载均衡策略

轮询策略(默认)

策略对应类名:RoundRobinRule

实现原理:轮流选择多个服务提供者

权重轮询策略

策略对应类名:WeightedResponseTimeRule

实现原理:根据provider的响应时间(收到结果的时间 - 发生请求的时间)设置每个provider的权值,按权重随机选择provider。响应时间和权值定期进行计算。

随机策略

策略对应类名:RandomRule

实现原理:从provider中随机选择一个

最小并发策略

策略对应类名:BestAvailableRule

实现原理:选择请求中并发量最小的provider

重试策略

策略对应类名:RetryRule

实现原理:轮询策略的增强版,在轮询策略选择的服务不可用时不做处理,而重试策略在轮询的服务不可用时会尝试选择下一个的服务

可用性敏感策略

策略对应类名:AvailablilityFilteringRule

实现原理:过滤掉性能差的provider

第一种:过滤掉在Eureka种一直处于链接失败的provider(收不到心跳)

第二种:过滤掉高并发(繁忙)的provider

区域敏感性策略

策略对应类名:ZoneAvoidanceRule

实现原理:

以一个区域作为考察单位,对不可用的区域整个丢弃,从剩下区域中选可用的provider。

一个区域内有一个或者多个实例不可到达或者速度变慢都会降低该区域被选中的权值。

Ribbon案例搭建

新建立一个service-provider02项目代码与service-provider几乎一样,只用修改项目的名称,启动类的名称和端口即可。名称与service-provider一致,作为一个service-provider集群

然后启动这个服务,然后在服务消费者中使用负载均衡器来远程调用服务接口,并打印负载均衡器所选择的路由。

然后不断向localhost:9090/order/3发生请求,得到控制台的打印信息,结果如下:

image-20220116213751704

因为我们没有设置负载均衡选择的策略,所以默认使用的轮询策略,因而会出现两个路由交替出现的情况

设置负载均衡策略

方式一:全局设置

在启动类中使用@Bean向spring容器中添加对应策略的对象,使用这种方式会设置所有远程调用的服务所使用的策略。

    @Bean//设置负载均衡选择的策略,全局生效,所有的服务均使用这个策略
    public RandomRule randomRule()//随机选择法
    {
        return new RandomRule();
    }

image-20220116215844728

方式二:局部设置

在service-comsumer的yml文件中添加配置:

#负载均衡策略设置
service-provider: #服务提供者的名称
  ribbon: #选择这个服务提供者的负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #策略对应的类名

image-20220116215433908

Ribbon点对点直连

点对点直连就是服务消费者绕过注册中心而直接用ribbon和服务提供者连接,一般用于测试阶段,直连时需要去掉和eureka有关的代码而并添加ribbon的依赖:

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

完整的pom文件为:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xxxx</groupId>
    <artifactId>service-consumer-test</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <artifactId>euraka-demo</artifactId>
        <groupId>com.xxxx</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <dependencies>
<!--        <dependency>-->
<!--            <groupId>org.springframework.cloud</groupId>-->
<!--            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

这里创建了一个新的服务消费者service-provider-test,表示这个时用于测试的

在yml文件中同样需要去掉eureka的配置并添加ribbon的配置

server:
  port: 9091

spring:
  application:
    name: server-consumer

#eureka:
#  client:
#    register-with-eureka: false #这里创建的是一个纯粹的消费者,所以这里不将其注册到注册中心
#    registry-fetch-interval-seconds: 10 #拉取服务列表的时间间隔,默认30s
#    service-url:
#      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/
##注册中心
#eureka:
#  instance:
#    prefer-ip-address: true #使用ip显示服务名
#    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
#  client:
#    service-url:   #注册中心的url都写上
#      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/

#负载均衡策略设置
service-provider: #服务提供者的名称
  ribbon: #选择这个服务提供者的负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    listOfServers: http://localhost:7070,http://localhost:7071 #设置服务提供者的url
ribbon:
  eureka:
    enabled: false #显式地关掉eureka,因为eureka是默认开启的

service-provider 用于指定服务名称

NFLoadBalancerRuleClassName 用于指定负载均衡的算法

listOfServers 用于指定ribbon直连的服务的url

然后启动项目,即可完成服务消费者与服务提供者的直连

注册中心Consul

前言

每个注册中心都可以很好地用来进行开发,市面上大部分注册中心都使用的是eureka,而eureka 2已经停止开发了,而eureka 1还在积极维护当中。学习更多的注册中心是为了在其中一个注册中心不可用时,可以有更多的选择方案,也能更好地应对各个公司的情况。

Consul特性

Raft算法

服务发现

健康检测

Key/Value存储

多数据中心

支持http和dns协议接口

官方提供Web管理界面

Consul角色

eureka在编写和部署时,是作为一个server通过简单的代码编写完成注册中心的创建和部署,运行在servlet容器中

Consul是一个用go语言开发的第三方注册中心,在安装包中有一个exe文件,需要我们用命令去启动

Consul有三种启动命令:

-dev

-client

-server

-dev 为开发模式启动,以单节点的模式开发,原理和eureka一致,功能如下:

  • 服务注册:将服务的名称和地址添加到注册中心(张三把名字告诉我,我把他的名字和电话号码记在通讯录中)

  • 服务发现:根据服务名称找到服务地址,并进行调用(根据张三的名字在通讯录中查找到电话号码并拨打电话)

  • 服务提供者将可以使用的服务注册到注册中心,并与注册中心建立心跳

  • 服务使用者从注册中心获取服务列表,得知远程调用的途径

  • 服务使用者对服务提供者进行远程调用

-client 和 -server 则用于开发完成后集群环境的部署

采用集群的模式是为了提高服务的可用性(高可用),如果是单节点,在这个结点挂掉后,整个服务就停掉了,而如果是集群环境,在其中一个结点挂掉后,仍然有其他可用的结点,不至于让服务宕机。

Consul有两种角色,需要用两种启动命令启动:

client:客户端,无状态,将http和DNS接口请求转发给局域网内的服务端集群。

server:服务端,保存配置信息,高可用集群,每个数据中心的server数量推荐为3个或者5个。

Eureka遵循的是CAP原则中的AP,也就是保证了快速响应和分区容错而放弃了一致性。而Consul遵循的是CP,也就是保证了一致性和分区容错而放弃了快速响应,因而会因为结点数量的增加而导致响应时间增加(为了保证一致性而需要进行同步),所以限制server的数量为3到5个。而为什么不选择4个呢?因为集群有一个规则:一般一个集群里有大于等于一半的结点不可用时,这个集群即为不可用。在选择4个的时候,如果有2个结点宕机,这个集群会变得不可用,而如果有3个结点时,同样会在两个结点宕机时,集群不可用,因而多出来的一个结点没有意义,所以为了节省资源,集群一般采用奇数个结点。如果太少,例如只用1个,会让服务不能高可用,而如果太多,会让响应时间急剧增加,所以我们一般采用3个或者5个结点。

而client负责用于请求的转发,并通过流言协议(LAN GOSSIP)获取server的数据并在client直接进行共享

Consul工作原理

而server和client是注册中心Consul内部的进一步划分,在整体上仍然是起到一个注册中心的作用。工作方式与Eureka基本一致。

Consul安装

Downloads | Consul by HashiCorp 下载Consul的安装包,安装包里面只有一个可执行文件,在同级目录下输入下列命令:

consul agent -dev -client=0.0.0.0

(直接输入cmd可以打开命令行)

agent 意思是代理

-dev 是指单节点的开发模式

-client 为放行的ip,即允许什么ip的服务进行注册

打开http://localhost:8500/ 看到如下界面即为安装成功:

image-20220117145641106

Consul入门案例

创建工程的步骤和Eureka一致,只是配置文件有所区别。

创建父工程

直接创建maven项目,修改pom文件

pom文件如下,引入consul依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>consul-demo</artifactId>
        <groupId>com.xxxx</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>service-provider</artifactId>
    <version>1.0-SNAPSHOT</version>
    <groupId>com.xxxx</groupId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

</project>

创建service-provider

使用手脚架quick-start,修改pom文件

pom文件与Erureka基本一致,只是将eureka依赖换成了consul依赖

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <parent>
    <artifactId>consul-demo</artifactId>
    <groupId>com.xxxx</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <modelVersion>4.0.0</modelVersion>

  <artifactId>service-provider</artifactId>
  <version>1.0-SNAPSHOT</version>
  <groupId>com.xxxx</groupId>

  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.22</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>


    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi</artifactId>
      <version>4.0.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>4.0.1</version>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>2.0.6</version>
    </dependency>
    <!--MyBatis配置-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.2.0</version>
    </dependency>
    <!--MySQL数据库配置-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.microsoft.sqlserver</groupId>
      <artifactId>mssql-jdbc</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
      <version>5.1.41</version>
    </dependency>
    <dependency>
      <groupId>io.swagger</groupId>
      <artifactId>swagger-annotations</artifactId>
      <version>1.5.20</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-core</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-spi</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-spring-web</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>io.github.swagger2markup</groupId>
      <artifactId>swagger2markup</artifactId>
      <version>1.3.3</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-test</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
    </dependency>
  </dependencies>
</project>

调整项目接口,编写业务代码(和eureka完全一致)

修改yml文件:

server:
  port: 7070

spring:
  application:
    name: service-provider #应用名称
  datasource:
    name: document
    url: jdbc:mysql://localhost:3306/document
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  #配置Consul注册中心
  cloud:
    consul: #注册中心的访问地址
      host: localhost
      port: 8500
      #服务提供者信息(本服务),被拉取后能获得这些信息
      discovery:
        register: true                                #是否需要注册
        instance-id: ${spring.application.name}-01    #注册实例的id,必须唯一
        service-name: ${spring.application.name}      #服务名称
        port: ${server.port}                          #服务的端口
        prefer-ip-address: true                       #使用ip地址注册
        ip-address: ${spring.cloud.client.ip-address} #请求服务的ip

mybatis:
  mapper-locations: classpath:mapper/*.xml  #配置映射文件
  type-aliases-package: com.example.demo.bean #配置实体类

#度量健康指标与健康检测
management:
  endpoints:
    web:
      exposure:
        include: '*' #开启 shutdown 端点访问
  endpoint:
    shutdown:
      enabled: true       #开启shutdown优雅停服
创建服务消费者

同样创建quick-start项目,pom文件也与服务创建者一致,yml略加修改:

server:
  port: 9090

spring:
  application:
    name: server-consumer
  #配置Consul注册中心
  cloud:
    consul: #注册中心的访问地址
      host: localhost
      port: 8500
      #服务提供者信息(本服务),被拉取后能获得这些信息
      discovery:
        register: true                                #是否需要注册
        instance-id: ${spring.application.name}-01    #注册实例的id,必须唯一
        service-name: ${spring.application.name}      #服务名称
        port: ${server.port}                          #服务的端口
        prefer-ip-address: true                       #使用ip地址注册
        ip-address: ${spring.cloud.client.ip-address} #请求服务的ip


#负载均衡策略设置
service-provider: #服务提供者的名称
  ribbon: #选择这个服务提供者的负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

相比service-provider,这个目前这个只是一个服务消费者,暂时不提供服务,所以可以先不连接数据库,所以可以在pom和yml中去掉数据库(mybatis)相关的配置,然后服务消费者需要用负载均衡的方式选择服务提供者集群的服务(虽然暂时不是集群)。然后编写业务代码,和Eureka一致,即可完成服务消费者的搭建。

集群环境创建

因为没有多台机器来构建集群,而且8500端口好像是固定的,所以没办法在模拟集群的环境,所以这里只记录笔记。

在开发完成后,要部署到服务器上时,需要部署到Linux系统的多个终端上,每个终端都有一个ip地址,由他们构成一个注册中心的集群,一般需要3个或者5个。

构建步骤如下:

构建环境
  • 在每个终端上上传consul.exe文件的压缩包(或者直接上传文件)

  • 输入命令:mkdir -p /user/local/consul创建一个文件夹(多级目录需要加上-p)

  • 输入命令:yum -y install unzip 安装解压插件

  • 输入命令:unzip <下载的压缩包的名称> -d /user/local/consul/ 将压缩包解压到刚才创建的目录下

  • 输入命令:mkdir -p /user/local/consul/data 创建数据存放的目录

在每个终端上都执行上述操作

启动server

进入到consul目录下,输入命令:

./consul agent -server -bind=192.168.10.101 -client=0.0.0.0 -ui -bootstrap-expect=3 -data-dir=/user/local/consul/data/ -node=server-01

./ 用于启动一个可执行文件

-server 以server的角色启动

-bind 绑定到当前终端(机器)上,ip为当前机器的ip

-client 允许哪些ip注册服务,0.0.0.0表示放行所有ip

-ui 打开ui界面,这样可以在8500端口看到注册中心的ui界面

-bootstrap-expect 最大机器的数量,至少是3台,最多尽量不用超过5台

-data-dir 数据存放的目录,这里设置为刚才创建的目录

-node 结点名称

在三个终端上输入上述命令后,三个server就启动启动起来了,但是还没有构成集群。

启动client

在客户端输入如下命令(windows):

consul agent -client=0.0.0.0 -bind=192.168.10.1 -data-dir=D:\consul\data -node=client-01

参数含义与server一致

构成集群

./consul join <主节点的ip>

在主节点以外的其他结点的终端输入这条命令即可构成一个集群,主节点为我们指定的主节点

在客户端(client)上也输入类似的命令:(Windows)

consul join <主节点ip>

输入:./consul members 可以查看集群中的所有结点

集群环境测试

创建新的服务消费者02

yml修改如下:(只用改一下id,名称和port)

server:
  port: 7071

spring:
  application:
    name: service-provider02 #应用名称
  datasource:
    name: document
    url: jdbc:mysql://localhost:3306/document
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  #配置Consul注册中心
  cloud:
    consul: #注册中心的访问地址
      host: localhost
      port: 8500
      #服务提供者信息(本服务),被拉取后能获得这些信息
      discovery:
        register: true                                #是否需要注册
        instance-id: ${spring.application.name}-02    #注册实例的id,必须唯一
        service-name: ${spring.application.name}      #服务名称
        port: ${server.port}                          #服务的端口
        prefer-ip-address: true                       #使用ip地址注册
        ip-address: ${spring.cloud.client.ip-address} #请求服务的ip

mybatis:
  mapper-locations: classpath:mapper/*.xml  #配置映射文件
  type-aliases-package: com.example.demo.bean #配置实体类



#度量健康指标与健康检测
management:
  endpoints:
    web:
      exposure:
        include: '*' #开启 shutdown 端点访问
  endpoint:
    shutdown:
      enabled: true       #开启shutdown优雅停服

我们发现consul那一块的内容并没有发送变化,这是因为本地使用了client代理,我们使用注册中心时,只需要绑定到client上(例如端口为8500),由client将请求进行收集并转发到server集群,所以部署使用起来十分方便。

Feign

前言

Feign是封装过的Ribbon和RestTemplate,用于在Consumer端进行服务的拉取和调用。但是普通的Feign与Spring并不兼容,而SpringCloud开发了OpenFeign,在Feign的基础上加上了springmvc的注解,可以很容易地与Spring组件整合。

Feign是申明式服务调用组件,可以将远程方法像本地方法那样进行调用,是无感知的http请求。

环境准备

将之前eureka的工程拿过来即可。

Feign入门案例

搭建一个springboot工程,作为service-consumer

添加配置

pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xxxx</groupId>
  <artifactId>service-comsumer</artifactId>
  <version>1.0-SNAPSHOT</version>

  <parent>
    <artifactId>euraka-demo</artifactId>
    <groupId>com.xxxx</groupId>
    <version>1.0-SNAPSHOT</version>
  </parent>

  <dependencies>
<!--    openfeign-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
<!--    eureka的client端-->
    <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>
<!--    lombook-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
        <version>1.18.22</version>
      <scope>provided</scope>
    </dependency>
<!--    测试-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</project>

在之前eureka的基础上添加了OpenFeign的配置:

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

然后编写业务代码,业务代码和之前的类似,只是远程调用的方式有所区别。

编写调用接口

我们在Feign注解的帮助下编写一个ProductService来远程调用:

package com.xxxx.service;

import com.xxxx.bean.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@FeignClient("service-provider")//服务提供者的名称
public interface ProductService {
    @GetMapping("/product/list")//服务提供者的接口url
    List<Product> selectProductList();
}

@FeignClient("service-provider")声明需要远程调用的服务提供者的name,这样Feign就会帮助我们根据这个name从注册中心获取服务提供者的ip和port

@GetMapping("/product/list")声明需要调用的接口的url,结合url和上述的ip和port就能进行远程调用,在我们调用这个函数时,Feign会自动根据url进行远程调用并将结果直接返回给调用这个接口的方法,就好像在调用本地的方法一样,极大的简化了代码的编写。

编写实现类

OrderServiceImpl类:

package com.xxxx.serviceImpl;

import com.xxxx.bean.Order;
import com.xxxx.service.OrderService;
import com.xxxx.service.ProductService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class OrderServiceImpl implements OrderService {
    @Resource
    ProductService productService;

    @Override
    public Order selectOrderById(int id) {
        return new Order(id,"order-001","中国",31994D,
                productService.selectProductList());
    }
}

直接注入ProductService然后进行调用即可

启动类添加注解@EnableFeignClients
package com.xxxx;

import com.netflix.loadbalancer.RandomRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;

//默认开启EnableEurekaClient 以注册中心使用者启动
@SpringBootApplication
@EnableFeignClients
public class ServiceConsumerApplication
{
    @Bean
    public RandomRule randomRule() {return new RandomRule();}

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

*注册中心扩展

我们上述创建的项目均在同一个项目(parent)下面,造成服务提供者和服务消费者在一起的假象,但其实注册中心,服务消费者,服务提供者可以分别在不同的机器上运行,只需要指定连接的注册中心的url即可,也就是在yml文件中需要有这样的一段配置。

eureka:
  instance:
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:   #注册中心的url都写上
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/

我在项目外创建了一个新的结点,但是在启动的时候出现了问题,原因是因为eureka-client和spring-boot-web出现了冲突,需要在pom修改client配置:

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
      <exclusions>
        <exclusion> <!-- 解决和web的冲突问题 -->
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-core</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.sun.jersey.contribs</groupId>
          <artifactId>jersey-apache-client4</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

完整的pom文件为:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.xxxx</groupId>
  <artifactId>service-provider</artifactId>
<!--  parent始终要有-->
  <parent>
    <artifactId>spring-boot-starter-parent</artifactId>
    <groupId>org.springframework.boot</groupId>
    <version>2.2.4.RELEASE</version>
  </parent>
<!--  springcloud依赖-->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
<!--    eureka-client配置 -->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
      <exclusions>
        <exclusion> <!-- 解决和web的冲突问题 -->
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-client</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.sun.jersey</groupId>
          <artifactId>jersey-core</artifactId>
        </exclusion>
        <exclusion>
          <groupId>com.sun.jersey.contribs</groupId>
          <artifactId>jersey-apache-client4</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
      <!--    openfeign-->
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.22</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>io.github.swagger2markup</groupId>
      <artifactId>swagger2markup</artifactId>
      <version>1.3.3</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-test</artifactId>
    </dependency>
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
    </dependency>
      <dependency>
          <groupId>org.apache.poi</groupId>
          <artifactId>poi</artifactId>
          <version>4.0.1</version>
      </dependency>
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
      <version>4.0.1</version>
    </dependency>
      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>2.0.6</version>
      </dependency>
    <!--MyBatis配置-->
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>2.2.0</version>
    </dependency>
    <!--MySQL数据库配置-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>com.microsoft.sqlserver</groupId>
      <artifactId>mssql-jdbc</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
      <version>5.1.41</version>
    </dependency>
  </dependencies>
</project>

因为这个结点是典型的一个可以提供服务也可以消费服务的工程,所以它拥有service-provider和service-consumer的配置文件(但其实他们两个配置文件本来就是一样的)

yml文件:

server:
  port: 7072

spring:
  application:
    name: service-provider-another
  datasource:
    name: document
    url: jdbc:mysql://localhost:3306/document
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml  #配置映射文件
  type-aliases-package: com.example.demo.bean #配置实体类

#注册中心
eureka:
  instance:
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:   #注册中心的url都写上
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/

#度量健康指标与健康检测
management:
  endpoints:
    web:
      exposure:
        include: '*' #开启 shutdown 端点访问
  endpoint:
    shutdown:
      enabled: true       #开启shutdown优雅停服

使用7072端口并使用另一个名称:service-provider-another 这样就能与其他结点进行相互调用。

调用service-provider提供的服务:
@FeignClient("service-provider")
public interface GetProductService {
    @GetMapping("/product/list")
    List<Product> selectProductList();
}

image-20220118234343098

调用成功

老结点调用新节点:
@FeignClient("service-provider-another")
public interface AnotherProductsService {
    @GetMapping("/test/productlist")
    List<Product> GetTestProducts();
}

新节点调用了老结点的service-provider的服务,老结点的service-consumer调用新结点的服务(相当于绕了一圈……)

结果如下:

image-20220118234658449

调用成功,这也构成了微服务架构的一个雏形

Feign负载均衡算法修改

Feign封装了Ribbon,所以修改策略的方式和Ribbon一致

全局

和Ribbon一样,在启动类里注入对应的策略对象即可

@Bean
public RandomRule randomRule() {return new RandomRule();}
局部

和ribbon一样在yml中添加如下配置即可

#负载均衡策略设置
service-provider: #服务提供者的名称
  ribbon: #选择这个服务提供者的负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #d

Feign传递参数

get方法
service-product:

Get方法传递参数其实就是把参数放到路径上

	@GetMapping("/{id}")
    public Product selectProductById(@PathVariable("id") Integer id)
    {
        return productService.selectProductbyID(id);
    }

@GetMapping("/{id}")建立函数和url的映射关系,并指明传递的参数所在的位置和名称,用{}括起来即为传递的参数

@PathVariable("id")获取url中的参数,并和参数建立映射关系,将值注入到遍量中,如果变量名称与路径参数名称的相同则可以不加声明

即为:

	@GetMapping("/{id}")
    public Product selectProductById(@PathVariable Integer id)
    {
        return productService.selectProductbyID(id);
    }

在这个例子中{id}和Integer id都用的是id则,则可以不加那一层映射关系。(最好还是加上)

selectProductbyID为一个提供数据的函数(这里用假数据)

service-consumer:

在ProductService层使用Feign调用接口:

@GetMapping("product/{id}")
    Product selectProductById(@PathVariable("id") Integer id);

和Controller层的逻辑相反,这个是在调用这个方法时,将id填入url中的对应位置,然后根据服务提供者的名称获取对应的ip和port,然后进行远程调用。

在service中提供接口并在实现类中调用ProductService的方法:

	@Override
    public Order selectOrderById(int id) {
//        return new Order(id,"order-001","中国",31994D,
//                productService.selectProductList());
        return new Order(id,"order-001","中国",22788D,
                Arrays.asList(productService.selectProductById(5)));
    }

在实现类中使用假数据并进行返回。

在controller中继续使用:

使用@RestController//@controller+@responsebody

	@GetMapping("/{id}")//路径参数id
    public Order selectOrderById(@PathVariable("id") int id)//获取路径参数id
    {
        return orderService.selectOrderById(id);
    }

测试后获得成功:

image-20220119184243410

Post方法

post传递参数时,参数需要放在请求体中

service-provider

服务提供者就和正常的springboot项目一样提供接口即可:

    @PostMapping("/single")//Post方法,根据主键查询商品
    public Product queryProductById(Integer id)
    {
        return productService.queryProductById(id);
    }

    @PostMapping("/save")//post方法,创建商品
    public Map<Object, Object> createProduct(Product product)
    {
        return productService.createProduct(product);
    }
srevice-consumer

在Feign的接口中添加如下方法:

    @PostMapping("/product/single")//post方法,参数默认就放在请求体中,url起的有些随意……
    Product queryProductById(Integer id);//根据主键查询商品

    @PostMapping("/product/save")//post方法,参数product会默认放在请求体中
    Map<Object, Object> createProduct(Product product);

调用方法时,会根据@FeignClient("service-provider")//服务提供者的名称找到服务提供者的ip和port,然后和 @PostMapping("/product/save")中的url进行拼接,参数例如id,product放入请求体中,然后发送请求进行远程调用,使用起来十分方便。

完整的接口如下:

package com.xxxx.service;

import com.xxxx.bean.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;
import java.util.Map;

@FeignClient("service-provider")//服务提供者的名称
public interface ProductService {
    @GetMapping("/product/list")//服务提供者的接口url
    List<Product> selectProductList();//查询商品列表

    @GetMapping("product/{id}")
    Product selectProductById(@PathVariable("id") Integer id);

    @PostMapping("/product/single")//post方法,参数默认就放在请求体中,url起的有些随意……
    Product queryProductById(Integer id);//根据主键查询商品

    @PostMapping("/product/save")//post方法,参数product会默认放在请求体中
    Map<Object, Object> createProduct(Product product);
}

编写一个新的Controller直接调用即可:

package com.xxxx.controller;

import com.xxxx.bean.Product;
import com.xxxx.service.ProductService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Map;

@RestController
@RequestMapping("/product")
public class ProductController {
    @Resource
    ProductService productService;

    //查询商品
    @RequestMapping(value = "/info",method = RequestMethod.POST)
    public Product queryProductById(Integer id)
    {
        return productService.queryProductById(id);
    }
    //新增商品
    @RequestMapping(value = "/save",method = RequestMethod.POST)
    public Map<Object, Object> createProduct(Product product){
        return productService.createProduct(product);
    }
}

Feign性能调优

Gzip压缩

Gzip可以对文本数据进行压缩,从而提高客户端的响应速度,http协议中,对Gzip的规定如下:

  • 客户端对服务端的请求中带有:Accept-Encoding:gizp,deflate字段,向服务器表示客户端支持的压缩格式(gzip或者deflate),如果不发送该请求,服务端默认不压缩。

  • 服务端在收到请求后,如果发现请求头中含有Accept-Encoding:gizp,deflate字段,并且支持该类型压缩,就会对响应报文压缩之后返回给客户端,并且携带Content-Encoding:gzip消息头,表示响应报文是根据该格式进行压缩的

  • 客户端收到请求的返回值之后,先判断是否含有Content-Encoding消息头,如果有则按照该消息头解压,否则不解压

实际的调用逻辑为:

浏览器–>service-consumer–>service-provider所以会有两个阶段的数据传输作用于service-consumer–>service-provider为局部优化,作用于两个阶段的为全局优化

image-20220119230031628

但是局部优化难以察觉,所以我们直接配置全局的优化方式(有全局为啥要局部

一般浏览器都会支持gzip,可以F12查看网络,如果看到如下:

image-20220119231157326

即Accept-Encoding:gizp,deflate,就代表浏览器支持gzip,并会自动解压,所以我们只需要在service-consumer的配置文件中开启压缩即可:

yml文件中添加:

server:
  port: 9090 #端口
  compression:
    enabled: true #开启压缩
    mime-types: application/json,application/xml,text/html,text/xml,text/plain #压缩类型

enabled: true 开启压缩

mime-types 指出开启压缩的类型,其实我们写的这些都是默认支持的,所以这个可以不写

完整的yml文件:

server:
  port: 9090 #端口
  compression:
    enabled: true #开启压缩
    mime-types: application/json,application/xml,text/html,text/xml,text/plain #压缩类型

spring:
  application:
    name: server-consumer

#eureka:
#  client:
#    register-with-eureka: false #这里创建的是一个纯粹的消费者,所以这里不将其注册到注册中心
#    registry-fetch-interval-seconds: 10 #拉取服务列表的时间间隔,默认30s
#    service-url:
#      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/
#注册中心
eureka:
  instance:
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:   #注册中心的url都写上
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/

#负载均衡策略设置
service-provider: #服务提供者的名称
  ribbon: #选择这个服务提供者的负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #策略对应的类名

测试结果:

image-20220119232013036

在浏览器中看到Content-Encoding:gzip 即代表开启成功

Http连接池

前言

为什么要用http连接池呢?

  • 两条服务器之间http协议是很复杂的一个过程,设计到多个数据包的交换时很耗时间(包括压缩和解压)

  • http连接需要三次握手和四次挥手,开销很大,尤其对于大量较小的http请求。(http2可以处理)

Tip:三次握手和四次握手:http三次握手四次挥手详解 - 一支会记忆的笔 - 博客园 (cnblogs.com)

传统的HttpURLConnect是JDK自带的,并不支持连接池,自己去实现一个连接池需要自己管理各种对象并且操作比较繁琐,最后实现出来的性能也没有别人实现的好。

HttpClient相比传统的HttpURLConnect,对各种参数进行了封装,相当于一个实现好的Http连接池,我们只需要调用即可。提高了吞吐量和开发效率,也能用于提高高并发时的吞吐量。

环境配置

在service-consumer中添加pom配置

(apache的httpclient已经自动在Hoston.SR1配置过了,因而不用配置)

<!--      feign httpclient :http连接池-->
      <dependency>
          <groupId>io.github.openfeign</groupId>
          <artifactId>feign-httpclient</artifactId>
      </dependency>

yml文件:

feign:
  httpclient:
    enabled: true

开启httpclient后就开启了连接池。

使用httpclient后,Get方法中路径参数也会被自动装入到我们自定义的对象中,和Post方法类似(但是Get方法一般是不传参数的,所以没啥用

状态查看(日志文件)

状态查看就是输出程序执行时的各种调试信息,方便我们追踪链路的整个调用过程。也就是输出日志文件。

在service-consumer中添加一个配置文件logback.xml:(写了好久QWQ)

<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="true" scanPeriod="10 seconds">
    <contextName>my_logback</contextName>
    <property name="log.path" value="${catalina.base}/service-consumer/logs"/>
<!--彩色日志-->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH-mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p})
    %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint}%clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!--    日志文件输入格式-->
    <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}%msg%n"/>
<!--    输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>DEBUG</level>
        </filter>
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
<!--    输出到文件-->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--        正在记录的日志文件的路径及文件名-->
        <file>${log.path}/log_debug.log</file>
<!--        日志输出格式-->
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!--    日志记录器的滚动策略 按日期大小记录-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--        日志归档-->
            <fileNamePattern>${log.path}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--        日志保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!--    debug级别-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
<!--    时间滚动输出level为INFO日志-->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--        正在记录的日志文件的路径和文件名-->
        <file>${log.path}/log_info.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--            每日归档日志以及格式-->
            <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
<!--            日志保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
<!--    输出level为warn-->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/log_warn.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--            每日归档日志以及格式-->
            <fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--            日志保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
<!--    时间滚动输出Error日志-->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/log_error.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--            每日归档日志以及格式-->
            <fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--            日志保留天数-->
            <maxHistory>15</maxHistory>
<!--            日志量最大为10G-->
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <logger name="myLog" level="INFO" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root level="DEBUG">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="DEBUG_FILE"/>
        <appender-ref ref="INFO_FILE"/>
        <appender-ref ref="WARN_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>
</configuration>

然后需要启动日志文件,有两种方式:

全局:

在启动类,向spring容器添加myLog对象:

	@Bean
    public Logger.Level getLog(){//日志对象输出
        return Logger.Level.FULL;
        /*
        NONE:无信息
        BASIC:记录基础信息
        HEADERS:在BASIC的基础上记录一些常用信息
        FULL:全部信息
         */
    }

这样对于所有服务都开启日志

局部:

feign:	
  client:
    config:
      service-provider:		#对特定服务开启日志
        loggerLevel: FULL

service-provider指出需要开启日志的服务。

最后的日志文件会保存在:

image-20220123014912482

也可以通过设置catalina.base设置日志的保存位置:

image-20220123015033464

这样就能简单地查看服务调用的情况,后续还有更直观的方法:链路追踪

超时时间

在处理不同的请求时,需要有不同的超时时间,Feign对于所有的请求的超时时间都默认是1s,但是有些耗时的服务可能需要更多的时间,所以我们需要配置它的超时时间。

我们在服务提供者中设置一下,模拟长时间的请求:

	@Override
    public Product selectProductbyID(int id) {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Product(id,"冰箱",1,166D);
    }

Thread.sleep(2000); 让当前线程睡眠两秒

如果不配置超时时间就会报500:Read Timeout

全局

为所有的服务都设置相同的超时时间

ribbon:
  ConnectTimeout: 5000 #请求连接的超时时间 默认为为1s
  ReadTimeout: 5000 #请求处理的超时时间
局部

但实际上我们需要根据不同服务的需要设置不同的超时时间,所以我们在给定服务名称的配置下设置超时时间即可

#指定服务的配置
service-provider: #服务提供者的名称
  ribbon: #选择这个服务提供者的负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡策略对应的类名
    OkToRetryOnAllOperations: true     #对所有超时请求开启重试
    MaxAutoRetries: 2                  #对当前实例的重试次数
    MaxAutoRetriesNextServer: 0        #切换实例的重试次数
    ConnectTimeout: 3000               #请求连接的超时时间 默认为为1s
    ReadTimeout: 3000                  #请求处理的超时时间

这样就可以简单的处理超时问题,后续可以用服务熔点和降级来处理

Hystrix服务容错

前言

Hystrix是netflix公式使用的一款开源产品,可以用于在微服务错综复杂调用关系中,提供服务熔断,服务降级,服务隔离等服务,为某个服务不可用时,提供一个备选方案,避免雪崩效应。

雪崩效应

现实当中服务的调用是错综复杂的,如果一个服务不可用时,可能会造成严重的后果。

当服务运行正常时,是这样:

image-20220123151046889

当服务R出现问题时:

image-20220123151239348

服务R不可用就会导致服务N不可用,服务N不可用就会导致服务H不可用,服务H不可用时,请求服务H的客户端就会阻塞在服务器里面,当有大量客户端请求这个服务H时,所有的客户端都会被阻塞在这里,服务器的线程资源很快就会被耗尽,进而导致服务器崩溃,然后进一步导致整个微服务系统不可用。这一连串的连锁反应就叫雪崩效应。

雪崩效应解决方案

我们无法从源头上杜绝雪崩效应的发生,但是可以设计好容错的逻辑,减少服务之间的强依赖,避免错误的大量传播。

  • 请求缓存:将一个请求和返回值做缓存处理,如果一个请求所得到的值长时间不发生变化,我们可以将这个值放入缓存,请求时直接从缓存中拿到返回值,从而降低服务之间的依赖。
  • 请求合并:将多个请求合并为一次请求,例如将多个根据主键查询的请求,合并为一次查询列表的请求。
  • 服务隔离:限制每个分布式服务的资源,当一个服务的请求个数过多时,不会占用其他服务的资源,这样可以当一个服务出现雪崩时,大量线程阻塞,其他服务也能够正常运行,防止雪崩的扩散,同时也能保障请求少的服务能够快速响应(不然我偶尔请求一次还让我等那么久也太不公平了)。
  • 服务熔断:牺牲局部的服务,保全整个系统的稳定性,当一个服务出现雪崩时,可以关闭该服务,防止雪崩扩散。
  • 服务降级:服务熔断后,返回一个能快速得到的缺省值,避免客户端那么没有响应。

环境搭建

创建两个eureka注册中心

product-service :提供查询商品列表,根据主键查询商品,根据多个主键查询商品

order-service-rest:调用product-service中的服务,同时自身也可能提供一些服务

模拟高并发环境

使用JMeter来模拟高并发的环境。

下载第三方的一个依赖包Apache JMeter - Download Apache JMeter

image-20220124213525991

解压后,打开bin目录下的jmeter.bat,我们发现里面是英文,不方便我们使用,所以我们需要修改它的配置文件。

打开同级目录下的jmeter.properties,进行以下修改:

image-20220124213915237

把语言改成zh_CN,即改成中文

language=zh_CN

image-20220124214106498

sampleresult.default.encoding=UTF-8

把编码字符集改成UTF-8

然后运行jmeter.bat

文件->添加->线程->线程组

image-20220124214523059

线程组->取样器->http请求

image-20220124214630323

http请求->添加->监听器->查看结果树

image-20220124214656378

设置线程组,将请求设置为50✖50,也就是2500个请求

image-20220124214950206

设置请求的url(ip,端口,路径)

image-20220124215135921

然后点击运行即可模拟出一个高并发的场景

请求缓存

前言

Hystrix自带缓存有以下问题:

缓存保存在本地,无法和集群同步

不支持第三方缓存,例如redis,MemCache

下面使用spring的缓存方案,Nosql用redis来实现,redis使用5.0.7

redis其实是一个用于第三方缓存的服务器,可以通过http协议向这个服务器发送请求,服务器提供缓存服务。

下载redis

参考博客:(29条消息) windows下Redis的安装和配置–图文教程_Zepal-CSDN博客_redis安装配置

添加pom依赖
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>
添加yml配置

在服务消费者的yml文件添加如下依赖:

spring:
  application:
    name: order-service-rest
  redis:
    timeout: 10000         #超时时间:10s
    host: localhost        #redis服务器地址
    port: 6379             #redis服务器端口
    password: 123456         #redis服务器密码
    database: 0            #选择哪个库
    lettuce:
      pool:
        max-active: 1024   #最大连接数
        max-wait: 10000    #最大连接阻塞等待时间,单位ms,默认-1
        max-idle: 200      #最大空闲连接,默认8
        min-idle: 5        #最小空闲连接 ,默认0

完整的yml文件:

server:
  port: 9090 #端口
  compression:
    enabled: true #开启压缩
    #mime-types: application/json,application/xml,text/html,text/xml,text/plain #压缩类型
  tomcat:
    max-threads: 10 #设置最大线程数,为了模拟高并发才加上的,实际使用不用设置

spring:
  application:
    name: order-service-rest
  redis:
    timeout: 10000         #超时时间:10s
    host: localhost        #redis服务器地址
    port: 6379             #redis服务器端口
    password: 123456         #redis服务器密码
    database: 0            #选择哪个库
    lettuce:
      pool:
        max-active: 1024   #最大连接数
        max-wait: 10000    #最大连接阻塞等待时间,单位ms,默认-1
        max-idle: 200      #最大空闲连接,默认8
        min-idle: 5        #最小空闲连接 ,默认0

#注册中心
eureka:
  instance:
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:   #注册中心的url都写上
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/

#指定服务的配置
service-provider: #服务提供者的名称
  ribbon: #选择这个服务提供者的负载均衡策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡策略对应的类名
    OkToRetryOnAllOperations: true     #对所有超时请求开启重试
    MaxAutoRetries: 2                  #对当前实例的重试次数
    MaxAutoRetriesNextServer: 0        #切换实例的重试次数
    ConnectTimeout: 3000               #请求连接的超时时间 默认为为1s
    ReadTimeout: 3000                  #请求处理的超时时间

feign:
  httpclient: #开启httpclient,http连接池
    enabled: true

#ribbon:
#  ConnectTimeout: 5000 #请求连接的超时时间 默认为为1s
#  ReadTimeout: 5000 #请求处理的超时时间

添加配置类

直接使用jdk自带的序列化对空间的消耗很大,是json的5倍,所以我们编写一个配置类,重写序列化方法

package com.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
    {
        RedisTemplate<String, Object> template=new RedisTemplate<>();
        //为String 类型的key设置序列化器
        template.setKeySerializer(new StringRedisSerializer());
        //为String 类型的value设置序列化器
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        //为Hash类型key设置序列化器
        template.setHashKeySerializer(new StringRedisSerializer());
        //为hash类型设置序列化器
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
    //重写Cache序列化
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate)
    {
        RedisConnectionFactory connectionFactory;
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))   //设置过期时间
                //key和value的序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getKeySerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
        return new RedisCacheManager(redisCacheWriter,redisCacheConfiguration);

    }
}
启动类

启动类上加上这个注解,表示开启缓存服务

@EnableCaching //开启缓存
业务实现类
package com.demo.serviceImpl;

import com.demo.bean.Product;
import com.demo.service.ProductService;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Service
public class ProductServiceImpl implements ProductService {
    @Resource
    RestTemplate restTemplate;

    @Cacheable(cacheNames = "orderService:product:list")
    @Override
    public List<Product> selectProductList() {
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }
        ).getBody();
    }

    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        StringBuffer sb=new StringBuffer();
        ids.forEach(id->sb.append("id="+id+"&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?"+sb.toString(), ArrayList.class);
    }

    @Cacheable(cacheNames = "orderService:product:single",key = "#id") //id即为下面传入的参数
    @Override
    public Product selectProductById(Integer id) {
        return restTemplate.getForObject("http://product-service/product/"+id,Product.class);
    }
}

@Cacheable(cacheNames = “orderService:product:list”)

@Cacheable(cacheNames = “orderService:product:single”,key = “#id”) //id即为下面传入的参数

Redis的Cache是一个键值对,cacheNames和key共同决定键值对的名称,:是一个序列化的分隔符,:后面的名称相当于是前面名称的子属性,后面的key会作为最后一个子属性,在调用这个方法前,会先根据键值对的名称在缓存中查找是否已经有存在的记录,如果有,则返回缓存中对应的值,如果没有,则调用方法进行远程调用,调用结束后会创建键值对将返回值值存入缓存。

调用后:

image-20220125014117059

可见缓存中已经有了对应的值

因为我们模拟了高并发环境,调用服务时会睡眠2s,所以第一次调用服务会比较慢(大于2s),后面调用起来就会很慢(小于1s),这是因为第一次将值存入了缓存,后面的值直接从缓存中取得。

这里演示的是GET方法的请求,但其实用POST方法也可以缓存,因为注解中的key来自调用时传入的参数,至于使用POST还是GET取决于方法体,与缓存无关。

使用缓存可以减少一些不必的远程调用,从而提高系统的整体性能。

请求合并

请求合并就是将一堆请求收集起来,等到达了某个阈值后批量处理,好处是提高了系统的吞吐量,减少了线程阻塞而大量消耗的线程资源,缺点是请求会有延迟,因为要等到达到阈值后才能响应。相当于为请求开辟了一个缓冲区。

请求合并需要一定的等待时间,如果系统要求请求要快速响应,则不能用请求合并。如果请求本身本身延迟就很高,那么请求合并所等待的时间就显得微不足道了(例如每10ms将这个时间端的请求批量处理,会有一定的延迟,但是如果请求本身就要好几秒,等待的时间就不那么重要了),另外高并发也是请求合并应用的重要场景。

服务消费者添加pom依赖:
<!--    hystrix-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
添加注解
package com.demo.serviceImpl;

import com.demo.bean.Product;
import com.demo.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;

@Service
public class ProductServiceImpl implements ProductService {
    @Resource
    RestTemplate restTemplate;

    @Cacheable(cacheNames = "orderService:product:list")
    @Override
    public List<Product> selectProductList() {
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }
        ).getBody();
    }

    @HystrixCommand //声明是一个服务容错的方法
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        System.out.println("----selectProductListByIds---被调用----");//看看请求合并是否生效
        StringBuffer sb=new StringBuffer();
        ids.forEach(id->sb.append("id="+id+"&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?"+sb.toString(), ArrayList.class);
    }

    @HystrixCollapser(batchMethod = "selectProductListByIds",//合并后用于批处理的方法
            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,//请求方式
            collapserProperties = {//批处理的时间阈值和数量阈值,两个只要有一个达到就会进行一次批处理
                //间隔多久进行一次批处理,默认是10ms
                @HystrixProperty(name = "timerDelayInMilliseconds",value = "20") ,
                //批处理中允许的最大请求数
                @HystrixProperty(name = "maxRequestsInBatch",value = "200")
            }
    )
    @Cacheable(cacheNames = "orderService:product:single",key = "#id") //id即为下面传入的参数
    @Override//Future是异步,将批量处理后得到的结果与请求一一对应
    public Future<Product> selectProductById(Integer id) {
        System.out.println("----selectProductById---被调用----");//看看请求合并是否生效
        return null;//请求会进行合并后调用上面的方法,所以实际上没有使用这个方法,直接返回null即可
    }
}

@HystrixCommand` 用于声明这是一个用于请求合并(服务容错)的方法

@HystrixCollapser(batchMethod = “selectProductListByIds”,//合并后用于批处理的方法
scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,//请求方式
collapserProperties = {//批处理的时间阈值和数量阈值,两个只要有一个达到就会进行一次批处理
//间隔多久进行一次批处理,默认是10ms
@HystrixProperty(name = “timerDelayInMilliseconds”,value = “20”) ,
//批处理中允许的最大请求数
@HystrixProperty(name = “maxRequestsInBatch”,value = “200”)
}
)

用于申明需要合并的方法,其中的参数:

batchMethod 用于申明用哪个方法将进行合并

scope 这个用于指定请求方式

collapserProperties 用于添加配置

@HystrixProperty(name = “timerDelayInMilliseconds”,value = “20”) 用于设置最大间隔时间

@HystrixProperty(name = “maxRequestsInBatch”,value = “200”) 用于设置最多等待线程

因为这是一个异步的请求,所以需要用到Future进行异步处理

在启动类中添加注解
package com.demo;

import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

//默认开启EnableEurekaClient 以注册中心使用者启动
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker //开启熔断器,也可以使用EnableHystrix
//@EnableCaching //开启缓存注解
public class OrderServiceApplication
{
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
//    @Bean
//    public RandomRule randomRule() {return new RandomRule();}
    @Bean
    public Logger.Level getLog(){//日志对象输出
        return Logger.Level.NONE;
        /*
        NONE:无信息
        BASIC:记录基础信息
        HEADERS:在BASIC的基础上记录一些常用信息
        FULL:全部信息
         */
    }
    public static void main( String[] args )
    {
        SpringApplication.run(OrderServiceApplication.class,args);
    }
}

@EnableCircuitBreaker 开启熔断器,也可以使用EnableHystrix

模拟多个请求
	@Override
    public Order searchOrderById(int id) {
        Future<Product> p1=productService.selectProductById(1);
        Future<Product> p2=productService.selectProductById(2);
        Future<Product> p3=productService.selectProductById(3);
        Future<Product> p4=productService.selectProductById(4);
        Future<Product> p5=productService.selectProductById(5);
        try {
            System.out.println(p1.get());//get方法拿到数据,返回值是hashmap
            System.out.println(p2.get());
            System.out.println(p3.get());
            System.out.println(p4.get());
            System.out.println(p5.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();//重新设置中断信号
            e.printStackTrace();
        }
        return new Order(id,"order-003","中国",29000D,null);
    }

一次调用五个请求,判断请求合并是否成功。

线程隔离

前言

线程隔离分为信号量隔离和线程池隔离,线程池隔离适用于绝大多数情况,但是如果隔离的实例太多,进程上下文之间的切换较大,此时可以采用信号量隔离,而信号量隔离是设置一个互斥使用的信号量,如果由信号量则取走一个信号量并在线程运行完后归还,如果没有信号量则将线程阻塞,并设置一定的超时时间。相当于调用方的线程进入线程池后,再进入服务独有的线程池,因而提高服务的线程和调用方的线程是两个不同的线程,因而是异步的方法,这这两个线程切换时,可以做超时处理。

线程池隔离

image-20220125181820684

一开始这两个接口使用同一个线程池(从他们前缀可以看出,并且使用相同的计数规则)

在调用服务的业务实现类中添加注解,将查询商品列表的服务和根据主键查询商品的服务所使用的线程池分开,为这俩个服务分配可以使用的线程,例如一共有10个线程,为查询商品列表的分配6个线程,为根据主键查询商品分配3个线程,还有1个线程用于调用其他服务,通过这样合理地分配资源,防止服务之间相互干扰,从而达到服务隔离地目的。

业务实现类
package com.demo.serviceImpl;

import com.demo.bean.Product;
import com.demo.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;

@Service
public class ProductServiceImpl implements ProductService {
    @Resource
    RestTemplate restTemplate;

    @HystrixCommand(groupKey = "order-productService-listPool",//服务名称,使用相同名称的服务使用同一个线程池
            commandKey = "selectProductList", //接口名称,默认为方法名
            threadPoolKey = "order-productService-listPool",//线程池名称,相同名称的服务使用同一个线程池
            commandProperties = {
                    //超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000"
                    )
            },
            threadPoolProperties = {
                    //线程池大小(设置的总数量为10)
                    @HystrixProperty(name = "coreSize",value = "6"),
                    //队列等待的阈值(最大队列长度),默认为-1
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    //线程存活时间,默认1min
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    //超出队列等待阈值执行拒绝策略,value和上面的maxQueueSize的value保持一致
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
            }, fallbackMethod = "selectProductListFallback"  //使用托底方法返回的数据
    )
    @Cacheable(cacheNames = "orderService:product:list")
    @Override
    public List<Product> selectProductList() {
        System.out.println(Thread.currentThread().getName()+"调用---ProductService---selectProductList---");
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }
        ).getBody();
    }

    //托底方法,参数和返回值类型要与原方法一致
    private List<Product> selectProductListFallback(){
        System.out.println("---托底数据的方法:selectProductListFallback---");
        return Arrays.asList(
                new Product(1,"托底数据-华为手机",1,5800D),
                new Product(2,"托底数据-联想笔记本",1,6888D),
                new Product(3,"托底数据-小米平板",5,2020D)
        );

    }

    //@HystrixCommand //声明是一个服务容错的方法
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        System.out.println("----selectProductListByIds---被调用----");//看看请求合并是否生效
        StringBuffer sb=new StringBuffer();
        ids.forEach(id->sb.append("id="+id+"&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?"+sb.toString(), ArrayList.class);
    }

//    @HystrixCollapser(batchMethod = "selectProductListByIds",//合并后用于批处理的方法
//            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,//请求方式
//            collapserProperties = {//批处理的时间阈值和数量阈值,两个只要有一个达到就会进行一次批处理
//                //间隔多久进行一次批处理,默认是10ms
//                @HystrixProperty(name = "timerDelayInMilliseconds",value = "20") ,
//                //批处理中允许的最大请求数
//                @HystrixProperty(name = "maxRequestsInBatch",value = "200")
//            }
//    )

    @HystrixCommand(groupKey = "order-productService-singlePool",   //服务名称,相同名称使用同一个线程池
            commandKey = "selectProductById",//接口名称,默认为方法名
            threadPoolKey = "order-productService-singlePool",      //线程池名称,相同名称使用同一个线程池
            commandProperties = {
                    //超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000"
                    )
            },
            threadPoolProperties = {
                    //线程池大小
                    @HystrixProperty(name = "coreSize",value = "3"),
                    //队列等待阈值(最大队列长度,最大等待的线程数,默认为-1)
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    //线程存活时间,默认1min
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    //超出队列等待阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
            }//不写fallback就使用系统默认的fallback
    )
    @Cacheable(cacheNames = "orderService:product:single",key = "#id") //id即为下面传入的参数
    @Override
    public Product selectProductById(Integer id) {
        System.out.println(Thread.currentThread().getName()+"调用---ProductService---selectProductById---");
        return restTemplate.getForObject("http://product-service/product/"+id,Product.class);
    }
}

相比之前添加了用于线程隔离的注解:

@HystrixCommand(groupKey = "order-productService-listPool",//服务名称,使用相同名称的服务使用同一个线程池
            commandKey = "selectProductList", //接口名称,默认为方法名
            threadPoolKey = "order-productService-listPool",//线程池名称,相同名称的服务使用同一个线程池
            commandProperties = {
                    //超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000"
                    )
            },
            threadPoolProperties = {
                    //线程池大小(设置的总数量为10)
                    @HystrixProperty(name = "coreSize",value = "6"),
                    //队列等待的阈值(最大队列长度),默认为-1
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    //线程存活时间,默认1min
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    //超出队列等待阈值执行拒绝策略,value和上面的maxQueueSize的value保持一致
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
            }, fallbackMethod = "selectProductListFallback"  //使用托底方法返回的数据
    )

t哦那个给设置等待队列长度和线程池大小,可以达到一个限流的目的,防止过多的请求使得服务器崩溃

selectProductListFallback为托底方法,在等待队列满了之后剩下的请求会执行这个方法:

	//托底方法
    private List<Product> selectProductListFallback(){
        System.out.println("---托底数据的方法:selectProductListFallback---");
        return Arrays.asList(
                new Product(1,"托底数据-华为手机",1,5800D),
                new Product(2,"托底数据-联想笔记本",1,6888D),
                new Product(3,"托底数据-小米平板",5,2020D)
        );
    }

线程池的参数如下:

image-20220125182116765

最后记得在启动类中加上注解@EnableCircuitBreaker 或者@EnableHystrix,来启用Hystrix:

package com.demo;

import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

//默认开启EnableEurekaClient 以注册中心使用者启动
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker //开启熔断器,也可以使用EnableHystrix
//@EnableCaching //开启缓存注解
public class OrderServiceApplication
{
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
//    @Bean
//    public RandomRule randomRule() {return new RandomRule();}
    @Bean
    public Logger.Level getLog(){//日志对象输出
        return Logger.Level.NONE;
        /*
        NONE:无信息
        BASIC:记录基础信息
        HEADERS:在BASIC的基础上记录一些常用信息
        FULL:全部信息
         */
    }
    public static void main( String[] args )
    {
        SpringApplication.run(OrderServiceApplication.class,args);
    }
}

信号量隔离

线程池隔离会为每个线程创建一个新的线程实例,相当于情况开始和结束时会有两个不同的线程,因而它是异步的隔离方法,当实例很多时(上千个),需要进行很多的线程上下文的切换,会耗费很多时间。而信号量隔离则可以解决这个问题,信号量隔离竞争的是信号量而不是线程,拿到一个信号量后才能进行线程的运行,节省了许多上下文切换的时间。信号量隔离是同步的隔离方法,因为竞争的是信号量,而不是线程,所以只有一个线程,所以不需要做线程切换和异步处理。

pom依赖:信号量隔离与线程池隔离都在hystrix依赖中:

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

然后更改业务实现类的注解即可:

package com.demo.serviceImpl;

import com.demo.bean.Product;
import com.demo.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Future;

@Service
public class ProductServiceImpl implements ProductService {
    @Resource
    RestTemplate restTemplate;

    //信号量隔离
    @HystrixCommand(commandProperties = {
            //超时时间,默认为1000ms
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds"
                    ,value = "5000"),
            //信号量隔离
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,value = "SEMAPHORE"),
            //信号量最大并发量,调小一定便于模拟高并发
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
            value = "6")
    },fallbackMethod = "selectProductListFallback")//没有设置等待队列的长度(默认-1),所以超过最大并发时会直接调用拖地方法返回,可以达到一个限流的目的
    @Cacheable(cacheNames = "orderService:product:list")
    @Override
    public List<Product> selectProductList() {
        System.out.println(Thread.currentThread().getName()+"调用---ProductService---selectProductList---");
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }
        ).getBody();
    }

    //托底方法
    private List<Product> selectProductListFallback(){
        System.out.println("---托底数据的方法:selectProductListFallback---");
        return Arrays.asList(
                new Product(1,"托底数据-华为手机",1,5800D),
                new Product(2,"托底数据-联想笔记本",1,6888D),
                new Product(3,"托底数据-小米平板",5,2020D)
        );
    }

    //@HystrixCommand //声明是一个服务容错的方法
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        System.out.println("----selectProductListByIds---被调用----");//看看请求合并是否生效
        StringBuffer sb=new StringBuffer();
        ids.forEach(id->sb.append("id="+id+"&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?"+sb.toString(), ArrayList.class);
    }

//    @HystrixCollapser(batchMethod = "selectProductListByIds",//合并后用于批处理的方法
//            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,//请求方式
//            collapserProperties = {//批处理的时间阈值和数量阈值,两个只要有一个达到就会进行一次批处理
//                //间隔多久进行一次批处理,默认是10ms
//                @HystrixProperty(name = "timerDelayInMilliseconds",value = "20") ,
//                //批处理中允许的最大请求数
//                @HystrixProperty(name = "maxRequestsInBatch",value = "200")
//            }
//    )

    @HystrixCommand(groupKey = "order-productService-singlePool",   //服务名称,相同名称使用同一个线程池
            commandKey = "selectProductById",//接口名称,默认为方法名
            threadPoolKey = "order-productService-singlePool",      //线程池名称,相同名称使用同一个线程池
            commandProperties = {
                    //超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000"
                    )
            },
            threadPoolProperties = {
                    //线程池大小
                    @HystrixProperty(name = "coreSize",value = "3"),
                    //队列等待阈值(最大队列长度,最大等待的线程数,默认为-1)
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    //线程存活时间,默认1min
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    //超出队列等待阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
            }//不写fallback就使用系统默认的fallback
    )
    @Cacheable(cacheNames = "orderService:product:single",key = "#id") //id即为下面传入的参数
    @Override
    public Product selectProductById(Integer id) {
        System.out.println(Thread.currentThread().getName()+"调用---ProductService---selectProductById---");
        return restTemplate.getForObject("http://product-service/product/"+id,Product.class);
    }
}

核心部分为:

//信号量隔离
    @HystrixCommand(commandProperties = {
            //超时时间,默认为1000ms
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds"
                    ,value = "5000"),
            //信号量隔离
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,value = "SEMAPHORE"),
            //信号量最大并发量,调小一定便于模拟高并发
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
            value = "6")
    },fallbackMethod = "selectProductListFallback")//没有设置等待队列的长度(默认-1),所以超过最大并发时会直接调用拖地方法返回,可以达到一个限流的目的

看到以下结果代表配置成功:

image-20220127021621434

前六个线程成功拿到信号量,开始运行业务,而后面没有拿到信号量的则直接执行托底方法(因为没有配置等待队列的长度,配置方法和前面的相同),虽然只有6个信号量,但是线程id是110而不是16,这是因为信号量隔离时,各个线程使用的仍然是同一个线程池(容量为10),因而信号量隔离是同步的隔离方法。

信号量隔离vs线程池隔离

image-20220127023211370

image-20220127023421229

但其实绝大部分情况都使用线程池隔离即可,一般不再调用其他请求访问其他资源时,使用信号量隔离即可。

信号量的调用是同步的,也就是说,每次调用都得阻塞调用方的线程,直到结果返回。这样就致使了没法对访问作超时(只能依靠调用协议超时,没法主动释放),而线程池隔离是异步的,可以检测超时时间。

服务熔断

服务熔断相当于电闸的跳闸功能,当系统出现大量异常时,为了保全整个系统正常运行而会关闭一些服务(使它们执行托底方法)

当每过10s检测一次系统状态,如果10s中出现的异常大于所有请求的50%,则让这个服务进入服务熔断状态,在接下来的5s内让所有请求都去执行托底方法,5s过后,然后再允许请求去尝试请求服务,如果请求成功则关闭服务熔断,如果请求失败则继续服务熔断,5s后再次尝试。

服务熔断为系统提供了最基本的保护,而前面的服务隔离则是为服务提供第二层保护,前者通过统计请求数和异常数量,后者通过提供的线程来进行限流,防止出现过载。

image-20220206004831706

服务降级

其实服务降级在前面已经用到,其实就是当服务处于异常情况时,让服务fallback,返回托底数据,以保证系统的稳定性。

出发服务降级的条件如下:

  • 抛出非HystrixBadRequestException的异常,例如空指针异常,数据库异常等等
  • 方法调用超时
  • 熔断器开始拦截
  • 线程池/信号量/等待队列跑满

在出现这些情况时,则会直接使得方法fallback,在保证系统安全的同时,让请求可以得到回应

结合了服务熔断,服务隔离,还有超时时间,这些都在@HystrixCommand中进行设置

package com.demo.serviceImpl;

import com.demo.bean.Product;
import com.demo.bean.Status;
import com.demo.service.ProductService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.lang.reflect.Array;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.Future;

@Service
public class ProductServiceImpl implements ProductService {
    @Resource
    RestTemplate restTemplate;

    //信号量隔离
    @HystrixCommand(commandProperties = {
            //超时时间,默认为1000ms
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds"
                    ,value = "5000"),
            //信号量隔离
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,value = "SEMAPHORE"),
            //信号量最大并发量,调小一定便于模拟高并发
            @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
            value = "6")
    },fallbackMethod = "selectProductListFallback")//没有设置等待队列的长度(默认-1),所以超过最大并发时会直接调用拖地方法返回,可以达到一个限流的目的
    @Cacheable(cacheNames = "orderService:product:list")
    @Override
    public List<Product> selectProductList() {//查询商品列表
        System.out.println(Thread.currentThread().getName()+"调用---ProductService---selectProductList---");
        return restTemplate.exchange(
                "http://product-service/product/list",
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Product>>() {
                }
        ).getBody();
    }

    //托底方法
    private List<Product> selectProductListFallback(){
        System.out.println("---托底数据的方法:selectProductListFallback---");
        return Arrays.asList(
                new Product(1,"托底数据-华为手机",1,5800D),
                new Product(2,"托底数据-联想笔记本",1,6888D),
                new Product(3,"托底数据-小米平板",5,2020D)
        );
    }

    //@HystrixCommand //声明是一个服务容错的方法
    @Override
    public List<Product> selectProductListByIds(List<Integer> ids) {
        System.out.println("----selectProductListByIds---被调用----");//看看请求合并是否生效
        StringBuffer sb=new StringBuffer();
        ids.forEach(id->sb.append("id="+id+"&"));
        return restTemplate.getForObject("http://product-service/product/listByIds?"+sb.toString(), ArrayList.class);
    }

//    @HystrixCollapser(batchMethod = "selectProductListByIds",//合并后用于批处理的方法
//            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,//请求方式
//            collapserProperties = {//批处理的时间阈值和数量阈值,两个只要有一个达到就会进行一次批处理
//                //间隔多久进行一次批处理,默认是10ms
//                @HystrixProperty(name = "timerDelayInMilliseconds",value = "20") ,
//                //批处理中允许的最大请求数
//                @HystrixProperty(name = "maxRequestsInBatch",value = "200")
//            }
//    )

    @HystrixCommand(
            //线程池隔离
            groupKey = "order-productService-singlePool",   //服务名称,相同名称使用同一个线程池
            commandKey = "selectProductById",//接口名称,默认为方法名
            threadPoolKey = "order-productService-singlePool",      //线程池名称,相同名称使用同一个线程池
            commandProperties = {
                    //超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000"),
                    //10s内请求的数量大于这个值(10个)后会启动服务熔断,默认20个
                    @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
                            value = "10"),
                    //请求的错误率阈值,如果请求错误的个数占总请求的50%(设置的值),则会启动服务熔断
                    @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
                            value = "50"),
                    //每隔多少秒重试一次,这里设置的是5s
                    @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
                            value = "5000")
            },
            threadPoolProperties = {
                    //线程池大小
                    @HystrixProperty(name = "coreSize",value = "3"),
                    //队列等待阈值(最大队列长度,最大等待的线程数,默认为-1)
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    //线程存活时间,默认1min
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    //超出队列等待阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
            },fallbackMethod = "selectedProductByIdFallback"
    )
    @Cacheable(cacheNames = "orderService:product:single",key = "#id") //id即为下面传入的参数
    @Override
    public Product selectProductById(Integer id) {//根据主键查询id
        System.out.println(Thread.currentThread().getName()+"---selectProductById---"+
                LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
        //人为抛出异常
        if(id==1)
        {
            throw new RuntimeException("查询主键为1的商品信息导致异常");
        }
        return restTemplate.getForObject("http://product-service/product/"+id,Product.class);
    }

    /**
     * 托底方法
     * @param id
     * @return
     */
    private Product selectedProductByIdFallback(Integer id)
    {
        return new Product(id,"根据主键查找-托底数据",1,2666D);
    }

    //托底方法
    private LinkedHashMap<String, Object> consumeProductFallback(Integer id)
    {
        LinkedHashMap<String, Object> result=new LinkedHashMap<String, Object>();
        System.out.println("---调用托底方法---");
        result.put("status", Status.ERROR);
        result.put("product",new Product());
        result.put("msg","请求失败稍后重试");
        return result;
    }


    @HystrixCommand(groupKey = "order-productConsumeService-listPool",//服务名称,使用相同名称的服务使用同一个线程池
            commandKey = "consumeProductById", //接口名称,默认为方法名
            threadPoolKey = "order-productConsumeService-listPool",//线程池名称,相同名称的服务使用同一个线程池
            commandProperties = {
                    //超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000"
                    )
            },
            threadPoolProperties = {
                    //线程池大小(设置的总数量为10)
                    @HystrixProperty(name = "coreSize",value = "50"),
                    //队列等待的阈值(最大队列长度),默认为-1
                    @HystrixProperty(name = "maxQueueSize",value = "200"),
                    //线程存活时间,默认1min
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    //超出队列等待阈值执行拒绝策略,value和上面的maxQueueSize的value保持一致
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
            }, fallbackMethod = "consumeProductFallback"  //使用托底方法返回的数据
    )
    @Override
    public LinkedHashMap<String, Object> consumeProductById(Integer id) {
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<String, Object>();
        paramMap.add("id", id);
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<MultiValueMap<String, Object>>(paramMap,new HttpHeaders());
        return restTemplate.exchange(
                "http://product-service/product/consume",
                HttpMethod.POST,
                httpEntity,
                LinkedHashMap.class
        ).getBody();
    }
}

整合后的注解:

    @HystrixCommand(
            //线程池隔离
            groupKey = "order-productService-singlePool",   //服务名称,相同名称使用同一个线程池
            commandKey = "selectProductById",//接口名称,默认为方法名
            threadPoolKey = "order-productService-singlePool",      //线程池名称,相同名称使用同一个线程池
            commandProperties = {
                    //超时时间,默认1000ms
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                            value = "5000"),
                	//服务熔断
                    //10s内请求的数量大于这个值(10个)后会启动服务熔断,默认20个
                    @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
                            value = "10"),
                    //请求的错误率阈值,如果请求错误的个数占总请求的50%(设置的值),则会启动服务熔断
                    @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
                            value = "50"),
                    //每隔多少秒重试一次,这里设置的是5s
                    @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
                            value = "5000")
            },
            threadPoolProperties = {
                    //线程池大小
                    @HystrixProperty(name = "coreSize",value = "3"),
                    //队列等待阈值(最大队列长度,最大等待的线程数,默认为-1)
                    @HystrixProperty(name = "maxQueueSize",value = "100"),
                    //线程存活时间,默认1min
                    @HystrixProperty(name = "keepAliveTimeMinutes",value = "2"),
                    //超出队列等待阈值执行拒绝策略
                    @HystrixProperty(name = "queueSizeRejectionThreshold",value = "100")
            },fallbackMethod = "selectedProductByIdFallback"
    )

包含了三种触发条件,满足其中任意一个就会返回fallback方法,此外方法中如果出现Hystrix以外的异常也会直接出发服务降级返回fallback。

Feign实战记录

这里将一个单节点应用分成了两个结点:计算密集型(用于使用搜索算法,寻找合适的活动安排方案),存储密集型结点(用于操作数据库)

首先是编码问题:

yml应当使用UTF-8的编码,需要在setting里面单独设置,避开中文

然后是pom依赖问题,pom依赖应当参照spring的官方文档,对于各种版本都有适配的要求:

Spring Cloud

此时项目使用的版本是:

image-20220315122949109

也就是springboot使用的是2.6.3,spring-cloud使用的是2021.0.1

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.3</version>
 </parent>
··<dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>2021.0.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

netflix和openfeign都使用的是3.1.1

这样依赖就配合好了

对于需要连接数据库的yml配置方式:

server:
  port: 8080
  servlet:
    session:
      timeout: 3000
  tomcat:
    KeepAliveTimeOut: 30000 #多少ms后没有请求断开连接
    maxKeepAliveRequests: 10000 #多少次请求后断开连接

spring:
  application:
    name: time-manager
  datasource:
    name: time_manager
    url: jdbc:mysql://127.0.0.1:3306/time_manager?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  aop:
    auto: true
  jpa:
    show-sql: true   # 是否启动日志SQL语句
  redis:
    timeout: 10000         #超时时间:10s
    host: localhost        #redis服务器地址
    port: 6379             #redis服务器端口
    password: 123456         #redis服务器密码
    database: 0            #选择哪个库
    lettuce:
      pool:
        max-active: 1024   #最大连接数
        max-wait: 10000    #最大连接阻塞等待时间,单位ms,默认-1
        max-idle: 200      #最大空闲连接,默认8
        min-idle: 5        #最小空闲连接 ,默认0

mybatis:
  mapper-locations: classpath:mapper/*.xml  #配置映射文件
  type-aliases-package: com.demo.bean #配置实体类

eureka:
  instance:
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:   #注册中心的url都写上
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/

#度量健康指标与健康检测
management:
  endpoints:
    web:
      exposure:
        include: '*' #开启 shutdown 端点访问
  endpoint:
    shutdown:
      enabled: true       #开启shutdown优雅停服

对于不需要连接数据库的配置方式(计算密集型):

server:
  port: 8081
  servlet:
    session:
      timeout: 3000
  tomcat:
    KeepAliveTimeOut: 30000 #多少ms后没有请求断开连接
    maxKeepAliveRequests: 10000 #多少次请求后断开连接

spring:
  application:
    name: task-manager
  aop:
    auto: true

#注册中心
eureka:
  instance:
    prefer-ip-address: true #使用ip显示服务名
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #ip名称=客户端ip+端口号
  client:
    service-url:   #注册中心的url都写上
      defaultZone: http://root:123456@localhost:8761/eureka/,http://root:123456@localhost:8761/eureka/

#度量健康指标与健康检测
management:
  endpoints:
    web:
      exposure:
        include: '*' #开启 shutdown 端点访问
  endpoint:
    shutdown:
      enabled: true       #开启shutdown优雅停服

双方都要在启动类加上@EnableFeignClients注解

服务使用方:远程过程调用

package com.demo.service;

import com.demo.bean.FixedEventsAndTasks;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.LinkedHashMap;

/**
 * @author 李天航
 * 调用计算结点进行活动安排
 */
@FeignClient("task-manager")
public interface TaskManageRemoteCallService {
    /**
     * 远程过程调用
     * @param fixedEventsAndTasks 固定活动表和参数列表
     * @return msg,status,results
     */
    @PostMapping("/task/manage")
    LinkedHashMap<String, Object> taskManage(@RequestBody FixedEventsAndTasks fixedEventsAndTasks);
}

@RequestBody 服务调用者可以不加,因为它默认将参数放到请求体中

package com.compute.controller;

import com.compute.bean.FixedEvent;
import com.compute.bean.FixedEventsAndTasks;
import com.compute.bean.Task;
import com.compute.service.EventManageService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;

/**
 * @author 李天航
 * 活动安排的接口
 */
@RestController
@RequestMapping("/task")
public class TaskManageController {
    @Resource
    EventManageService eventManageService;

    @PostMapping("/manage")
    LinkedHashMap<String, Object> taskManage(@RequestBody FixedEventsAndTasks fixedEventsAndTasks)
    {
        System.out.println(fixedEventsAndTasks);
        List<FixedEvent> alreadyIn = fixedEventsAndTasks.getFixedEvents();
        List<Task> tasks = fixedEventsAndTasks.getTasks();
        if(alreadyIn!=null&&tasks!=null) {
            return eventManageService.eventManage(alreadyIn,tasks);
        }
        else
        {
            LinkedHashMap<String, Object> resultMap = new LinkedHashMap<>();
            resultMap.put("status",500);
            resultMap.put("msg","参数不全");
            return resultMap;
        }
    }
}

服务提供者也要加上@RequestBody注解,否则会默认从路径中读取数据而读取不到数据

返回参数的时候:

        if(trySearchAnswer(alreadyIn,taskAndVis,0,0))
        {
            resultMap.put("status",200);
            resultMap.put("msg","安排成功");
            resultMap.put("results",alreadyIn);
        }
        else
        {
            resultMap.put("status",500);
            resultMap.put("msg","安排失败");
        }

就和正常的服务一样返回即可,alreadyIn是一个对象数组

对象数组在返回时(从Map中拿出来时),会从对象变成一个Map,因而会解析失败

所以需要我们把拿出来的内容解析成JSON格式,再进行转换(从=模式变成:模式)

		List<FixedEvent> fixedEventArray = (List<FixedEvent>) returnResult.get("results");
        ObjectMapper objectMapper=new ObjectMapper();
        List<FixedEvent> fixedEventList=objectMapper.convertValue(fixedEventArray, new TypeReference<List<FixedEvent>>() {});
        //添加安排后的结果
        addManagedResults(fixedEventList,userAccount,weekNum);

如果不是List类型,直接强制转换即可:

Integer status= (Integer) returnResult.get("status");

服务调用者传递参数的时候只能传递一个参数,所以我们可以传一个Map过去,或者新定义一个类

传Map:

package com.demo.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.LinkedHashMap;

/**
 * @author 李天航
 * 调用计算结点进行活动安排
 */
@FeignClient("task-manager")
public interface TaskManageRemoteCallService {
    /**
     * 远程过程调用
     *
     * @param params 固定活动表,和任务列表
     * @return msg,status,results
     */
    @PostMapping("/task/manage")
    LinkedHashMap<String, Object> taskManage(LinkedHashMap<String, Object> params);
}

接收map:

package com.compute.controller;

import com.compute.bean.FixedEvent;
import com.compute.bean.FixedEventsAndTasks;
import com.compute.bean.Task;
import com.compute.service.EventManageService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;

/**
 * @author 李天航
 * 活动安排的接口
 */
@RestController
@RequestMapping("/task")
public class TaskManageController {
    @Resource
    EventManageService eventManageService;

    @PostMapping("/manage")
    LinkedHashMap<String, Object> taskManage(@RequestBody LinkedHashMap<String, Object> params)
    {
        List<FixedEvent> alreadyIn= (List<FixedEvent>) params.get("fixedEvents");
        List<FixedEvent> fixedEvents=new ObjectMapper().convertValue(alreadyIn, new TypeReference<List<FixedEvent>>(){});
        List<Task> tasks = (List<Task>) params.get("tasks");
        List<Task> taskList=new ObjectMapper().convertValue(tasks, new TypeReference<List<Task>>() {});
        if(fixedEvents!=null&&tasks!=null) {
            return eventManageService.eventManage(fixedEvents,taskList);
        }
        else
        {
            LinkedHashMap<String, Object> resultMap = new LinkedHashMap<>();
            resultMap.put("status",500);
            resultMap.put("msg","参数不全");
            return resultMap;
        }
    }
}

基于Feign的服务熔断和降级

新版本的Feign不能服务熔断和降级,并且不再默认包含Hystrix(我TM……)

Spring Cloud openFeign学习【3.0.2版本】 - SegmentFault 思否

解决方案:回退版本,统一回退到2.2.4.RELEASE 和JDK 13的版本 SpringCloud HostonSR1

JDK下载:http://jdk.java.net/java-se-ri/13

把MAVEN中原有的jar删掉,然后再下新的

实现服务降级其实很简单,在原有的Feign的基础上,编写一个实现类,作为它的托底方法类,然后加上@FeignClient(value = “task-manager”,fallback = TaskManageFallBack.class) 注解即可

package com.demo.RemoteCall;

import com.demo.RemoteCall.FallBack.TaskManageFallBack;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.LinkedHashMap;

/**
 * @author 李天航
 * 调用计算结点进行活动安排
 */
@FeignClient(value = "task-manager",fallback = TaskManageFallBack.class)
public interface TaskManageRemoteCallService {
    /**
     * 远程过程调用
     *
     * @param params 固定活动表,和任务列表
     * @return msg,status,results
     */

    @PostMapping("/task/manage")
    LinkedHashMap<String, Object> taskManage(LinkedHashMap<String, Object> params);
}

value:服务名称 fallback:托底方法类

托底方法类:

package com.demo.RemoteCall.FallBack;

import com.demo.RemoteCall.TaskManageRemoteCallService;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.util.LinkedHashMap;

@Service
public class TaskManageFallBack implements TaskManageRemoteCallService {
    /**
     * 远程过程调用
     *
     * @param params 固定活动表,和任务列表
     * @return msg, status, results
     */
    @Override
    public LinkedHashMap<String, Object> taskManage(LinkedHashMap<String, Object> params) {
        params.put("status",500);
        params.put("msg","网络繁忙");
        return params;
    }
}

添加FallbackFactory来实现异常信息地输出

package com.demo.RemoteCall.FallBack;

import com.demo.RemoteCall.TaskManageRemoteCallService;
import feign.hystrix.FallbackFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * @author 李天航
 * 用于捕获异常信息的工场
 */
@Service
public class TaskManageFallBackFactory implements FallbackFactory<TaskManageRemoteCallService> {
    Logger logger = LoggerFactory.getLogger(TaskManageFallBackFactory.class);
    @Override
    public TaskManageRemoteCallService create(Throwable cause) {
        logger.error("出现异常,触发服务降级:"+cause.getMessage());
        cause.printStackTrace();
        return new TaskManageFallBack();
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值