SpringCloud Alibaba

微服务环境搭建

商品微服务

  • 查询商品列表

订单微服务

  • 创建订单

2.1 技术选型

持久层: SpingData Jpa

数据库: MySQL5.7

其他: SpringCloud Alibaba 技术栈

2.2 模块设计

— shop-parent 父工程

​ — shop-product-api 商品微服务api 【存放商品实体】

​ — shop-product-server 商品微服务 【端口:808x】

​ — shop-order-api 订单微服务api 【存放订单实体】

​ — shop-order-server 订单微服务 【端口:809x】

2.3 微服务调用

​ 在微服务架构中,最常见的场景就是微服务之间的相互调用。我们以电商系统中常见的用户下单

例来演示微服务的调用:客户向订单微服务发起一个下单的请求,在进行保存订单之前需要调用商品微

服务查询商品的信息。

我们一般把服务的主动调用方称为服务消费者,把服务的被调用方称为服务提供者

在这里插入图片描述

在这种场景下,订单微服务就是一个服务消费者, 商品微服务就是一个服务提供者。

2.4 版本说明

​ https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明

在这里插入图片描述

2.5 创建父工程

创建一个maven工程,然后在pom.xml文件中添加下面内容

<?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>cn.wolfcode</groupId>
    <artifactId>shop-parent</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>
    <!--父工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
    </parent>
    <!--依赖版本的锁定-->
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

2.6 创建商品微服务

1.创建shop-product-api项目,然后在pom.xml文件中添加下面内容

<?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>shop-parent</artifactId>
        <groupId>cn.wolfcode</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>shop-product-api</artifactId>
    <!--依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

2 创建实体类

//商品
@Entity(name = "t_shop_product")
@Data
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long pid;//主键
    private String pname;//商品名称
    private Double pprice;//商品价格
    private Integer stock;//库存
}

3.创建shop-product-server项目,然后在pom.xml文件中添加下面内容

<?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>shop-parent</artifactId>
        <groupId>cn.wolfcode</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>shop-product-server</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>
        <dependency>
            <groupId>cn.wolfcode</groupId>
            <artifactId>shop-product-api</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

4.编写启动类ProductServer.java

package cn.wolfcode;
@SpringBootApplication
public class ProductServer {
    public static void main(String[] args) {
        SpringApplication.run(ProductServer.class,args);
    }
}

5.编写配置文件application.yml

server:
  port: 8081
spring:
  application:
    name: product-service
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///shop-product?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: admin
  jpa:
    properties:
      hibernate:
        hbm2ddl:
          auto: update
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

6.在数据库中创建shop-product的数据库

7.创建ProductDao

package cn.wolfcode.dao;
public interface ProductDao extends JpaRepository<Product, Long> {
}

8.创建ProductService接口和实现类

package cn.wolfcode.service.impl;
@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductDao productDao;
    @Override
    public Product findByPid(Long pid) {
        return productDao.findById(pid).get();
    }
}

9.创建Controller

package cn.wolfcode.controller;
@RestController
@Slf4j
public class ProductController {
    @Autowired
    private ProductService productService;
    //商品信息查询
    @RequestMapping("/product")
    public Product findByPid(@RequestParam("pid") Long pid) {
        log.info("接下来要进行{}号商品信息的查询", pid);
        Product product = productService.findByPid(pid);
        log.info("商品信息查询成功,内容为{}", JSON.toJSONString(product));
        return product;
    }
}

10.启动工程,等到数据库表创建完毕之后,加入测试数据

INSERT INTO t_shop_product VALUE(NULL,'小米','1000','5000'); 
INSERT INTO t_shop_product VALUE(NULL,'华为','2000','5000'); 
INSERT INTO t_shop_product VALUE(NULL,'苹果','3000','5000'); 
INSERT INTO t_shop_product VALUE(NULL,'OPPO','4000','5000');

11.通过浏览器访问服务

在这里插入图片描述

2.7 创建订单微服务

1.创建shop-order-api项目,然后在pom.xml文件中添加下面内容

<?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>shop-parent</artifactId>
        <groupId>cn.wolfcode</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>shop-order-api</artifactId>
    <!--依赖-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

2 创建实体类

//订单
@Entity(name = "t_shop_order")
@Data
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long oid;//订单id

    //用户
    private Long uid;//用户id
    private String username;//用户名
    //商品
    private Long pid;//商品id
    private String pname;//商品名称
    private Double pprice;//商品单价
    //数量
    private Integer number;//购买数量
}

3.创建shop-order-server项目,然后在pom.xml文件中添加下面内容

<?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>shop-parent</artifactId>
        <groupId>cn.wolfcode</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>shop-order-server</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>
        <dependency>
            <groupId>cn.wolfcode</groupId>
            <artifactId>shop-order-api</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

4.编写启动类OrderServer.java

package cn.wolfcode;
@SpringBootApplication
public class OrderServer {
    public static void main(String[] args) {
        SpringApplication.run(OrderServer.class,args);
    }
}

5.编写配置文件application.yml

server:
  port: 8091
spring:
  application:
    name: order-service
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///shop-order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
    username: root
    password: admin
  jpa:
    properties:
      hibernate:
        hbm2ddl:
          auto: update
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

6.在数据库中创建shop-order的数据库

7.创建OrderDao

package cn.wolfcode.dao;

public interface OrderDao extends JpaRepository<Order, Long> {
}

8.创建OrderService接口和实现类

package cn.wolfcode.service.impl;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;
    @Override
    public Order createOrder(Long productId,Long userId) {
        log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", productId);
        //远程调用商品微服务,查询商品信息
        Product product = null;
        log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product));
        //创建订单并保存
        Order order = new Order();
        order.setUid(userId);
        order.setUsername("叩丁狼教育");
        order.setPid(productId);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderDao.save(order);
        log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
        return order;
    }
}

9.创建Controller

package cn.wolfcode.controller;
@RestController
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;
    @RequestMapping("/save")
    public Order order(Long pid,Long uid) {
        return orderService.createOrder(pid,uid);
    }
}

2.8 服务间如何进行远程调用

商品微服务已经提供了数据接口了,订单微服务应该如何去调用呢?

在这里插入图片描述

其实就是如何通过Java代码去调用一个http的接口地址,我们可以使用RestTemplate来进行调用.

1.在启动类上添加RestTemplate的bean配置

@SpringBootApplication
public class OrderServer {
    public static void main(String[] args) {
        SpringApplication.run(OrderServer.class,args);
    }
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

2.在OrderServiceImpl中注入RestTemplate并实现远程调用

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private RestTemplate restTemplate;
    @Override
    public Order createOrder(Long productId,Long userId) {
        log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", productId);
        //远程调用商品微服务,查询商品信息
        Product product = restTemplate.getForObject(
                "http://localhost:8081/product/"+productId,Product.class);
        log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product));
        //创建订单并保存
        Order order = new Order();
        order.setUid(userId);
        order.setUsername("叩丁狼教育");
        order.setPid(productId);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderDao.save(order);
        log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
        return order;
    }
}

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

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

  • 一旦是多个服务提供者,无法实现负载均衡功能

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

那么应该怎么解决呢, 这时候就需要通过注册中心动态的实现服务治理

服务治理 Nacos Discovery

3.1 什么是服务治理

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

**服务注册:**在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服

务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中

的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。

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

例的访问。

在这里插入图片描述

通过上面的调用图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构非常重要

的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

  1. 服务发现:

服务注册:保存服务提供者和服务调用者的信息

服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息

  1. 服务健康检测

检测服务提供者的健康情况,如果发现异常,执行服务剔除

3.2 常见注册中心

  • Zookeeper

    Zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式

应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用

