SpringCloud

单体架构

单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
单体架构的优缺点如下:
优点: 架构简单、部署成本低
缺点: 耦合度高(维护困难、升级困难)

分布式架构

分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
分布式架构的优缺点:
优点: 降低服务耦合、有利于服务升级和拓展
缺点: 服务调用关系错综复杂

微服务的架构特征

单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
自治:团队独立、技术独立、数据独立,独立部署和交付
面向服务:服务提供统一标准的接口,与语言和技术无关
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

SpringCloud

SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。

服务拆分原则

不同微服务,不要重复开发相同业务
微服务数据独立,不要访问其它微服务的数据库
微服务可以将自己的业务暴露为接口,供其它微服务调用

实现远程调用

1,user-service项目

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

2,注入RestTemplate对象

springboot的主程序类

MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
    //创建RestTemplate对象并注入spring容器
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

3,order-service服务中,跨服务的远程调用

order-service模块的OrderService

/**
 * 实现了跨服务的远程调用,其实就是发送了一次http请求
 */
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        
        //2利用RestTemplate发起http请求,查询用户
        String url = "http://localhost:8081/user/"+order.getUserId();
        //发送http请求,实现远程调用
        //get请求,设置返回值json封装成User对象
        User user = restTemplate.getForObject(url, User.class);
        
        //3封装user到order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

4,测试

http://localhost:8080/order/101

{
	"id":101,"price":699900,"name":"Apple 苹果 iPhone 12 ","num":1,
	"userId":1,
	"user":{"id":1,"username":"柳岩","address":"湖南省衡阳市"}
}

提供者与消费者

服务提供者:
一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)

服务消费者:
一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)

但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。
如果服务A调用了服务B,而服务B又调用了服务C,服务B的角色是什么?
对于A调用B的业务而言:A是服务消费者,B是服务提供者
对于B调用C的业务而言:B是服务消费者,C是服务提供者
因此,服务B既可以是服务提供者,也可以是服务消费者。

Eureka注册中心

在这里插入图片描述

问题1:order-service如何得知user-service实例地址?

获取地址信息的流程如下:

- user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务端)。这个叫服务注册
- eureka-server保存服务名称到服务实例地址列表的映射关系
- order-service根据服务名称,拉取实例地址列表。这个叫服务发现或服务拉取

问题2:order-service如何从多个user-service实例中选择具体的实例?

- order-service从实例列表中利用负载均衡算法选中一个实例地址
- 向该实例地址发起远程调用

问题3:order-service如何得知某个user-service实例是否依然健康,是不是已经宕机?

- user-service会每隔一段时间(默认30秒)向eureka-server发起请求,报告自己状态,称为心跳
- 当超过一定时间没有发送心跳时,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除
- order-service拉取服务时,就能将故障实例排除了

> 注意:一个微服务,既可以是服务提供者,又可以是服务消费者,因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端

1,搭建eureka-server

1.1,创建eureka-server服务

1.1.1,引入eureka依赖
<dependencies>
    <!--eureka服务端,无需指定版本号,因为父工程中springboot已经指定-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>
1.1.2,编写启动类
@EnableEurekaServer//自动装配开关
@SpringBootApplication
public class EurekeApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekeApplication.class,args);
    }
}
1.1.3,编写配置文件

application.yml

server:
  port: 10086 # 服务端口
spring:
  application:
    name: eurekaserver # eureka的服务名称
eureka:
  client:
    service-url:  # eureka的地址信息 eureka会将自己也注册到eureka服务中
      defaultZone: http://127.0.0.1:10086/eureka
1.1.4,启动服务

启动微服务,然后在浏览器访问:http://127.0.0.1:10086
在这里插入图片描述

2,服务注册

2.1,引入依赖

在user-service,order-service的pom文件中,引入下面的eureka-client依赖:

<!--eureka客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.2,配置文件application.yml

在user-service,order-service中,修改application.yml文件,添加服务名称、eureka地址:

2.2.1,order-service的application.yml
server:
  port: 8080
spring:
  datasource:省略

  application:
        name: orderservice # order服务名称

eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

mybatis:省略
logging:省略
2.2.2,user-service的application.yml
server:
  port: 8081
spring:
  datasource:省略

  application:
    name: userservice # user服务名称

eureka:
  client:
    service-url:  # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

mybatis:省略
logging:省略
2.3,启动两个user-service实例

在这里插入图片描述

2.4,查看eureka-server管理页面

在这里插入图片描述

3,服务发现

从eureka-server拉取user-service的信息,实现服务发现

3.1,负载均衡

RestTemplate这个Bean添加一个@LoadBalanced注解:

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
    
    @LoadBalanced //加入负载均衡,order去调取user或者user2
    @Bean //创建RestTemplate对象并注入spring容器
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
3.2,修改userservice服务名称
/**
 * 实现了跨服务的远程调用,其实就是发送了一次http请求
 */
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        
        //2利用RestTemplate发起http请求,查询用户
        //userservice是user-service application.yml配置的映射名
        //userservice替代localhost:8081
        String url = "http://userservice/user/"+order.getUserId();
        
        //发送http请求,实现远程调用
        //get请求,设置返回值json封装成User对象
        User user = restTemplate.getForObject(url, User.class);
        //3封装user到order
        order.setUser(user);
        // 4.返回
        return order;
    }
}
3.3,测试
http://localhost:8080/order/101

{
	"id":101,"price":699900,"name":"Apple 苹果 iPhone 12 ","num":1,
	"userId":1,
	"user":{"id":1,"username":"柳岩","address":"湖南省衡阳市"}
}

Ribbon负载均衡

在这里插入图片描述

基本流程如下:

- 拦截我们的RestTemplate请求http://userservice/user/1
- RibbonLoadBalancerClient会从请求url中获取服务名称,也就是user-service
- DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表
- eureka返回列表,localhost:8081、localhost:8082
- IRule利用内置负载均衡规则,从列表中选择一个,例如localhost:8081
- RibbonLoadBalancerClient修改请求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,发起真实请求

负载均衡策略

负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
在这里插入图片描述
不同规则的含义如下:默认的实现就是ZoneAvoidanceRule,是一种轮询方案