配置项的管理等。

  • Eureka

    Eureka是Springcloud Netflflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭

  • Consul

    Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现

和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value

存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以

安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。

  • Nacos

    Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 Spring

Cloud Alibaba 组件之一,负责服务注册发现和服务配置。

3.3 Nacos 简介

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速

实现动态服务发现、服务配置、服务元数据及流量管理。

从上面的介绍就可以看出,Nacos的作用就是一个注册中心,用来管理注册上来的各个微服务。

核心功能点:

  • 服务注册: Nacos Client会通过发送REST请求想Nacos Server注册自己的服务,提供自身的元数据,比如IP地址,端口等信息。Nacos Server接收到注册请求后,就会把这些元数据存储到一个双层的内存Map中。

  • 服务心跳: 在服务注册后,Nacos Client会维护一个定时心跳来维持统治Nacos Server,说明服务一致处于可用状态,防止被剔除,默认5s发送一次心跳

  • 服务同步: Nacos Server集群之间会相互同步服务实例,用来保证服务信息的一致性。

  • 服务发现: 服务消费者(Nacos Client)在调用服务提供的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务拉取服务最新的注册表信息更新到本地缓存。

  • 服务健康检查: Nacos Server 会开启一个定时任务来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将他的healthy属性设置为false(客户端服务发现时不会发现),如果某个实例超过30s没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)

3.4 Nacos实战入门

接下来,我们就在现有的环境中加入nacos,并将我们的两个微服务注册上去。

3.4.1 搭建Nacos环境

  1. 安装Nacos

    下载地址: https://github.com/alibaba/nacos/releases 
    下载zip格式的安装包,然后进行解压缩操作,上课使用的Nacos Server版本是1.3.2
    
  2. 启动Nacos

    #切换目录 
    cd nacos/bin 
    #命令启动 
    startup.cmd -m standalone
    
  3. 访问Nacos

    打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos

在这里插入图片描述

3.4.2 将商品服务注册到Nacos

接下来开始修改 shop-product-server 模块的代码, 将其注册到nacos服务上

  1. 在pom.xml中添加Nacos的依赖
<!--nacos客户端-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 在主类上添加**@EnableDiscoveryClient**注解

    @SpringBootApplication
    @EnableDiscoveryClient
    public class ProductServer {
        public static void main(String[] args) {
            SpringApplication.run(ProductServer.class,args);
        }
    }
    
  2. 在application.yml中添加Nacos服务的地址

    spring:
      cloud: 
        nacos: 
          discovery: 
            server-addr: localhost:8848
    
  3. 启动服务, 观察Nacos的控制面板中是否有注册上来的商品微服务

在这里插入图片描述

3.4.3 将订单服务注册到Nacos

接下来开始修改 shop-order-server 模块的代码, 将其注册到nacos服务上

  1. 在pom.xml中添加Nacos的依赖
<!--nacos客户端-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 在主类上添加**@EnableDiscoveryClient**注解

    @SpringBootApplication
    @EnableDiscoveryClient
    public class OrderServer {
        public static void main(String[] args) {
            SpringApplication.run(OrderServer.class,args);
        }
    }
    
  2. 在application.yml中添加Nacos服务的地址

spring:
  cloud: 
    nacos: 
      discovery: 
        server-addr: localhost:8848
  1. 启动服务, 观察Nacos的控制面板中是否有注册上来的订单微服务

在这里插入图片描述

  1. 修改OrderServiceImpl, 实现微服务调用
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private DiscoveryClient discoveryClient;
    @Autowired
    private RestTemplate restTemplate;
    @Override
    public Order createOrder(Long productId,Long userId) {
        log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", productId);
        //从nacos中获取服务地址
        ServiceInstance instance = discoveryClient.
                getInstances("product-service").get(0);
        String url = instance.getHost()+":"+instance.getPort();
        //远程调用商品微服务,查询商品信息
        Product product = restTemplate.getForObject(
                "http://"+url+"/product/"+productId,Product.class);
        log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product));
        //创建订单并保存
        Order order = new Order();
        order.setUid(userId);
        order.setUsername("叩丁狼教育");
        order.setPid(productId);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderDao.save(order);
        log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
        return order;
    }
}

远程调用负载均衡 Ribbon

4.1 什么是负载均衡

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

进行执行。

4.2 自定义负载均衡

  1. 通过idea再启动一个 shop-product 微服务,设置其端口为8082

    在这里插入图片描述

  2. 通过nacos查看微服务的启动情况

    在这里插入图片描述

  3. 修改 OrderServiceImpl 的代码,实现负载均衡

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private DiscoveryClient discoveryClient;
    @Autowired
    private RestTemplate restTemplate;
    @Override
    public Order createOrder(Long productId,Long userId) {
        log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", productId);
        //从nacos中获取服务地址
        //自定义规则实现随机挑选服务
        List<ServiceInstance> instances = discoveryClient.
                getInstances("product-service");
        int index = new Random().nextInt(instances.size());
        ServiceInstance instance = instances.get(index);
        String url = instance.getHost()+":"+instance.getPort();
        log.info(">>从nacos中获取到的微服务地址为:" + url);
        //远程调用商品微服务,查询商品信息
        Product product = restTemplate.getForObject(
                "http://"+url+"/product/"+productId,Product.class);
        log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product));
        //创建订单并保存
        Order order = new Order();
        order.setUid(userId);
        order.setUsername("叩丁狼教育");
        order.setPid(productId);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(1);
        orderDao.save(order);
        log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
        return order;
    }
}
  1. 启动两个服务提供者和一个服务消费者,多访问几次消费者测试效果

    在这里插入图片描述

4.3 基于Ribbon实现负载均衡

Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡

  1. 在RestTemplate 的生成方法上添加@LoadBalanced注解

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
    	return new RestTemplate();
    }
    
  2. 修改OrderServiceImpl服务调用的方法

    @Service
    @Slf4j
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private OrderDao orderDao;
        @Autowired
        private RestTemplate restTemplate;
        @Override
        public Order createOrder(Long productId,Long userId) {
            log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", productId);
            //远程调用商品微服务,查询商品信息
            Product product = restTemplate.getForObject(
                    "http://product-service/product?productId="+productId,Product.class);
            log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product));
            //创建订单并保存
            Order order = new Order();
            order.setUid(userId);
            order.setUsername("叩丁狼教育");
            order.setPid(productId);
            order.setPname(product.getPname());
            order.setPprice(product.getPprice());
            order.setNumber(1);
            orderDao.save(order);
            log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
            return order;
        }
    }
    
  3. 为了更直观看到请求是进行负载均衡了,我们修改一下ProductController代码

    @RestController
    @Slf4j
    public class ProductController {
        @Autowired
        private ProductService productService;
        @Value("${server.port}")
        private String port;
        //商品信息查询
        @RequestMapping("/product")
        public Product findByPid(@RequestParam("pid") Long pid) {
            log.info("接下来要进行{}号商品信息的查询", pid);
            Product product = productService.findByPid(pid);
            product.setPname(product.getPname()+",data from "+port);
            log.info("商品信息查询成功,内容为{}", JSON.toJSONString(product));
            return product;
        }
    }
    
  4. 调用订单保存的方法,查看日志.

    在这里插入图片描述

默认情况下,采取的是ZoneAvoidanceRule的策略,复合判断server所在区域的性能和server的可用性选择server

Ribbon支持的负载均衡策略

Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为

com.netflix.loadbalancer.IRule , 具体的负载策略如下图所示:

策略名策略描述实现说明
BestAvailableRule选择一个最小的并发请求的server逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server
AvailabilityFilteringRule先过滤掉故障实例,再选择并发较小的实例;使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态
WeightedResponseTimeRule根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低。一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单responsetime 减去每个server自己平均的responsetime是server的权
RetryRule对选定的负载均衡策略机上重试机制。在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server
RoundRobinRule轮询方式轮询选择server轮询index,选择index对应位置的server
RandomRule随机选择一个server在index上随机,选择index对应位置的server
ZoneAvoidanceRule(默认)复合判断server所在区域的性能和server的可用性选择server使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。

我们可以通过修改配置来调整Ribbon的负载均衡策略,在order-server项目的application.yml中增加如下配置:

product-service: # 调用的提供者的名称
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

远程调用Feign

5.1 什么是Feign

​ Feign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务

一样简单, 只需要创建一个接口并添加一个注解即可。

​ Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负

载均衡的效果。

5.2 订单微服务集成Feign

  1. 在shop-order-server项目的pom文件加入Fegin的依赖

    <!--fegin组件-->
    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  2. 在启动类OrderServer.java上添加Fegin的扫描注解,注意扫描路径(默认扫描当前包及其子包)

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderServer {
    public static void main(String[] args) {
        SpringApplication.run(OrderServer.class,args);
    }
}
  1. 在shop-order-server项目中新增接口ProductFeignApi

    package cn.wolfcode.feign;
    //name的名称一定要和订单服务的服务名保持一致
    @FeignClient(name = "product-service")
    public interface ProductFeignApi {
        @RequestMapping("/product")
        public Product findByPid(@RequestParam("pid") Long pid);
    }
    
  2. 修改OrderServiceImpl.java的远程调用方法

    @Service
    @Slf4j
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private OrderDao orderDao;
        @Autowired
        private ProductFeignApi productFeignApi;
        @Override
        public Order createOrder(Long productId,Long userId) {
            log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", productId);
            //远程调用商品微服务,查询商品信息
            Product product = productFeignApi.findByPid(productId);
            log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product));
            //创建订单并保存
            Order order = new Order();
            order.setUid(userId);
            order.setUsername("叩丁狼教育");
            order.setPid(productId);
            order.setPname(product.getPname());
            order.setPprice(product.getPprice());
            order.setNumber(1);
            orderDao.save(order);
            log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
            return order;
        }
    }
    
  3. 重启订单服务,并验证.

5.3 Feign的重要属性

超时配置

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

重试次数配置

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

product-service: # 调用的提供者的名称
  ribbon:
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 0

服务熔断降级 Sentinel

6.1 高并发带来的问题

​ 在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络

原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会

出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。

接下来,我们来模拟一个高并发的场景

  1. 在订单服务中新建SentinelController.java

    @RestController
    public class SentinelController {
        @RequestMapping("/sentinel1")
        public String sentinel1(){
            //模拟一次网络延时
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "sentinel1";
        }
        @RequestMapping("/sentinel2")
        public String sentinel2(){
            return "测试高并发下的问题";
        }
    }
    
  2. 修改配置文件中tomcat的并发数

    server:
      port: 8091
      tomcat:
        threads:
          max: 10 #tomcat的最大并发值修改为10,
    
  3. 接下来使用压测工具,对请求进行压力测试

    下载地址https://jmeter.apache.org/

    • 第一步:修改配置,并启动软件

      进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN,然后点击jmeter.bat

      启动软件。

      在这里插入图片描述

      在这里插入图片描述

    • 第二步:添加线程组

      在这里插入图片描述

    • 第三步:配置线程并发数

      在这里插入图片描述

    • 第四步:添加Http请求

      在这里插入图片描述

    • 第五步:配置取样,并启动测试

      在这里插入图片描述

    第六步:访问 http://localhost:8091/sentinel2 观察结果

    结论:此时会发现, 由于sentinel1方法囤积了大量请求, 导致sentinel2方法的访问出现了问题,这就是服务雪

    崩的雏形。

    6.2 服务器雪崩效应

    ​ 在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了

    问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等

    待,进而导致服务瘫痪。

    ​ 由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是

    服务故障的 “雪崩效应” 。

    • 情景1: 微服务之间相互调用,关系复杂,正常情况如下图所示:

      在这里插入图片描述

    • 情景2:某个时刻,服务A挂了,服务B和服务C依然在调用服务A

      在这里插入图片描述

    • 情景3:由于服务A挂了,导致服务C和服务B无法得到服务A的响应,这时候服务C和服务B由于大量线程积压,最终导致服务C和服务B挂掉.

      在这里插入图片描述

    • 情景4: 相同道理,由于服务之间有关联,所以会导致整个调用链上的所有服务都挂掉.

      在这里插入图片描述

    ​ 服务器的雪崩效应其实就是由于某个微小的服务挂了,导致整一大片的服务都不可用.类似生活中的雪崩效应,由于落下的最后一片雪花引发了雪崩的情况.

    ​ 雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某

    台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问

    题,不会影响到其它服务的正常运行。

6.2 常见容错方案

​ 要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措

施, 下面介绍常见的服务容错思路和组件。

常见的容错思路

常见的容错思路有隔离、超时、限流、熔断、降级这几种,下面分别介绍一下。

  • 隔离机制: 比如服务A内总共有100个线程, 现在服务A可能会调用服务B,服务C,服务D.我们在服务A进行远程调用的时候,给不同的服务分配固定的线程,不会把所有线程都分配给某个微服务. 比如调用服务B分配30个线程,调用服务C分配30个线程,调用服务D分配40个线程. 这样进行资源的隔离,保证即使下游某个服务挂了,也不至于把服务A的线程消耗完。比如服务B挂了,这时候最多只会占用服务A的30个线程,服务A还有70个线程可以调用服务C和服务D.

    在这里插入图片描述

  • 超时机制: 在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出 反应,

    就断开请求,释放掉线程。

    *在这里插入图片描述

  • 限流机制: 限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到

    的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。

    在这里插入图片描述

  • 熔断机制: 在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整

    体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。

    在这里插入图片描述

    服务熔断一般有三种状态:

    • 熔断关闭状态(Closed)

    服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制

    • 熔断开启状态(Open)

    后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法

    • 半熔断状态(Half-Open)

    尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预

    期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状

    态。

  • 降级机制: 降级其实就是为服务提供一个兜底方案,一旦服务无法正常调用,就使用兜底方案。

在这里插入图片描述

6.3 常见的容错组件

  • Hystrix

Hystrix是由Netflflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止

级联失败,从而提升系统的可用性与容错性。

  • Resilience4J

Resilicence4J一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推

荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和

prometheus等多款主流产品进行整合。

  • Sentinel

Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。

6.4 Sentinel入门

6.4.1 什么是Sentinel

Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量

为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即

突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用

应用等。

  • 完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒

级数据, 甚至 500 台以下规模的集群的汇总运行情况。

  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 Spring

Cloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入

Sentinel。

Sentinel分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo /

Spring Cloud 等框架也有较好的支持。

  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等

应用容器。

6.4.2 订单微服务集成Sentinel

为微服务集成Sentinel非常简单, 只需要加入Sentinel的依赖即可

在shop-order-server项目的pom文件中添加如下依赖

<!--sentinel组件-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

6.4.3 安装Sentinel控制台

Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。

  1. 下载jar包 https://github.com/alibaba/Sentinel/releases

  2. 启动控制台

# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目) 
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar
  1. 修改shop-order-server项目中的配置文件application.yml,新增如下配置:

    spring:
      cloud:
        sentinel: 
          transport: 
            port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可 
            dashboard: localhost:8080 # 指定控制台服务的地址
    
  2. 通过浏览器访问localhost:8080 进入控制台 ( 默认用户名密码是 sentinel/sentinel )

    注意: 默认是没显示order-service的,需要访问几次接口,然后再刷新sentinel管控台才可以看到.

    在这里插入图片描述

6.4.4 实现一个接口的限流