内置负载均衡规则类规则描述
RoundRobinRule简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule随机选择一个可用的服务器。
RetryRule重试机制的选择逻辑

修改负载均衡策略

通过定义IRule实现可以修改负载均衡规则,有两种方式:

方式1:代码方式(作用于order-service中调用别的任何微服务)

在order-service中的OrderApplication类中,定义一个新的IRule:

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    //加入负载均衡,order去调取user或者user2
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

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

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

}
方式2:配置文件方式(只作用于userservice)

在order-service的application.yml文件中,添加新的配置也可以修改规则:

server:
  port: 8080
spring:
  datasource:省略
  application:
    name: orderservice # order服务名称

eureka:
  client:
    service-url:  # eureka的地址信息 eureka会将自己也注册到eureka服务中
      defaultZone: http://127.0.0.1:10086/eureka
      
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

mybatis:省略
logging:省略

饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:

server:
  port: 8080
spring:
  datasource:省略
  application:
    name: orderservice # order服务名称

eureka:
  client:
    service-url:  # eureka的地址信息 eureka会将自己也注册到eureka服务中
      defaultZone: http://127.0.0.1:10086/eureka
      
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
    
ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
      - userservice

mybatis:省略
logging:省略

Nacos注册中心

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

Nacos Windows安装

将nacos-server-1.4.1.zip解压到任意非中文目录下
在安装目录,shift右击,打开"Windows PowerShell",执行".\startup.cmd -m standalone"启动nacos
访问	http://192.168.0.106:8848/nacos/index.html
输入用户名nacos,密码nacos登录

服务注册到nacos

Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:依赖不同、服务地址不同
注释掉eureka的依赖。

1,引入依赖

在cloud-demo父工程的pom文件中的<dependencyManagement>中引入SpringCloudAlibaba的依赖:

<!--nacos的管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.6.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

然后在user-service和order-service中的pom文件中引入nacos-discovery依赖,注释掉eureka依赖:

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

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2,配置nacos地址

在user-service和order-service的application.yml中添加nacos地址:
注意:需要注释掉eureka的地址

server:
  port: 8081
spring:
  datasource:省略
  application:
    name: userservice # user服务名称
  cloud:
    nacos:
      server-addr: localhost:8848

#eureka:
#  client:
#    service-url:  # eureka的地址信息 eureka会将自己也注册到eureka服务中
#      defaultZone: http://127.0.0.1:10086/eureka

userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
      - userservice

mybatis:省略
logging:省略
3,重启微服务

重启微服务后,登录nacos管理页面,可以看到微服务信息:
http://192.168.0.106:8848/nacos/index.html
在这里插入图片描述

nacos服务分级存储模型

Nacos就将同一机房内的实例 划分为一个集群
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:
在这里插入图片描述
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群

给user-service配置集群
1,修改user-service的application.yml文件,添加集群配置:
server:
  port: 8081
spring:
  datasource:省略
  application:
    name: userservice # user服务名称
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: bj XA # 集群名称,自定义XA表示西安集群 BJ代表北京集群

mybatis:省略
logging:省略
2,重启3个user-service实例后,我们可以在nacos控制台看到下面结果:

user-service服务
在这里插入图片描述
相关集群详情
在这里插入图片描述

同集群优先的负载均衡NacosRule

默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中随机挑选实例

1,给order-service配置集群信息并且修改负载均衡规则

修改order-service的application.yml文件,添加集群配置、修改负载均衡规则:

server:
  port: 8080
spring:
  datasource:省略
  application:
    name: orderservice # order服务名称
  cloud:
    nacos:
      server-addr: localhost:8848
      # 1,给order-service配置集群信息
      discovery:
         cluster-name: xa # 集群名称,自定义XA表示西安集群 BJ代表北京集群

userservice: # 配置负载均衡规则
  ribbon:
  	# 2,修改负载均衡规则
    NFLoadBalancerRuleClassName:
    	com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: # 指定饥饿加载的服务名称,这里是个集合,可以指定多个
      - userservice

mybatis:省略
logging:省略

2,order service访问时就会优先访问本集群的user service。

但是如果本集群user service异常,也会访问别的集群,但是会报警告,

A cross-cluster call occurs

权重配置

实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
注意:如果权重修改为0,则该实例永远不会被访问
在这里插入图片描述

环境隔离

Nacos提供了namespace来实现环境隔离功能。
nacos中可以有多个namespace
namespace下可以有group、service等
不同namespace之间相互隔离,例如不同namespace的服务互相不可见
在这里插入图片描述

创建namespace

默认情况下,所有service、data、group都在同一个namespace,名为public:
我们可以点击页面新增按钮,添加一个namespace:
在这里插入图片描述
然后,填写表单:
在这里插入图片描述
就能在页面看到一个新的namespace:
在这里插入图片描述

给微服务配置namespace

给微服务配置namespace只能通过修改配置来实现。
例如,修改order-service的application.yml文件:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver

  application:
        name: orderservice # order服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
      discovery:
        cluster-name: XA  # 集群名称,自定义XA表示西安集群 BJ代表北京集群
        namespace: 6269166d-9ecb-4cc9-bd97-739fdf6afd7b  # 命名空间,填ID

重启order-service后,访问控制台,可以看到下面的结果:
在这里插入图片描述
在这里插入图片描述
此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:

java.lang.IllegalStateException: No instances available for userservice

Nacos与Eureka的区别

Nacos的服务实例分为两种类型:

临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
配置一个服务实例为永久实例:

spring:
  datasource:省略

  application:
        name: orderservice # order服务名称
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
      discovery:
        cluster-name: XA  # 集群名称,自定义XA表示西安集群 BJ代表北京集群
        namespace: 6269166d-9ecb-4cc9-bd97-739fdf6afd7b # 命名空间,填ID
        ephemeral: false # 是否是临时实例

Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
在这里插入图片描述

Nacos与eureka的共同点

都支持服务注册和服务拉取
都支持服务提供者心跳方式做健康检测

Nacos与Eureka的区别

Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
临时实例心跳不正常会被剔除,非临时实例则不会被剔除
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