第一步: 簇点链路—>流控

在这里插入图片描述

第二步: 在单机阈值填写一个数值,表示每秒上限的请求数

在这里插入图片描述

第三步:通过控制台快速频繁访问, 观察效果

在这里插入图片描述

6.4.5 Sentinel容错的维度

在这里插入图片描述

流量控制:流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是

随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。

熔断降级:当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则

对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。

系统负载保护:Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让

请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其

它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保

护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请

求。

6.4.6 Sentinel规则种类

Sentinel主要提供了这五种的流量控制,接下来我们每种都给大家们演示一下.

在这里插入图片描述

6.5 Sentinel规则-流控

6.5.1 流控规则

流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时

对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

在这里插入图片描述

资源名:唯一名称,默认是请求路径,可自定义

针对来源:指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制

阈值类型/单机阈值

  • QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流

  • 线程数:当调用该接口的线程数达到阈值的时候,进行限流

是否集群:暂不需要集群

6.5.1.1 QPS流控

前面6.4.4案例就是演示的QPS流控

6.5.1.2 线程数流控
  1. 删除掉之前的QPS流控,新增线程数流控

    在这里插入图片描述

  2. 在Jmeter中新增线程

    在这里插入图片描述

    在这里插入图片描述

  3. 访问 http://localhost:8091/sentinel2 会发现已经被限流

    在这里插入图片描述

6.5.2 流控模式

点击上面设置流控规则的编辑按钮,然后在编辑页面点击高级选项,会看到有流控模式一栏。

在这里插入图片描述

sentinel共有三种流控模式,分别是:

  • 直接(默认):接口达到限流条件时,开启限流

  • 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]

  • 链路:当从某个接口过来的资源达到限流条件时,开启限流

6.5.2.1 直接流控模式

前面演示的案例就是这种.

6.5.2.2 关联流控模式

关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流。

场景:当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢.

  1. 在SentinelController.java中增加一个方法,重启订单服务

    @RequestMapping("/sentinel3")
    public String sentinel3(){
    	return "sentinel3";
    }
    
  2. 配置限流规则, 将流控模式设置为关联,关联资源设置为的 /sentinel2

    在这里插入图片描述

  3. 通过postman软件向/sentinel2连续发送请求,注意QPS一定要大于3

    在这里插入图片描述

    在这里插入图片描述

  4. 访问/sentinel3,会发现已经被限流

    在这里插入图片描述

6.5.2.3 链路流控模式

链路流控模式指的是,当从某个接口过来的资源达到限流条件时,开启限流。

  1. 在shop-order-server项目的application.yml文件中新增如下配置:

    spring:
      cloud:
        sentinel:
          web-context-unify: false
    
  2. 在shop-order-server项目中新增TraceServiceImpl.java

    package cn.wolfcode.service.impl;
    @Service
    @Slf4j
    public class TraceServiceImpl {
        @SentinelResource(value = "tranceService")
        public void tranceService(){
            log.info("调用tranceService方法");
        }
    }
    
  3. 在shop-order-server项目中新增TraceController.java

    package cn.wolfcode.controller;
    @RestController
    public class TraceController {
        @Autowired
        private TraceServiceImpl traceService;
        @RequestMapping("/trace1")
        public String trace1(){
            traceService.tranceService();
            return "trace1";
        }
        @RequestMapping("/trace2")
        public String trace2(){
            traceService.tranceService();
            return "trace2";
        }
    }
    
  4. 重新启动订单服务并添加链路流控规则

    在这里插入图片描述

  5. 分别通过 /trace1 和 /trace2 访问, 发现/trace1没问题, /trace2的被限流了

在这里插入图片描述

6.5.3 流控效果

  • 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果

  • Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的

    1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。

  • 排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设

    置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。

6.6 Sentinel规则-降级

降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:

  • 慢调用比例: 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例: 当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数:当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

6.6.1 慢调用比例案例

  1. 在shop-order-server项目中新增FallBackController.java类,代码如下:

    package cn.wolfcode.controller;
    @RestController
    @Slf4j
    public class FallBackController {
        @RequestMapping("/fallBack1")
        public String fallBack1(){
            try {
                log.info("fallBack1执行业务逻辑");
                //模拟业务耗时
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "fallBack1";
        }
    }
    
  2. 新增降级规则:

    在这里插入图片描述

    上面配置表示,如果在1S之内,有【超过1个的请求】且这些请求中【响应时间>最大RT】的【请求数量比例>10%】,就会触发熔断,在接下来的10s之内都不会调用真实方法,直接走降级方法。

    比如: 最大RT=900,比例阈值=0.1,熔断时长=10,最小请求数=10

    • 情况1: 1秒内的有20个请求,只有10个请求响应时间>900ms, 那慢调用比例=0.5,这种情况就会触发熔断

    • 情况2: 1秒内的有20个请求,只有1个请求响应时间>900ms, 那慢调用比例=0.05,这种情况不会触发熔断

    • 情况3: 1秒内的有8个请求,只有6个请求响应时间>900ms, 那慢调用比例=0.75,这种情况不会触发熔断,因为最小请求数这个条件没有满足.

    注意: 我们做实验的时候把最小请求数设置为1,因为在1秒内,手动操作很难在1s内发两个请求过去,所以要做出效果,最好把最小请求数设置为1。

6.6.2 异常比例案例

  1. 在shop-order-server项目的FallBackController.java类新增fallBack2方法:

    int i=0;
    @RequestMapping("/fallBack2")
    public String fallBack2(){
    	log.info("fallBack2执行业务逻辑");
    	//模拟出现异常,异常比例为33%
        if(++i%3==0){
    		throw new RuntimeException();
    	}
    	return "fallBack2";
    }
    
  2. 新增降级规则:

    在这里插入图片描述

    上面配置表示,在1s之内,,有【超过3个的请求】,异常比例30%的情况下,触发熔断,熔断时长为10s.

6.3.3 异常数案例

  1. 在shop-order-server项目的FallBackController.java类新增fallBack3方法:

    @RequestMapping("/fallBack3")
    public String fallBack3(String name){
    	log.info("fallBack3执行业务逻辑");
    	if("wolfcode".equals(name)){
    		throw new RuntimeException();
    	}
    	return "fallBack3";
    }
    
  2. 新增降级规则

    在这里插入图片描述

    上面配置表示,在1s之内,,有【超过3个的请求】,请求中超过2个请求出现异常就会触发熔断,熔断时长为10s

6.7 Sentinel规则-热点

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

  1. 在shop-order-server项目中新增HotSpotController.java,代码如下:

    package cn.wolfcode.controller;
    @RestController
    @Slf4j
    public class HotSpotController {
        @RequestMapping("/hotSpot1")
        @SentinelResource(value = "hotSpot1")
        public String hotSpot1(Long productId){
            log.info("访问编号为:{}的商品",productId);
            return "hotSpot1";
        }
    }
    

    注意:一定需要在请求方法上贴@SentinelResource直接,否则热点规则无效

  2. 新增热点规则:

    在这里插入图片描述

  3. 在热点规则中编辑规则,在编辑之前一定要先访问一下/hotSpot1,不然参数规则无法新增.

    在这里插入图片描述

  4. 新增参数规则:

    在这里插入图片描述
    **

  5. 点击保存,可以看到已经新增了参数规则.

    在这里插入图片描述

  6. 访问http://localhost:8091/hotSpot?productId=1 访问会降级

    访问http://localhost:8091/hotSpot?productId=2 访问不会降级

    在这里插入图片描述

6.8 Sentinel规则-授权

很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

  1. 在shop-order-server中新建RequestOriginParserDefinition.java,定义请求来源如何获取

    @Component
    public class RequestOriginParserDefinition implements RequestOriginParser {
        @Override
        public String parseOrigin(HttpServletRequest request) {
            /**
             *  定义从请求的什么地方获取来源信息
             *  比如我们可以要求所有的客户端需要在请求头中携带来源信息
             */
            String serviceName = request.getParameter("serviceName");
            return serviceName;
        }
    }
    
  2. 在shop-order-server中新建AuthController.java,代码如下:

    @RestController
    @Slf4j
    public class AuthController {
        @RequestMapping("/auth1")
        public String auth1(String serviceName){
            log.info("应用:{},访问接口",serviceName);
            return "auth1";
        }
    }
    
  3. 新增授权规则

    在这里插入图片描述

  4. 访问测试

    访问http://localhost:8091/auth1?serviceName=pc 不能访问

    访问http://localhost:8091/auth1?serviceName=app 可以访问

    在这里插入图片描述

6.9 Sentinel规则-系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

系统规则支持以下的模式:

  • Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

在这里插入图片描述

6.10 Sentinel 自定义异常返回

当前面设定的规则没有满足是,我们可以自定义异常返回.

  • FlowException 限流异常

  • DegradeException 降级异常

  • ParamFlowException 参数限流异常

  • AuthorityException 授权异常

  • SystemBlockException 系统负载异常

在shop-order-server项目中定义异常返回处理类

package cn.wolfcode.error;
@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        response.setContentType("application/json;charset=utf-8");
        ResultData data = null;
        if (e instanceof FlowException) {
            data = new ResultData(-1, "接口被限流了");
        } else if (e instanceof DegradeException) {
            data = new ResultData(-2, "接口被降级了");
        }else if (e instanceof ParamFlowException) {
            data = new ResultData(-3, "参数限流异常");
        }else if (e instanceof AuthorityException) {
            data = new ResultData(-4, "授权异常");
        }else if (e instanceof SystemBlockException) {
            data = new ResultData(-5, "接口被降级了...");
        }
        response.getWriter().write(JSON.toJSONString(data));
    }
}
@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor//无参构造
class ResultData {
    private int code;
    private String message;
}

6.11 @SentinelResource的使用

在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进行保护。同时还能

通过@SentinelResource来指定出现异常时的处理策略。

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。

其主要参数如下:

属性作用
value资源名称,必需项(不能为空)
entryTypeentry 类型,可选项(默认为 EntryType.OUT
blockHandler/blockHandlerClassblockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallback/fallbackClassfallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
1. 返回值类型必须与原函数返回值类型一致;
2.方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
3.fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
1. 返回值类型必须与原函数返回值类型一致;
2. 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
3. defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

定义限流和降级后的处理方法

直接将限流和降级方法定义在方法中

package cn.wolfcode.controller;
@RestController
@Slf4j
public class AnnoController {
    @RequestMapping("/anno1")
    @SentinelResource(value = "anno1",
            blockHandler="anno1BlockHandler",
            fallback = "anno1Fallback"
    )
    public String anno1(String name){
        if("wolfcode".equals(name)){
            throw new RuntimeException();
        }
        return "anno1";
    }
    public String anno1BlockHandler(String name,BlockException ex){
        log.error("{}", ex);
        return "接口被限流或者降级了";
    }
    //Throwable时进入的方法
    public String anno1Fallback(String name,Throwable throwable) {
        log.error("{}", throwable);
        return "接口发生异常了";
    }
}

6.12 Sentinel规则持久化

​ 通过前面的讲解,我们已经知道,可以通过Dashboard来为每个Sentinel客户端设置各种各样的规

则,但是这里有一个问题,就是这些规则默认是存放在内存中,极不稳定,所以需要将其持久化。

​ 本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更

新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:

在这里插入图片描述

首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的

规则保存到本地的文件中。

  1. 编写处理类

    package cn.wolfcode.sentinel;
    public class FilePersistence implements InitFunc {
        @Value("${spring.application.name}")
        private String appcationName;
    
        @Override
        public void init() throws Exception {
            String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + appcationName;
            String flowRulePath = ruleDir + "/flow-rule.json";
            String degradeRulePath = ruleDir + "/degrade-rule.json";
            String systemRulePath = ruleDir + "/system-rule.json";
            String authorityRulePath = ruleDir + "/authority-rule.json";
            String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
    
            this.mkdirIfNotExits(ruleDir);
            this.createFileIfNotExits(flowRulePath);
            this.createFileIfNotExits(degradeRulePath);
            this.createFileIfNotExits(systemRulePath);
            this.createFileIfNotExits(authorityRulePath);
            this.createFileIfNotExits(paramFlowRulePath);
    
            // 流控规则
            ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
                    flowRulePath,
                    flowRuleListParser
            );
            FlowRuleManager.register2Property(flowRuleRDS.getProperty());
            WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
                    flowRulePath,
                    this::encodeJson
            );
            WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
    
            // 降级规则
            ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
                    degradeRulePath,
                    degradeRuleListParser
            );
            DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
            WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
                    degradeRulePath,
                    this::encodeJson
            );
            WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
    
            // 系统规则
            ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
                    systemRulePath,
                    systemRuleListParser
            );
            SystemRuleManager.register2Property(systemRuleRDS.getProperty());
            WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
                    systemRulePath,
                    this::encodeJson
            );
            WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
    
            // 授权规则
            ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
                    authorityRulePath,
                    authorityRuleListParser
            );
            AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
            WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
                    authorityRulePath,
                    this::encodeJson
            );
            WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
    
            // 热点参数规则
            ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
                    paramFlowRulePath,
                    paramFlowRuleListParser
            );
            ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
            WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
                    paramFlowRulePath,
                    this::encodeJson
            );
            ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
        }
    
        private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
                source,
                new TypeReference<List<FlowRule>>() {
                }
        );
        private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
                source,
                new TypeReference<List<DegradeRule>>() {
                }
        );
        private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
                source,
                new TypeReference<List<SystemRule>>() {
                }
        );
    
        private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
                source,
                new TypeReference<List<AuthorityRule>>() {
                }
        );
    
        private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
                source,
                new TypeReference<List<ParamFlowRule>>() {
                }
        );
    
        private void mkdirIfNotExits(String filePath) throws IOException {
            File file = new File(filePath);
            if (!file.exists()) {
                file.mkdirs();
            }
        }
    
        private void createFileIfNotExits(String filePath) throws IOException {
            File file = new File(filePath);
            if (!file.exists()) {
                file.createNewFile();
            }
        }
        private <T> String encodeJson(T t) {
            return JSON.toJSONString(t);
        }
    }
    
  2. 添加配置

    resources下创建配置目录 META-INF/services ,然后添加文件 com.alibaba.csp.sentinel.init.InitFunc

    在文件中添加配置类的全路径

    cn.wolfcode.sentinel.FilePersistence

6.13 Feign整合Sentinel

  1. 在shop-order-server项目的配置文件中开启feign对Sentinel的支持

    feign:
      sentinel:
        enabled: true
    
  2. 创建容错类

@Component
public class ProductFeignFallBack implements ProductFeignApi {
    @Override
    public Product findByPid(Long pid) {
        Product product = new Product();
        product.setPid(-1L);
        product.setPname("兜底数据");
        product.setPprice(0.0);
        return product;
    }
}
  1. 在feign接口中定义容错类

    @FeignClient(name = "product-service",fallback = ProductFeignFallBack.class)
    public interface ProductFeignApi {
        @RequestMapping("/product")
        public Product findByPid(@RequestParam("pid") Long pid);
    }
    
  2. 停止所有 商品服务,重启 shop-order 服务,访问请求,观察容错效果

在这里插入图片描述