Nacos配置管理

Nacos除了可以做注册中心,同样可以做配置管理来使用。

Nacos统一配置管理

当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
在这里插入图片描述

在nacos中添加配置文件

如何在nacos中管理配置呢?
在这里插入图片描述
然后在弹出的表单中,填写配置信息:
注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
userservice-dev.yaml
在这里插入图片描述

pattern:
    dateformat: yyyy-MM-dd HH:mm:ss

从微服务拉取配置

微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。
但如果尚未读取application.yml,又如何得知nacos地址呢?
因此spring引入了一种新的配置文件:bootstrap.yaml文件,会在application.yml之前被读取,流程如下:
在这里插入图片描述

1,添加nacos配置
nacos配置管理-->配置列表-->新建userservice-dev.yaml
pattern:
    dateformat: yyyy-MM-dd HH:mm:ss
2,引入nacos-config依赖

首先,在user-service服务中,引入nacos-config的客户端依赖:

<!--nacos配置管理依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
3,添加bootstrap.yaml

然后,在user-service中添加一个bootstrap.yaml文件,内容如下:

spring:
  application:
    name: userservice # 服务名称
  profiles:
    active: dev # 开发环境,这里是dev 
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
        file-extension: yaml # 文件后缀名

这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id,来读取配置。
本例中,就是去读取userservice-dev.yaml
在这里插入图片描述

4,读取nacos配置

在user-service中的UserController中添加业务逻辑,读取pattern.dateformat配置:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
5,测试
http://localhost:8081/user/now
显示如下:
2024-05-06 17:55:21

nacos配置热更新

修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新
要实现配置热更新,可以使用两种方式:
在这里插入图片描述

方式一:在@Value注入的变量,所在类上添加注解@RefreshScope
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;
    
    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}
方式二:使用@ConfigurationProperties注解代替@Value注解

在user-service服务中,添加一个类,读取patterrn.dateformat属性:
@ConfigurationProperties注解使用
配置类

@Data
@Component
//@ConfigurationPropertie将application.yml配置的值注入到bean上,必须将对象注入IOC容器中才有配置绑定的功能
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
}

Controller类

@Slf4j
@RestController
@RequestMapping("/user")
//@RefreshScope
public class UserController {
    @Autowired
    private PatternProperties properties;

    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
    }
}

nacos配置共享

userservice.yaml不包含环境,可以被多个环境dev、prod等共享。

1,添加一个环境共享配置

在nacos中添加一个userservice.yaml文件:
在这里插入图片描述

2,在user-service中读取共享配置

在user-service服务中,修改PatternProperties类,读取新添加的属性:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
//@ConfigurationPropertie将application.yml配置的值注入到bean上,必须将对象注入IOC容器中才有配置绑定的功能
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
    private String dateformat;
    private String envSharedValue;
}

在user-service服务中,修改UserController,添加一个方法:

@Slf4j
@RestController
@RequestMapping("/user")
//@RefreshScope
public class UserController {
    @Autowired
    private PatternProperties properties;

    @GetMapping("prop")
    public PatternProperties properties(){
        return properties;
    }
}
3,使用不同的profile

不管是dev,还是test环境,都读取到了envSharedValue这个属性的值

http://localhost:8081/user/prop

{
  "dateformat": "yyyy-MM-dd HH:mm:ss",
  "envSharedValue": "多环境共享"
}

配置共享的优先级

nacos配置 > 本地配置
nacos各环境配置 > nacos不包含环境配置
在这里插入图片描述

Nacos集群搭建

其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。我们计划的集群结构:
在这里插入图片描述三个nacos节点的地址:

节点ipport
nacos1192.168.150.18845
nacos2192.168.150.18846
nacos3192.168.150.18847

搭建集群

搭建集群的基本步骤:

1,搭建数据库,初始化数据库表结构,

首先新建一个数据库,命名为nacos,而后导入下面的SQL:

/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info   */
/******************************************/
CREATE TABLE `config_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) DEFAULT NULL,
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL  COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL  COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  `c_desc` varchar(256) DEFAULT NULL,
  `c_use` varchar(64) DEFAULT NULL,
  `effect` varchar(64) DEFAULT NULL,
  `type` varchar(64) DEFAULT NULL,
  `c_schema` text,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
 
/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_aggr   */
/******************************************/
CREATE TABLE `config_info_aggr` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(255) NOT NULL COMMENT 'group_id',
  `datum_id` varchar(255) NOT NULL COMMENT 'datum_id',
  `content` longtext NOT NULL COMMENT '内容',
  `gmt_modified` datetime NOT NULL COMMENT '修改时间',
  `app_name` varchar(128) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
 
 
/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_beta   */
/******************************************/
CREATE TABLE `config_info_beta` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL  COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL  COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
 
/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_info_tag   */
/******************************************/
CREATE TABLE `config_info_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `tag_id` varchar(128) NOT NULL COMMENT 'tag_id',
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL COMMENT 'content',
  `md5` varchar(32) DEFAULT NULL COMMENT 'md5',
  `gmt_create` datetime NOT NULL  COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL  COMMENT '修改时间',
  `src_user` text COMMENT 'source user',
  `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
 
/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = config_tags_relation   */
/******************************************/
CREATE TABLE `config_tags_relation` (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `tag_name` varchar(128) NOT NULL COMMENT 'tag_name',
  `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type',
  `data_id` varchar(255) NOT NULL COMMENT 'data_id',
  `group_id` varchar(128) NOT NULL COMMENT 'group_id',
  `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id',
  `nid` bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`nid`),
  UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
 
/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = group_capacity   */
/******************************************/
CREATE TABLE `group_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL  COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL  COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_group_id` (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
 
/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = his_config_info   */
/******************************************/
CREATE TABLE `his_config_info` (
  `id` bigint(64) unsigned NOT NULL,
  `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `data_id` varchar(255) NOT NULL,
  `group_id` varchar(128) NOT NULL,
  `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name',
  `content` longtext NOT NULL,
  `md5` varchar(32) DEFAULT NULL,
  `gmt_create` datetime NOT NULL ,
  `gmt_modified` datetime NOT NULL ,
  `src_user` text,
  `src_ip` varchar(50) DEFAULT NULL,
  `op_type` char(10) DEFAULT NULL,
  `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段',
  PRIMARY KEY (`nid`),
  KEY `idx_gmt_create` (`gmt_create`),
  KEY `idx_gmt_modified` (`gmt_modified`),
  KEY `idx_did` (`data_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
 
 
/******************************************/
/*   数据库全名 = nacos_config   */
/*   表名称 = tenant_capacity   */
/******************************************/
CREATE TABLE `tenant_capacity` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID',
  `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值',
  `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量',
  `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值',
  `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数',
  `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值',
  `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量',
  `gmt_create` datetime NOT NULL  COMMENT '创建时间',
  `gmt_modified` datetime NOT NULL  COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
 
 
CREATE TABLE `tenant_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `kp` varchar(128) NOT NULL COMMENT 'kp',
  `tenant_id` varchar(128) default '' COMMENT 'tenant_id',
  `tenant_name` varchar(128) default '' COMMENT 'tenant_name',
  `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc',
  `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source',
  `gmt_create` bigint(20) NOT NULL COMMENT '创建时间',
  `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),
  KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
 
CREATE TABLE `users` (
	`username` varchar(50) NOT NULL PRIMARY KEY,
	`password` varchar(500) NOT NULL,
	`enabled` boolean NOT NULL
);
 
CREATE TABLE `roles` (
	`username` varchar(50) NOT NULL,
	`role` varchar(50) NOT NULL,
	UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE
);
 
CREATE TABLE `permissions` (
    `role` varchar(50) NOT NULL,
    `resource` varchar(255) NOT NULL,
    `action` varchar(8) NOT NULL,
    UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE
);
 
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
 
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
2,下载nacos安装包
3,配置nacos,

进入nacos的conf目录,然后修改application.properties文件,添加数据库配置
application.properties

spring.datasource.platform=mysql

db.num=1

db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root

修改配置文件cluster.conf.example,重命名为cluster.conf

127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847
4,启动nacos集群

将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3
然后分别修改三个文件夹中的application.properties,

nacos1中修改:server.port=8845
nacos2中修改:server.port=8846
nacos3中修改:server.port=8847

然后分别启动三个nacos节点:

shift+右击,打开Windows powershell,运行startup.cmd命令
5,nginx反向代理

修改conf/nginx.conf文件,配置如下:

upstream nacos-cluster {
    server 127.0.0.1:8845;
	server 127.0.0.1:8846;
	server 127.0.0.1:8847;
}

server {
    listen       81;
    server_name  localhost;

    location /nacos {
        proxy_pass http://nacos-cluster;
    }
}

而后在浏览器访问:http://localhost:81/nacos即可。
userservice代码中application.yml文件配置如下:

spring:
  cloud:
    nacos:
      server-addr: localhost:81 # Nacos地址

启动userservice1,userservice2就可以在nacos控制台看到服务列表里有启动的userservice了
遇到问题:无法向配置列表中添加配置,本地安装的nacos集群是1.x版本,也不是网上说的需要增加个字段的问题

Feign远程调用

以前利用RestTemplate发起远程调用的代码:

存在下面的问题:
代码可读性差,编程体验不统一
参数复杂URL难以维护

@Autowired
private RestTemplate restTemplate;

public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    //2利用RestTemplate发起http请求,查询用户
    //服务地址"userservice"是映射的Instances currently registered with Eureka上的application名字
    String url = "http://userservice/user/"+order.getUserId();
    //发送http请求,实现远程调用
    //get请求,设置返回值json封装成User对象
    User user = restTemplate.getForObject(url, User.class);
    //3封装user到order
    order.setUser(user);
    // 4.返回
    return order;
}

Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。

Feign替代RestTemplate

1,引入依赖

我们在order-service服务的pom文件中引入feign的依赖:

<!--feign客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2,添加@EnableFeignClients注解

在order-service的启动类添加注解开启Feign的功能:

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients//自动装配开关
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
3,编写Feign的客户端

在order-service中新建一个接口,内容如下:

@FeignClient("userservice")
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}
4,使用FeignClient中定义的方法代替RestTemplate

修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:

//实现了跨服务的远程调用,其实就是发送了一次http请求
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        //2使用feign远程调用
        User user = userClient.findById(order.getUserId());
        //3封装user到order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

Feign自定义配置如日志级别

Feign可以支持很多的自定义配置,如下表所示:

类型作用说明
feign.Logger.Level修改日志级别包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder响应结果的解析器http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder请求参数编码将请求参数编码,便于通过http请求发送
feign. Contract支持的注解格式默认是SpringMVC的注解
feign. Retryer失败重试机制请求失败的重试机制,默认是没有,不过会使用Ribbon的重试

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。
以日志为例来演示如何自定义配置

配置文件方式

基于配置文件修改feign的日志级别可以针对单个服务:

feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

也可以针对所有服务:

feign:  
  client:
    config: 
      default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

而日志的级别分为四种:
NONE:不记录任何日志信息,这是默认值。
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

Java代码方式

先声明一个类,然后声明一个Logger.Level的对象:

public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

@MapperScan("cn.benjamin.order.mapper")
@SpringBootApplication
//自动装配开关,全局有效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {

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

如果是局部生效,则把它放到对应的@FeignClient这个注解中:

//局部有效
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

使用连接池优化Feign

Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

底层客户端连接池优化手段
URLConnection默认实现,不支持连接池
Apache HttpClient支持连接池提高Feign的性能主要手段就是使用连接池代替默认的URLConnection
OKHttp支持连接池提高Feign的性能主要手段就是使用连接池代替默认的URLConnection

这里我们用Apache的HttpClient来演示。
1,引入依赖
在order-service的pom文件中引入Apache的HttpClient依赖:

<!--feign客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--引入feign HttpClient依赖-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

2,配置连接池
在order-service的application.yml中添加配置,日志级别尽量用basic

feign:
  client:
    config:
      default: # default全局的配置
        loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 单个路径的最大连接数

最佳实践

Feign的客户端与服务提供者的controller代码非常相似:

继承方式

一样的代码可以通过继承来共享:
1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。
2)Feign客户端和Controller都集成改接口
在这里插入图片描述

抽取方式

将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
在这里插入图片描述

1,抽取 首先创建一个module,命名为feign-api

在feign-api中然后引入feign的starter依赖

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

然后,order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中

@FeignClient(value = "userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}
public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level logLevel(){
        return Logger.Level.BASIC;
    }
}
@Data
public class User {
    private Long id;
    private String username;
    private String address;
}
2,在order-service中使用feign-api

首先,删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。
在order-service的pom文件中中引入feign-api的依赖:
注意:如果使用的是idea ide的多模块话,需要模块之间的依赖。需要把parent工程,也就是package是pom的那个工程先install一下

<dependency>
    <groupId>cn.test.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>

修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包

import cn.test.feign.clients.UserClient;
import cn.test.feign.config.DefaultFeignConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
@MapperScan("cn.test.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
import cn.test.feign.clients.UserClient;
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.用Feign远程调用
        User user = userClient.findById(order.getUserId());
        // 3.封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

3,解决扫描包问题

因为@SpringBootApplication只会扫描到本项目下的包,com.api.clients.UserClient是引入的第三方jar,需要再指定扫描

方式一:

指定Feign应该扫描的包:

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
//自动装配开关,全局有效
//basePackages指定feign client所在包
@EnableFeignClients(basePackages = "com.api.clients",defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}
方式二:

指定需要加载的Client接口:推荐方式二

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
//自动装配开关,全局有效
//clients = UserClient.class指定加载哪个客户端
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

Gateway服务网关

为什么需要网关?

Gateway网关是我们服务的守门神,所有微服务的统一入口。
在这里插入图片描述

网关的核心功能特性:

核心功能remark
权限控制网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。

gateway快速入门

1,新建SpringBoot工程gateway,引入nacos服务发现和gateway依赖
<!--网关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>
2,编写启动类
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class,args);
    }
}
3,配置application.yml,包括服务基本信息、nacos地址、路由
路由配置包括:
1. 路由id:路由的唯一标示
2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
3. 路由断言(predicates):判断路由的规则,
4. 路由过滤器(filters):对请求或响应做处理

创建application.yml文件,内容如下:

server:
  port: 10010

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
4,启动网关服务进行测试

重启网关,访问http://localhost:10010/user/1时,符合/user/**规则,请求转发到http://userservice/user/1

predicates断言工厂

我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来
处理的,像这样的断言工厂在SpringCloudGateway还有十几个:

名称说明示例
After是某个时间点后的请求- After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before是某个时间点之前的请求- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between是某两个时间点之前的请求- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie请求必须包含某些cookie- Cookie=chocolate, ch.p
Header请求必须包含某些header- Header=X-Request-Id, \d+
Host请求必须是访问某个host(域名)- Host=**.somehost.org, * *.anotherhost.org
Method请求方式必须是指定方式- Method=GET,POST
Path请求路径必须符合指定规则- Path=/red/{segment},/blue/**
Query请求参数必须包含指定参数- Query=name, Jack或者- Query=name
RemoteAddr请求者的ip必须是指定范围- RemoteAddr=192.168.1.1/24
Weight权重处理

比如Before和Path

server:
  port: 10010

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
            - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] # 时间要求在2031年之前

网关过滤器

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
在这里插入图片描述

路由过滤器的种类

Spring提供了31种不同的路由过滤器工厂。例如:

名称说明
AddRequestHeader给当前请求添加一个请求头
RemoveRequestHeader移除请求中的一个请求头
AddResponseHeader给响应结果中添加一个响应头
RemoveResponseHeader从响应结果中移除有一个响应头
RequestRateLimiter限制请求的流量
局部请求头过滤器

以AddRequestHeader 为例,添加局部过滤器
只需要修改gateway服务的application.yml文件,添加路由过滤即可:
当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。

spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/** 
        filters: # 过滤器
        - AddRequestHeader=Truth, you are freaking awesome! # 添加请求头
全局默认过滤器DefaultFilter

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:

spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/**
      default-filters: # 默认过滤项
      - AddRequestHeader=Truth, you are freaking awesome! 

全局过滤器GlobalFilter

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
自定义全局过滤器例子

1,在gateway中定义一个过滤器
/**
 *  处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
 *
 * @param exchange 请求上下文,里面可以获取Request、Response等信息
 * @param chain 用来把请求委托给下一个过滤器
 * @return {@code Mono<Void>} 返回标示当前过滤器业务结束
 */
@Order(-1)//顺序注解,说明filter执行顺序。数字越大越往后
@Component
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1,获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> queryParams = request.getQueryParams();
        //2,获取参数中的authorization参数
        String authorization = queryParams.getFirst("authorization");
        //3,判断参数值是否等于admin
        if("admin".equals(authorization)){
            //4,放行
            return chain.filter(exchange);
        }
        //5,拦截
        //5.1,设置状态码为401
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        //5.2,拦截请求
        return exchange.getResponse().setComplete();
    }
}
2,重启gateway模块,访问
2.1,携带请求参数,放行
http://localhost:10010/user/1?authorization=admin

{"id":1,"username":"柳岩","address":"湖南省衡阳市"}
2.2,未携带请求参数,拦截请求
http://localhost:10010/user/1

HTTP ERROR 401

过滤器Filter执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
在这里插入图片描述
排序的规则是什么呢?
1,每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
2,GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
3,路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
4,当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。

跨域问题

什么是跨域问题?
跨域:域名不一致就是跨域,主要包括:
域名不同: www.taobao.com 和 www.taobao.org(一级域名不同)
和 www.jd.com 和 miaosha.jd.com(三级域名不同)
域名相同,端口不同:localhost:8080和localhost:8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:

server:
  port: 10010

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
#          filters: # 路由的过滤器
#            - AddRequestHeader=Truth,good good!!!
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**
            - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
      default-filters:
              - AddRequestHeader=Truth,you is freaking awesome!
      
      # 跨域问题解决配置
      globalcors: # 全局的跨域处理
        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
        corsConfigurations:
          '[/**]':
            allowedOrigins: # 允许哪些网站的跨域请求 
              - "http://localhost:8090"
            allowedMethods: # 允许的跨域ajax的请求方式
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*" # 允许在请求中携带的头信息
            allowCredentials: true # 是否允许携带cookie
            maxAge: 360000 # 这次跨域检测的有效期

或者添加以下配置类

@SpringBootConfiguration
public class WebGlobalConfig {

    @Bean
    public CorsFilter corsFilter() {

        //创建CorsConfiguration对象后添加配置
        CorsConfiguration config = new CorsConfiguration();
        //设置放行哪些原始域
        config.addAllowedOriginPattern("*");
        //放行哪些原始请求头部信息
        config.addAllowedHeader("*");
        //暴露哪些头部信息
        config.addExposedHeader("*");
        //放行哪些请求方式
        config.addAllowedMethod("GET");     //get
        config.addAllowedMethod("PUT");     //put
        config.addAllowedMethod("POST");    //post
        config.addAllowedMethod("DELETE");  //delete
        //corsConfig.addAllowedMethod("*");     //放行全部请求

        //是否发送Cookie
        config.setAllowCredentials(true);

        //2. 添加映射路径
        UrlBasedCorsConfigurationSource corsConfigurationSource =
                new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**", config);
        //返回CorsFilter
        return new CorsFilter(corsConfigurationSource);
    }
}

微服务保护

雪崩问题

微服务中,服务间调用关系错综复杂,一个微服务往往依赖于多个其它微服务。服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,那么当前服务也就不可用了。
那么,依赖于当前服务的其它服务随着时间的推移,最终也都会变的不可用,形成级联失败,雪崩就发生了:
在这里插入图片描述

解决雪崩问题的常见方式有四种

流量控制是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施。
超时处理、舱壁模式、熔断降级是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施。

1,超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
在这里插入图片描述
2,舱壁模式:舱壁模式来源于船舱的设计,船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。
于此类似,我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
在这里插入图片描述
3,熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。断路器会统计访问某个服务的请求数量,异常比例
当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成熔断
在这里插入图片描述

4,流量控制:限制业务访问的QPS(每秒处理的请求数量),避免服务因流量的突增而故障。
在这里插入图片描述

安装Sentinel

1,将jar包sentinel-dashboard-1.8.1.jar放到任意非中文目录,执行命令
java -jar sentinel-dashboard-1.8.1.jar

如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:
| **配置项**                       | **默认值** | **说明**   |
| -------------------------------- | ---------- | ---------- |
| server.port                      | 8080       | 服务端口   |
| sentinel.dashboard.auth.username | sentinel   | 默认用户名 |
| sentinel.dashboard.auth.password | sentinel   | 默认密码   |

例如,修改端口:
java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar

2,访问http://localhost:8080页面,就可以看到sentinel的控制台了:
需要输入账号和密码,默认都是:sentinel

微服务整合Sentinel

在order-service中整合sentinel,并连接sentinel的控制台,步骤如下:

1,引入sentinel依赖
<!--sentinel依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId> 
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2,修改application.yaml文件,添加下面内容:
server:
  port: 8088
spring:
  cloud: 
    sentinel:
      transport:
        dashboard: localhost:8080 # sentinel控制台地址
3,访问order-service的任意端点

打开浏览器,访问http://localhost:8088/order/101,这样才能触发sentinel的监控。然后再访问sentinel的控制台查看监控。
在这里插入图片描述

流量控制

簇点链路
当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
例如,我们刚才访问的order-service中的OrderController中的端点:/order/{orderId}
在这里插入图片描述
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
流控:流量控制
降级:降级熔断
热点:热点参数限流,是限流的一种
授权:请求的权限控制

流控模式

在添加限流规则时,点击高级选项,可以选择三种流控模式

流控模式remark
直接模式统计当前资源的请求,触发阈值时对当前资源直接限流,也是默认的模式
关联模式统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
链路模式统计从指定链路访问到本资源的请求,触发阈值时,对指定链路限流

1,流控模式–直接模式

1.1,点击资源/order/{orderId}后面的流控按钮,就可以弹出表单。
在这里插入图片描述
1.2,在sentinel控制台添加限流规则
在这里插入图片描述
1.3,利用jmeter测试
选中流控入门,QPS<5右键运行:
20个用户,2秒内运行完,QPS是10,超过了5
在这里插入图片描述
1.4,查看Sentinel 控制台,可以看到,成功的请求每次只有5个
在这里插入图片描述

2,关联模式

关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
两个有竞争关系的资源
一个优先级较高,一个优先级较低

配置规则
在OrderController新建两个端点:/order/query和/order/update,无需实现业务.配置流控规则,当/order/ update资源被访问的QPS超过5时,对/order/query请求限流
2.1,定义/order/query端点,模拟订单查询。定义/order/update端点,模拟订单更新

@RestController
@RequestMapping("order")
public class OrderController {

   @Autowired
   private OrderService orderService;

    @GetMapping("{orderId}")
    public Order queryContextOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        return orderService.queryOrderById(orderId);
    }

    @GetMapping("/query")
    public String queryOrder(){
        return "查询订单成功";
    }

    @GetMapping("/update")
    public String updateOrder(){
        return "修改订单成功";
    }
}

2.2,重启服务,查看sentinel控制台的簇点链路
在这里插入图片描述
2.3,配置流控规则
对哪个端点限流,就点击哪个端点后面的按钮。我们是对订单查询/order/query限流,因此点击它后面的按钮:
在这里插入图片描述
在表单中填写流控规则:
在这里插入图片描述2.4,在Jmeter测试
可以看到1000个用户,100秒,因此QPS为10,超过了我们设定的阈值:5
在这里插入图片描述

在这里插入图片描述
2.5,但限流的目标是/order/query,我们在浏览器访问,可以发现
在这里插入图片描述

3,链路模式

链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
需求:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。

步骤:
3.1,在OrderService中添加一个queryGoods方法,不用实现业务

@SentinelResource("goods")
public void queryGoods(){
    //查询商品数据库
}

3.2,在OrderController中,改造/order/query端点,调用OrderService中的queryGoods方法

@GetMapping("/query")
public String queryOrder() {
    // 查询商品
    orderService.queryGoods();
    // 查询订单
    System.out.println("查询订单");
    return "查询订单成功";
}

3.3,在OrderController中添加一个/order/save的端点,调用OrderService的queryGoods方法

@GetMapping("/save")
public String saveOrder() {
    // 查询商品
    orderService.queryGoods();
    // 查询订单
    System.err.println("新增订单");
    return "新增订单成功";
}

3.4,给queryGoods设置限流规则,从/order/query进入queryGoods的方法限制QPS必须小于2
只统计从/order/query进入/goods的资源,QPS阈值为2,超出则被限流。
点击goods资源后面的流控按钮,在弹出的表单中填写下面信息:
在这里插入图片描述
3.5,关闭context整合

server:
  port: 8088
spring:
  datasource:省略
  application:
    name: orderservice
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
    sentinel:
      transport:
        dashboard: localhost:8080 # sentinel控制台地址
      web-context-unify: false # 关闭context整合
      datasource:
        flow:
          nacos:
            server-addr: localhost:8848 # nacos地址
            dataId: orderservice-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: flow # 还可以是:degrade、authority、param-flow

3.6,Jmeter测试
在这里插入图片描述
一个http请求是访问/order/save,结果完全不受影响:
在这里插入图片描述
另一个是访问/order/query:
在这里插入图片描述
在这里插入图片描述

流控效果

在流控的高级选项中,还有一个流控效果选项:
在这里插入图片描述
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:

快速失败warm up 预热模式排队等待
达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长

1,warm up 预热模式

阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机。
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor,持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3.
例如设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.
在这里插入图片描述1.1,配置流控规则:
在这里插入图片描述
1.2,Jmeter测试
1.3,Sentinel控制台查看实时监控
在这里插入图片描述

2,排队等待

排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
工作原理
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。

那什么叫做预期等待时长呢?
比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:
第6个请求的预期等待时长 = 200 * (6 - 1) = 1000ms
第12个请求的预期等待时长 = 200 * (12-1) = 2200ms

如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑:
在这里插入图片描述
需求:给/order/{orderId}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s
2.1,添加流控规则
在这里插入图片描述

2.2,Jmeter测试
sentinel查看实时监控的QPS曲线
QPS非常平滑,一致保持在10,但是超出的请求没有被拒绝,而是放入队列。因此响应时间(等待时间)会越来越长。
当队列满了以后,才会有部分请求失败:
在这里插入图片描述

热点参数限流

之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。

刚才的配置中,对查询商品这个接口的所有商品一视同仁,QPS都限定为5.
而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了:

给/order/{orderId}这个资源添加热点参数限流,规则如下:
默认的热点参数规则是每1秒请求量不超过2
给102这个参数设置例外:每1秒请求量不超过4
给103这个参数设置例外:每1秒请求量不超过10

注意事项:热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源
1,标记资源
给order-service中的OrderController中的/order/{orderId}资源添加注解

@RestController
@RequestMapping("order")
public class OrderController {

   @Autowired
   private OrderService orderService;

   @SentinelResource("hot")
   @GetMapping("{orderId}")
   public Order queryContextOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        return orderService.queryOrderById(orderId);
   }
}

2,热点参数限流规则
点击左侧菜单中热点规则菜单
在这里插入图片描述
点击新增,填写表单
在这里插入图片描述

3,Jmeter测试
在这里插入图片描述
4,运行结果
101资源每秒只能成功2个,102资源每秒成功4个,103资源每秒5次请求全部成功

FeignClient整合Sentinel

SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
注意:feign注入报循环依赖问题,一般是<spring-cloud.version>Hoxton.SR8</spring-cloud.version>版本问题
1,修改配置,开启sentinel功能
修改OrderService的application.yml文件,开启Feign的Sentinel功能:

feign:
  httpclient:
    enabled: true # 支持HttpClient的开关
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 单个路径的最大连接数
  sentinel:
    enabled: true # 开启feign对sentinel的支持

2,编写失败降级逻辑
feing-api项目中定义类,实现FallbackFactory

@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable throwable) {
        return new UserClient() {
            @Override
            public User findById(Long id) {
            	//失败降级逻辑
                log.error("查询用户异常", throwable);
                return new User();
            }
        };
    }
}

3,在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean

public class DefaultFeignConfiguration {
	@Bean
	public UserClientFallbackFactory userClientFallbackFactory(){
	    return new UserClientFallbackFactory();
	}
}

4,在feing-api项目中的UserClient接口中使用UserClientFallbackFactory

@FeignClient(value = "userservice", fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

5,重启后,访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路
在这里插入图片描述

线程隔离(舱壁模式)

线程隔离有两种方式实现:

HystixSentinel
Hystix默认是基于线程池实现的线程隔离Sentinel是基于信号量(计数器)实现的线程隔离
每一个被隔离的业务都要创建一个独立的线程池不用创建线程池
支持主动超时,支持异步调用不支持主动超时,不支持异步调用
线程的额外开销比较大,性能一般,但是隔离性更强轻量级,无额外开销,性能较好,但是隔离性一般

在这里插入图片描述

sentinel的线程隔离(信号量隔离)

案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试
1,配置隔离规则
选择feign接口后面的流控按钮:
在这里插入图片描述
填写表单:
在这里插入图片描述
2,Jmeter测试
一次发生10个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
发现虽然结果都是通过了,不过部分请求得到的响应是降级返回的null信息。
在这里插入图片描述

熔断降级

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
在这里插入图片描述

熔断状态
closed关闭状态断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
open打开状态服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
half-open半开状态放行一次请求,根据执行结果来判断接下来的操作。请求成功:则切换到closed状态;请求失败:则切换到open状态

熔断策略

慢调用、异常比例、异常数

慢调用

慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
例如:
1,设置慢调用规则
在这里插入图片描述
解读:RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
2,设置慢调用
修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth", required = false) String truth) throws InterruptedException {
        if (id == 1) {
            // 休眠,触发熔断
            Thread.sleep(500);
        }
        System.out.println("truth:" + truth);
        return userService.queryById(id);
    }
}

3,测试
在浏览器访问:http://localhost:8088/order/101,快速刷新5次,可以发现:
在这里插入图片描述
触发了熔断,请求时长缩短至5ms,快速失败了,直接走降级逻辑,返回的null
在浏览器访问:http://localhost:8088/order/102,也被熔断了,当超过5s再次访问时,请求恢复正常。
在这里插入图片描述

异常比例、异常数

异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。

异常比例设置:

统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。
在这里插入图片描述

异常数设置:

统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于2次,则触发熔断。
在这里插入图片描述

设置异常请求

修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常比例/异常数的熔断:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth", required = false) String truth) throws InterruptedException {
        if (id == 1) {
            // 休眠,触发熔断
            Thread.sleep(500);
       	} else if (id == 2) {
            throw new RuntimeException("故意出错,触发熔断");
        }
        System.out.println("truth:" + truth);
        return userService.queryById(id);
    }
}

授权规则

授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。

白名单:来源(origin)在白名单内的调用者允许访问
黑名单:来源(origin)在黑名单内的调用者不允许访问

其实就是给允许访问的资源加上特定头信息,在RequestOriginParser实现类中进行判断,如果符合授权规则就允许访问

比如:
我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)
在这里插入图片描述
1,例如order-service服务中,我们定义一个RequestOriginParser的实现类:

@Component
public class HeaderOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 1.获取请求头
        String origin = request.getHeader("origin");
        // 2.非空判断
        if (StringUtils.isEmpty(origin)) {
            origin = "blank";
        }
        return origin;
    }
}

2,给网关添加请求头
既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头
修改gateway服务中的application.yml,添加一个defaultFilter:

spring:
  cloud:
    gateway:
      default-filters:
        - AddRequestHeader=origin,gateway
      routes:
        - id: user-service # 路由标示,必须唯一
          uri: lb://userservice # 路由的目标地址
          predicates: # 路由断言,判断请求是否符合规则
            - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
        - id: order-service
          uri: lb://orderservice
          predicates:
            - Path=/order/**

3,配置授权规则
在这里插入图片描述
在这里插入图片描述
4.1,直接访问order-service服务
在这里插入图片描述

4.2,通过网关访问order-service服务
在这里插入图片描述

自定义异常结果

默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
在这里插入图片描述
而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:

public interface BlockExceptionHandler {
    /**
     * 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
     */
    void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}

这里的BlockException包含多个不同的子类:

异常说明
FlowException限流异常
ParamFlowException热点参数限流的异常
DegradeException降级异常
AuthorityException授权规则异常
SystemBlockException系统规则异常

1,自定义异常处理
在order-service定义一个自定义异常处理类:

@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        String msg = "未知异常";
        int status = 429;

        if (e instanceof FlowException) {
            msg = "请求被限流了";
        } else if (e instanceof ParamFlowException) {
            msg = "请求被热点参数限流";
        } else if (e instanceof DegradeException) {
            msg = "请求被降级了";
        } else if (e instanceof AuthorityException) {
            msg = "没有权限访问";
            status = 401;
        }

        response.setContentType("application/json;charset=utf-8");
        response.setStatus(status);
        response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
    }
}

2,重启测试,在不同场景下,会返回不同的异常消息.
限流:

localhost:8088/order/103

{
	"msg": 请求被限流了, 
	"status": 429
}

授权拦截时:

localhost:8088/order/103

{
	"msg": 没有权限访问, 
	"status": 401
}

Sentinel 规则持久化

1,引入依赖 在order-service中引入sentinel监听nacos的依赖:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

2,配置nacos地址
在order-service中的application.yml文件配置nacos地址及监听的配置信息:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos服务地址
    sentinel:
      transport:
        dashboard: localhost:8080 # sentinel控制台地址
      web-context-unify: false # 关闭context整合
      datasource:
        flow:
          nacos:
            server-addr: localhost:8848 # nacos地址
            dataId: orderservice-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: flow # 还可以是:degrade、authority、param-flow

3,修改sentinel-dashboard源码
4,启动修改后的sentinel-dashboard

java -jar sentinel-dashboard.jar

5,添加持久化后的sentinel配置
将流控规则添加到“流控规则-NACOS”
在这里插入图片描述
6,查看nacos控制台
在这里插入图片描述
7,重启order service后,sentinel配置依然在

Spring Cloud是一个用于构建分布式系统的开发工具集合。它提供了一些常用的组件和框架,包括服务注册和发现、负载均衡、断路器、分布式配置等等。在使用Spring Cloud时,有一些常见的错误和注意事项需要注意。 首先,关于Spring BootSpring Cloud版本对应错误。在使用Spring Cloud时,需要确保Spring BootSpring Cloud的版本兼容。不同版本之间可能存在依赖冲突或不兼容的情况,因此需要根据官方文档或者相关文档来选择合适的版本。 另外,Spring Cloud Config是一个用于集中管理和动态获取配置的工具。它支持从Git、SVN或本地文件系统中获取配置文件,并提供了服务器和客户端支持。你可以通过官方使用说明文档了解更多关于Spring Cloud Config的详细信息。 此外,关于选择使用Nacos还是Eureka作为服务注册和发现组件的问题。Nacos是一个功能更强大的服务注册和发现组件,它整合了Spring Cloud Eureka、Spring Cloud Config和Spring Cloud Bus的功能。使用Nacos可以实现配置的中心动态刷新,而不需要为配置中心新增集群或使用消息队列。另一方面,Eureka是Spring Cloud原生全家桶的一部分,相对来说更加稳定一些。选择使用哪个组件需要根据具体的需求和项目特点来决定。 综上所述,Spring Cloud是一个用于构建分布式系统的开发工具集合,它提供了一些常用的组件和框架。在使用Spring Cloud时,需要注意Spring BootSpring Cloud版本的兼容性,并可以使用Spring Cloud Config来动态获取配置。同时,可以选择使用Nacos或Eureka作为服务注册和发现组件,具体选择需要根据项目需求来决定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值