可能上面的案例并不是特别恰当,我们只是通过案例来演示Feign集成Sentinel实现降级的效果. 接下来我们具体更贴切的案例来讲解Feign降级的作用.

比如我们在购物的时候,查看商品详情页面的时候,里面包含库存信息,商品详情信息,评论信息,这个需求包含的微服务如下:

在这里插入图片描述

假设现在评论服务宕机了,那是不是意味用户发出查看商品请求也无法正常显示了,商品都看不到了,那用户也无法进行下单的操作了. 但是对于用户来说,评论看不到并不影响他购物,所以这时候我们应该对评论服务进行及·降级处理,返回一个兜底数据(空数据),这样用户的查看商品请求能正常显示,只是评论数据看不到而已,这样的话,用户的下单请求也不会受到影响.

服务网关Gateway

大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用

这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调

用。

在这里插入图片描述

这样的架构,会存在着诸多的问题:

  • 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性

  • 认证复杂,每个服务都需要独立认证。

  • 微服务做集群的情况下,客户端并没有负责均衡的功能

上面的这些问题可以借助API网关来解决。

所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服

务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。

添加上API网关之后,系统的架构图变成了如下所示:

在这里插入图片描述

网关是如何知道微服务的地址?网关如何进行负载均衡呢?

网关需要将自己的信息注册到注册中心上并且拉取其他微服务的信息,然后再调用的时候基于Ribbon实现负载均衡

7.1 常见网关介绍

  • Ngnix+lua

使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用,lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本

  • Kong

基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。 问题:只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。

  • Zuul

Netflflix开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺乏管控,无法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx,Spring Cloud Gateway

Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。

注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway

来做网关

7.2 Gateway简介

​ Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术

开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代

Netflflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安

全,监控和限流。 to_days(start_date) = to_days(now())

优点:

  • 性能强劲:是第一代网关Zuul的1.6倍

  • 功能强大:内置了很多实用的功能,例如转发、监控、限流等

  • 设计优雅,容易扩展

缺点:

  • 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高

  • 不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行

  • 需要Spring Boot 2.0及以上的版本,才支持

7.3 Gateway快速入门

  1. 创建一个 api-gateway 的模块,导入相关依赖

    <?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>shop-parent</artifactId>
            <groupId>cn.wolfcode</groupId>
            <version>1.0.0</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>api-gateway</artifactId>
        <dependencies>
            <!--gateway网关-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <!--nacos客户端-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
        </dependencies>
    </project>
    
  2. 编写启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    public class ApiGatewayServer {
        public static void main(String[] args) {
            SpringApplication.run(ApiGatewayServer.class,args);
        }
    }
    
  3. 编写配置文件

    server:
      port: 9000
    spring:
      application:
        name: api-gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          discovery:
            locator:
              enabled: true # 让gateway可以发现nacos中的微服务
    
  4. 启动测试

    在这里插入图片描述

7.4 自定义路由规则

  1. 在application.yml中添加新的路由规则

    spring:
      application:
        gateway:
          routes:
            - id: product_route
              uri: lb://product-service 
              predicates:
                - Path=/product-serv/**
              filters:
                - StripPrefix=1
            - id: order_route
              uri: lb://order-service 
              predicates:
                - Path=/order-serv/**
              filters:
                - StripPrefix=1
    
  2. 启动测试

    在这里插入图片描述

7.5 Gateway概念

路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个

信息:

  • id,路由标识符,区别于其他 Route。

  • uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。

  • order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。

  • predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。

  • filter,过滤器用于修改请求和响应信息。

执行流程图:

在这里插入图片描述

7.6 过滤器Filter

过滤器就是在请求的传递过程中,对请求和响应做一些手脚.

在Gateway中, Filter的生命周期只有两个:“pre” 和 “post”。

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择

请求的微服务、记录调试信息等。

  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP

Header、收集统计信息和指标、将响应从微服务发送给客户端等。

在Gateway中,Filter的作用范围两种:

  • GatewayFilter:应用到单个路由或者一个分组的路由上。

  • GlobalFilter:应用到所有的路由上

7.6.1 局部路由过滤器

局部过滤器是针对单个路由的过滤器,在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器,有兴趣大家可以自行了解,这里太多了,我们就不一一讲解,我们主要来讲一下自定义路由过滤器。

需求: 统计订单服务调用耗时.

  1. 编写Filter类,注意名称是有固定格式xxxGatewayFilterFactory

    @Component
    public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> {
    
        /**
         * Parts key.
         */
        public static final String PARTS_KEY = "parts";
    
        public TimeGatewayFilterFactory() {
            super(TimeGatewayFilterFactory.Config.class);
        }
    
        @Override
        public List<String> shortcutFieldOrder() {
            return Arrays.asList(PARTS_KEY);
        }
    
        public GatewayFilter apply(TimeGatewayFilterFactory.Config config) {
            return new GatewayFilter() {
                @Override
                public Mono<Void> filter(ServerWebExchange exchange,
                                         GatewayFilterChain chain) {
    
    //                System.out.println("前置逻辑");
    
                    if(!config.isParts()){
                        return chain.filter(exchange);
                    }
                    /**
                     * 前置这里去记录一下时间
                     */
                    exchange.getAttributes().put("beginTime",new Date().getTime());
    
                    return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
                        @Override
                        public void run() {
                            // 记录一下时间, 然后相减
                            System.out.println("后置逻辑");
                            long endTime = new Date().getTime();
                            long beginTime = (long) exchange.getAttributes().get("beginTime");
                            System.out.println("最终耗时"+(endTime-beginTime) +"ms");
    
                        }
                    }));
                }
            };
        }
    
        @Getter@Setter
        public static class Config {
            private boolean parts;
        }
    }
    
  2. 在指定的路由中添加路由规则

    server:
      port: 9000
    spring:
      application:
        name: api-gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          discovery:
            locator:
              enabled: true 
          routes:
            - id: product_route
              uri: lb://product-service 
              predicates:
                - Path=/product-serv/**
              filters:
                - StripPrefix=1
            - id: order_route
              uri: lb://order-service 
              predicates:
                - Path=/order-serv/**
              filters:
                - StripPrefix=1
                - Time=true
    
  3. 访问商品服务的时候是没有打印日志的,访问订单服务的时候打印入职如下:

    在这里插入图片描述

7.6.2 全局路由过滤器

全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功

能。

SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NH550vuH-1627442113215)(图片/网关全局过滤器.jpg)]

需求: 实现统一鉴权的功能,我们需要在网关判断请求中是否包含token且,如果没有则不转发路由,有则执行正常逻辑。

  1. 编写全局过滤类

    package cn.wolfcode.filters;
    @Component
    public class AuthGlobalFilter implements GlobalFilter {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String token = exchange.getRequest().getQueryParams().getFirst("token");
            if (StringUtils.isBlank(token)) {
                System.out.println("鉴权失败");
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
            return chain.filter(exchange);
        }
    }
    
  2. 启动并测试

    在这里插入图片描述

    在这里插入图片描述

7.7 集成Sentinel实现网关限流

网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前

面学过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进

行限流。

从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:

  • route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId

  • 自定义API维度:用户可以利用Sentinel提供的API来自定义一些API分组

7.7.1 网关集成Sentinel

https://github.com/alibaba/Sentinel/wiki/网关限流

  1. 添加依赖

    <dependency>
    	<groupId>com.alibaba.csp</groupId>
    	<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    </dependency>
    
  2. 编写配置类

    package cn.wolfcode.config;
    @Configuration
    public class GatewayConfiguration {
        private final List<ViewResolver> viewResolvers;
        private final ServerCodecConfigurer serverCodecConfigurer;
    
        public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                    ServerCodecConfigurer serverCodecConfigurer) {
            this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
            this.serverCodecConfigurer = serverCodecConfigurer;
        }
        // 配置限流的异常处理器
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
            // Register the block exception handler for Spring Cloud Gateway.
            return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
        }
        // 初始化一个限流的过滤器
        @Bean
        @Order(Ordered.HIGHEST_PRECEDENCE)
        public GlobalFilter sentinelGatewayFilter() {
            return new SentinelGatewayFilter();
        }
        //增加对商品微服务的 限流
         @PostConstruct
        private void initGatewayRules() {
            Set<GatewayFlowRule> rules = new HashSet<>();
            rules.add(new GatewayFlowRule("product_route")
                    .setCount(3)
                    .setIntervalSec(1)
            );
            GatewayRuleManager.loadRules(rules);
        }
    }
    
  3. 重启网关服务并测试.

在这里插入图片描述

7.7.2 修改限流默认返回格式

  1. 在配置类GatewayConfiguration.java中添加如下配置

    @PostConstruct
    public void initBlockHandlers() {
    	BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
    	public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
    		Map map = new HashMap<>();
    		map.put("code", 0);
    		map.put("message", "接口被限流了");
    			return ServerResponse.status(HttpStatus.OK).
    				contentType(MediaType.APPLICATION_JSON).
    				body(BodyInserters.fromValue(map));
                }
    };
    	GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }
    
  2. 重启并测试

    在这里插入图片描述

7.7.3 自定义API分组

自定义API分组是一种更细粒度的限流规则定义

  1. 在shop-order-server项目中添加ApiController

    package cn.wolfcode.controller;
    @RestController
    @RequestMapping("/api")
    public class ApiController {
        @RequestMapping("/hello")
        public String api1(){
            return "api";
        }
    }
    
  2. 重启shop-order-server项目.

  3. 在api-gateway项目的配置GatewayConfiguration.java中添加如下配置:

    @PostConstruct
    private void initCustomizedApis() {
    	Set<ApiDefinition> definitions = new HashSet<>();
    	ApiDefinition api1 = new ApiDefinition("order_api")
                    .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                        add(new ApiPathPredicateItem().setPattern("/order-serv/api/**").                 setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                    }});
    	definitions.add(api1);
    	GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }
    @PostConstruct
    private void initGatewayRules() {
    	Set<GatewayFlowRule> rules = new HashSet<>();
    	rules.add(new GatewayFlowRule("product_route")
                    .setCount(3)
                    .setIntervalSec(1)
    	);
    	rules.add(new GatewayFlowRule("order_api").
                    setCount(1).
                    setIntervalSec(1));
        GatewayRuleManager.loadRules(rules);
    }
    
  4. 直接访问http://localhost:8091/api/hello 是不会发生限流的,访问http://localhost:9000/order-serv/api/hello 就会出现限流了.

链路追踪 Sleuth&Zipkin

微服务架构是一个分布式架构,它按业务划分服务单元,一个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,一个请求可能需要调用很多个服务,而内部服务的调用复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进一个请求到底有哪些服务参与,参与的顺序又是怎样的,从而达到每个请求的步骤清晰可见,出了问题,很快定位。

分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

8.1 常见的链路追踪技术

  • cat :由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成

方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成

成本较高。风险较大。

  • zipkin :由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微

服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth

使用较为简单, 集成很方便, 但是功能较简单。

  • pinpoint: Pinpoint是韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点

是支持多种插件,UI功能强大,接入端无代码侵入。

  • skywalking:SkyWalking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多

种插件,UI功能较强,接入端无代码侵入。目前已加入Apache孵化器。

  • Sleuth

SpringCloud 提供的分布式系统中链路追踪解决方案。

8.2 集成链路追踪组件Sleuth

  1. 在product-server和order-server中添加sleuth依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    
  2. 订单微服务调用商品微服务,这个流程中通过@Slfj打印日志.重启服务并访问测试

    订单服务日志结果:

    在这里插入图片描述

    商品服务日志结果:

    在这里插入图片描述

8.3 日志参数解释

日志格式:
[order-server,c323c72e7009c077,fba72d9c65745e60,false]

1、第一个值,spring.application.name的值

2、第二个值,c323c72e7009c077 ,sleuth生成的一个ID,叫Trace ID,用来标识一条请求链路,一条请求链路中包含一个Trace ID,多个Span ID

3、第三个值,fba72d9c65745e60、spanID 基本的工作单元,获取元数据,如发送一个http

4、第四个值:true,是否要将该信息输出到zipkin服务中来收集和展示。

8.4 Zipkin+Sleuth整合

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

官网:https://zipkin.io/

  1. 下载Zipkin的jar包,在官网可以下载.

  2. 通过命令行,输入下面的命令启动ZipKin Server

    java -jar zipkin-server-2.22.1-exec.jar
    
  3. 通过浏览器访问 http://localhost:9411访问

    在这里插入图片描述

  4. 在订单微服务和商品微服务中添加zipkin依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
    
  5. 在订单微服务和商品微服务中添加如下配置:

    spring:
      zipkin:
        base-url: http://127.0.0.1:9411/ #zipkin server的请求地址
        discoveryClientEnabled: false #让nacos把它当成一个URL,而不要当做服务名
      sleuth: 
        sampler: 
          probability: 1.0 #采样的百分比
    
  6. 重启订单微服务和商品微服务,访问 http://localhost:8091/save?uid=1&pid=1

  7. 访问zipkin的UI界面,观察效果

在这里插入图片描述

在这里插入图片描述

配置中心 Nacos Config

9.1 服务配置中心介绍

首先我们来看一下,微服务架构下关于配置文件的一些问题:

  1. 配置文件相对分散。在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散

在各个微服务中,不好统一配置和管理。

  1. 配置文件无法区分环境。微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环

境。每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动

维护,这比较困难。

  1. 配置文件无法实时更新。我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一

个正在运行的项目来说是非常不友好的。

基于上面这些问题,我们就需要配置中心的加入来解决这些问题。

配置中心的思路是

首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。

当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置。

当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动

态更新。

当加入了服务配置中心之后,我们的系统架构图会变成下面这样:

在这里插入图片描述

9.2 常见的服务配置中心

  • Apollo

Apollo是由携程开源的分布式配置中心。特点有很多,比如:配置更新之后可以实时生效,支持灰

度发布功能,并且能对所有的配置进行版本管理、操作审计等功能,提供开放平台API。并且资料

也写的很详细。

  • Disconf

Disconf是由百度开源的分布式配置中心。它是基于Zookeeper来实现配置变更后实时通知和生效

的。

  • SpringCloud Confifig

这是Spring Cloud中带的配置中心组件。它和Spring是无缝集成,使用起来非常方便,并且它的配

置存储支持Git。不过它没有可视化的操作界面,配置的生效也不是实时的,需要重启或去刷新。

  • Nacos

这是SpingCloud alibaba技术栈中的一个组件,前面我们已经使用它做过服务注册中心。其实它也

集成了服务配置的功能,我们可以直接使用它作为服务配置中心。

9.3 Nacos Confifig入门

​ 使用nacos作为配置中心,其实就是将nacos当做一个服务端,将各个微服务看成是客户端,我们

将各个微服务的配置文件统一存放在nacos上,然后各个微服务从nacos上拉取配置即可。

接下来我们以商品微服务为例,学习nacos confifig的使用。

  1. 搭建nacos环境【使用现有的nacos环境即可】

  2. 在商品微服务中引入nacos的依赖

    <dependency>
    	<groupId>com.alibaba.cloud</groupId>
    	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> 
    </dependency>
    
  3. 在微服务中添加nacos config的配置

    注意:不能使用原来的application.yml作为配置文件,而是新建一个bootstrap.yml作为配置文件

    配置文件优先级(由高到低):
    bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
    
    spring:
      application:
        name: product-service
      cloud:
        nacos:
          config:
            server-addr: 127.0.0.1:8848 #nacos中心地址
            file-extension: yaml # 配置文件格式
      profiles: 
      	active: dev # 环境标识
    
  4. 在nacos中添加配置,然后把商品微服务application.yml配置复制到配置内容中.

    在这里插入图片描述

    在这里插入图片描述

  5. 注释本地的application.yam中的内容, 启动程序进行测试

  6. 如果依旧可以成功访问程序,说明我们nacos的配置中心功能已经实现

9.4 配置动态刷新

​ 在入门案例中,我们实现了配置的远程存放,但是此时如果修改了配置,我们的程序是无法读取到的,因此,我们需要开启配置的动态刷新功能.

  1. 在nacos中的product-service-dev.yaml配置项中添加下面配置:

    appConfig:
      name: product2020
    
  2. 在商品微服务中新增NacosConfigControlller.java

    @RestController
    @RefreshScope
    public class NacosConfigController {
        @Value("${appConfig.name}")
        private String appConfigName;
        @RequestMapping("/nacosConfig1")
        public String nacosConfig(){
            return "远程信息:"+appConfigName;
        }
    }
    

9.5 配置共享

当配置越来越多的时候,我们就发现有很多配置是重复的,这时候就考虑可不可以将公共配置文件提取出来,然后实现共享呢?当然是可以的。接下来我们就来探讨如何实现这一功能。

  • 同一个微服务的不同环境之间共享配置

如果想在同一个微服务的不同环境之间实现配置共享,其实很简单。只需要提取一个以 spring.application.name 命名的配置文件,然后将其所有环境的公共配置放在里面即可。

  1. 新建一个名为product-service.yaml配置存放商品微服务的公共配置,把之前的公共配置都存放进去.

    在这里插入图片描述

  2. 新建一个名为product-service-test.yaml配置存放测试环境的配置

    在这里插入图片描述

  3. 新建一个名为product-service-dev.yaml配置存放测试环境的配置

    在这里插入图片描述

  4. 需要的配置信息具体如下:

    在这里插入图片描述

  5. 在NacosConfigController.java中新增如下逻辑

    @RestController
    @RefreshScope
    public class NacosConfigController {
        @Value("${appConfig.name}")
        private String appConfigName;
        @Value("${env}")
        private String env;
        @RequestMapping("/nacosConfig1")
        public String nacosConfig(){
            return "远程信息:"+appConfigName;
        }
        @RequestMapping("/nacosConfig2")
        public String nacosConfig2(){
            return "公共配置:"+appConfigName+",环境配置信息:"+env;
        }
    }
    
  6. 通过修改环境,参看是否可以读取公共配置和环境独有配置

    在这里插入图片描述

  • 不同微服务中间共享配置

不同为服务之间实现配置共享的原理类似于文件引入,就是定义一个公共配置,然后在当前配置中引

入。

  1. 在nacos中定义一个DataID为global-config.yaml的配置,用于所有微服务共享
globalConfig: global

在这里插入图片描述

  1. 修改bootstrap.yaml

    spring:
      application:
        name: product-service
      cloud:
        nacos:
          config:
            server-addr: 127.0.0.1:8848 #nacos中心地址
            file-extension: yaml # 配置文件格式
            shared-configs:
              - data-id: redis-config.yaml # 配置要引入的配置
                refresh: true
      profiles:
        active: test # 环境标识
    
  2. 在NacosConfigController.java中新增一个方法

    @RestController
    @RefreshScope
    public class NacosConfigController {
        @Value("${appConfig.name}")
        private String appConfigName;
        @Value("${env}")
        private String env;
        @Value("${globalConfig}")
        private String globalConfig;
        @RequestMapping("/nacosConfig1")
        public String nacosConfig(){
            return "远程信息:"+appConfigName;
        }
        @RequestMapping("/nacosConfig2")
        public String nacosConfig2(){
            return "公共配置:"+appConfigName+",环境配置信息:"+env;
        }
        @RequestMapping("/nacosConfig3")
        public String nacosConfig3(){
            return "全局配置:"+globalConfig+",公共配置:"+appConfigName+",环境配置信息:"+env;
        }
    }
    

动态刷新功能.

  1. 在nacos中的product-service-dev.yaml配置项中添加下面配置:

    appConfig:
      name: product2020
    
  2. 在商品微服务中新增NacosConfigControlller.java

    @RestController
    @RefreshScope
    public class NacosConfigController {
        @Value("${appConfig.name}")
        private String appConfigName;
        @RequestMapping("/nacosConfig1")
        public String nacosConfig(){
            return "远程信息:"+appConfigName;
        }
    }
    

9.5 配置共享

当配置越来越多的时候,我们就发现有很多配置是重复的,这时候就考虑可不可以将公共配置文件提取出来,然后实现共享呢?当然是可以的。接下来我们就来探讨如何实现这一功能。

  • 同一个微服务的不同环境之间共享配置

如果想在同一个微服务的不同环境之间实现配置共享,其实很简单。只需要提取一个以 spring.application.name 命名的配置文件,然后将其所有环境的公共配置放在里面即可。

  1. 新建一个名为product-service.yaml配置存放商品微服务的公共配置,把之前的公共配置都存放进去.

    在这里插入图片描述

  2. 新建一个名为product-service-test.yaml配置存放测试环境的配置

    在这里插入图片描述

  3. 新建一个名为product-service-dev.yaml配置存放测试环境的配置

    在这里插入图片描述

  4. 需要的配置信息具体如下:

    在这里插入图片描述

  5. 在NacosConfigController.java中新增如下逻辑

    @RestController
    @RefreshScope
    public class NacosConfigController {
        @Value("${appConfig.name}")
        private String appConfigName;
        @Value("${env}")
        private String env;
        @RequestMapping("/nacosConfig1")
        public String nacosConfig(){
            return "远程信息:"+appConfigName;
        }
        @RequestMapping("/nacosConfig2")
        public String nacosConfig2(){
            return "公共配置:"+appConfigName+",环境配置信息:"+env;
        }
    }
    
  6. 通过修改环境,参看是否可以读取公共配置和环境独有配置

    在这里插入图片描述

  • 不同微服务中间共享配置

不同为服务之间实现配置共享的原理类似于文件引入,就是定义一个公共配置,然后在当前配置中引

入。

  1. 在nacos中定义一个DataID为global-config.yaml的配置,用于所有微服务共享
globalConfig: global

在这里插入图片描述

  1. 修改bootstrap.yaml

    spring:
      application:
        name: product-service
      cloud:
        nacos:
          config:
            server-addr: 127.0.0.1:8848 #nacos中心地址
            file-extension: yaml # 配置文件格式
            shared-configs:
              - data-id: redis-config.yaml # 配置要引入的配置
                refresh: true
      profiles:
        active: test # 环境标识
    
  2. 在NacosConfigController.java中新增一个方法

    @RestController
    @RefreshScope
    public class NacosConfigController {
        @Value("${appConfig.name}")
        private String appConfigName;
        @Value("${env}")
        private String env;
        @Value("${globalConfig}")
        private String globalConfig;
        @RequestMapping("/nacosConfig1")
        public String nacosConfig(){
            return "远程信息:"+appConfigName;
        }
        @RequestMapping("/nacosConfig2")
        public String nacosConfig2(){
            return "公共配置:"+appConfigName+",环境配置信息:"+env;
        }
        @RequestMapping("/nacosConfig3")
        public String nacosConfig3(){
            return "全局配置:"+globalConfig+",公共配置:"+appConfigName+",环境配置信息:"+env;
        }
    }
    
  3. 重启服务并测试.

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值