SpringCloud微服务基础

SpringCloud

导学

微服务技术栈

image-20210922233946164

image-20210922234059562

image-20210922234411742

概念

架构

  • 单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署
    • 优点:
      • 架构简单
      • 部署成本低
    • 缺点:
      • 耦合度高
  • 分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
    • 优点:
      • 降低耦合度
      • 有利于服务升级拓展
    • 缺点:
      • 要考虑服务治理
        1. 服务拆分粒度如何?
        2. 服务集群地址如何维护?
        3. 服务之间如何实现远程调用?
        4. 服务健康状态如何感知?
      • 架构非常复杂:运维、监控、部署难度提高

微服务:

微服务是一种经过良好架构设计的分布式架构方案

  • 微服务架构的特征: image-20210923001208086
    • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
    • 面向服务:微服务对外暴露业务接口
    • 自治:团队独立、技术独立、数据独立、部署独立
    • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

微服务结构

微服务这种方案需要技术框架来落地,国内最知名的就是SpringCloud和阿里巴巴的Dubbo

Dubbo SpringCloud SpringCloudAlibaba
注册中心 zookeeper、Redis Eureka、Consul Nacos、Eureka
服务远程调用 Dubbo协议 Feign(http协议) Dubbo、Feign
配置中心 SpringCloudConfig SpringCloudConfig、Nacos
服务网关 SpringCloudGateway、Zuul SpringCloudGateway、Zuul
服务监控和保护 dubbo-admin,功能弱 Hystrix Sentinel

SpringCloud

  • SpringCloud是目前国内使用最广泛的微服务框架
  • SpringCloud继承了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
image-20210923002805853
  • SpringCloud与SpringBoot的版本兼容关系:(本教程使用Hoxton.SR10版本)
image-20210923003141626

服务拆分

  • 注意事项:
    1. 不同微服务,不要重复开发相同的业务
    2. 微服务数据独立,不要访问其他微服务的数据库
    3. 微服务可以将自己的业务暴露为接口,供其他微服务调用

微服务远程调用

  1. 注册RestTemplate为Bean

    1
    2
    3
    4
    @Bean //随意在配置类中注册Bean,建议在SpringBoot启动类
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }
  2. 服务远程调用RestTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Service
    public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate; //注入此Bean
    public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2.根据order对象的userId查询对应的用户信息,把userId发送到对应的url查询并封装
    Long userId = order.getUserId();
    User user = restTemplate.getForObject("http://localhost:8081/user/" + userId, User.class);
    // 3.封装数据
    order.setUser(user);
    // 4.返回
    return order;
    }
    }

调用方式:

  • 基于RestTemplate发起http请求实现远程调用
  • http请求做远程调用是与语言无关的调用,只需要知道对方的url(ip、端口、接口路径、请求参数)即可

Eureka注册中心

提供者与消费者

  • 服务提供者:一次业务中,被其他微服务调用的服务
  • 服务消费者:一次业务中,调用其他微服务的服务

提供者与消费者角色是相对的,一个服务即可以是提供者,又可以是消费者

Eureka的作用

image-20210923133841456

服务调用出现的问题

  • 消费者如何获取提供者的具体信息?
    • 服务提供者启动时向Eureka注册自己的信息
    • Eureka保存这些信息
    • 消费者根据服务名称向Eureka拉取提供者信息
  • 如果有多个服务提供者,消费者该如何选择?
    • 服务消费者利用负载均衡算法,从服务列表中挑选一个
  • 消费者如何感知服务提供者健康状态?
    • 服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
    • Eureka会更新记录服务列表信息,心跳不正常的会被剔除
    • 消费者可以拉取到最新的信息 image-20210923134248868

快速入门

1.搭建EurekaServer服务

  1. 创建项目,引入spring-cloud-starter-netflix-eureka-server的依赖

    1
    2
    3
    4
    5
    6
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    </dependencies>
  2. 编写启动类,添加@EnabledEurekaServer注解

    1
    2
    3
    4
    5
    6
    7
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaApplication {
    public static void main(String[] args) {
    SpringApplication.run(EurekaApplication.class, args);
    }
    }
  3. 在SpringBoot核心配置文件配置Eureka服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 8082 # Eureka的SpringBoot服务端口
    spring:
    application:
    name: eurekaserver # Eureka的服务名称
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:8082/eureka # Eureka的地址信息
    fetch-registry: false # 关闭从Eureka服务器获取信息

2.注册service到EurekaServer(服务注册)

  1. 在需要注册为Eureka客户端的项目中引入spring-cloud-starter-netflix-eureka-client的依赖

    1
    2
    3
    4
    5
    6
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    </dependencies>
  2. 在SpringBoot核心配置文件中配置Eureka客户端

    1
    2
    3
    4
    5
    6
    7
    spring:
    application:
    name: orderservice
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:8082/eureka

3.拉取service从EurekaServer(服务发现)

服务拉取是基于服务名称获取服务列表,然后再对服务列表做负载均衡

  1. 修改service中要访问的url路径,用服务名代替ip、端口

    1
    2
    User user = restTemplate.
    getForObject("http://userservice/user/" + userId, User.class);
  2. 在service的启动类中产生@RestTemplate的Bean上打上@LoadBalanced负载均衡注解

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

Ribbon负载均衡

负载均衡原理

image-20210923164855406

image-20210923170719968

负载均衡策略

image-20210923170909592

默认实现是ZoneAvoidanceRule,是一种轮询方案

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

  • 代码方式:在service启动类中定义一个@Bean方法返回一个IRule实现类

    1
    2
    3
    4
    @Bean
    public IRule randomRule() {
    return new RandomRule();
    }
  • 配置文件方式:在service所在的SpringBoot核心配置文件中配置规则

    1
    2
    3
    userserivce:
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡的规则

懒加载和饥饿加载

Ribbon默认采用懒加载,即第一次访问时才会创建LoadBalanceClient,请求时间会比较长

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时

开启饥饿加载:

1
2
3
4
5
6
ribbon:
eager-load:
enabled: true #开启饥饿加载
clients: #userservice #指定对userservice这个服务进行饥饿加载
- userservice
#- 其他service ,因为clients是一个List<String>

image-20210923223212010

Nacos注册中心

Nacos是阿里巴巴的产品,现在是SpringCloud的一个组件。相比Eureka功能更丰富

快速入门

1.安装Nacos

  • windows环境:

    1. 解压Nacos的zip包

    2. 启动Nacos安装目录下的bin目录下的startup.cmd

      1
      startup.cmd -m standalone
    3. 访问Nacos管理页面:默认用户名密码均为nacos image-20210923225135114

  • docker环境

    1. 拉取nacos server的镜像

    2. 创建配置文件

      1
      2
      3
      mkdir /home/nacos/init.d
      mkdir /home/nacos/logs
      vim /home/nacos/init.d/custom.properties

      配置文件内容:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      server.contextPath=/nacos
      server.servlet.contextPath=/nacos
      server.port=8848

      spring.datasource.platform=mysql

      db.num=1
      db.url.0=jdbc:mysql://120.79.141.53:3307/nacos?#characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
      db.user=root
      db.password=123

      nacos.cmdb.dumpTaskInterval=3600
      nacos.cmdb.eventTaskInterval=10
      nacos.cmdb.labelTaskInterval=300
      nacos.cmdb.loadDataAtStart=false

      management.metrics.export.elastic.enabled=false
      management.metrics.export.influx.enabled=false

      server.tomcat.accesslog.enabled=true
      server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{User-Agent}i

      nacos.security.ignore.urls=/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/login,/v1/console/health/**,/v1/cs/**,/v1/ns/**,/v1/cmdb/**,/actuator/**,/v1/console/server/**
      nacos.naming.distro.taskDispatchThreadCount=1
      nacos.naming.distro.taskDispatchPeriod=200
      nacos.naming.distro.batchSyncKeyCount=1000
      nacos.naming.distro.initDataRatio=0.9
      nacos.naming.distro.syncRetryDelay=5000
      nacos.naming.data.warmup=true
      nacos.naming.expireInstance=true
    3. 启动容器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      docker run \
      --name nacos -d \
      -p 8848:8848 \
      --privileged=true \
      --restart=always \
      -e JVM_XMS=128m \
      -e JVM_XMX=128m \
      -e MODE=standalone \
      -e PREFER_HOST_MODE=hostname \
      -v /home/nacos/logs:/home/nacos/logs \
      -v /home/nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties \
      nacos/nacos-server

2.服务注册与发现

  1. 在父工程添加spring-cloud-alibaba的管理依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <dependencyManagement>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencyManagement>
  2. 去掉service的Eureka依赖

  3. 添加Nacos的客户端依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <!--Eureka客户端依赖-->
    <!--<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>-->
    <!--nacos依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    </dependencies>
  4. 在service的SpringBoot核心配置文件中配置nacos的地址

    1
    2
    3
    4
    spring:
    cloud:
    nacos:
    server-addr: localhost:8848 #nacos的服务地址,默认

image-20210923231151320

3.服务集群属性

  1. 修改service的SpringBoot核心配置文件

    1
    2
    3
    4
    5
    spring:
    cloud:
    nacos:
    discovery:
    cluster-name: shanghai
  2. Nacos控制台可以查看集群

image-20210923232925983

4.NacosRule服务集群优先级

配置负载均衡的规则为NacosRule:

  • java配置
1
2
3
4
@Bean
public IRule nacosRule() {
return new NacosRule();
}
  • service的SpringBoot核心配置文件中配置
1
2
3
userserivce:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #规则

NacosRule的规则如下:

  • 优先选择同集群的服务实例列表
  • 本地集群找不到提供者,才会去其他集群寻找,并且会报警告
  • 确定了可用实例列表后,再采用随机负载均衡挑选实例

5.根据权重负载均衡

  1. 在Nacos控制台设置实例的权重值

    image-20210924002931392
  2. 设置此实例的权重(取值[0,1]之间): image-20210924003021042

image-20210924003043575

6.环境隔离namespace

Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离

image-20210924003227310
  1. 在Nacos控制台创建namespace,用来隔离不同环境

    image-20210924003539852
  2. 填写namespace信息

    image-20210924003635303
  3. 上述步骤之后会生成一个命名空间以及id

  4. 修改service的SpringBoot核心配置文件,添加namespace

    1
    2
    3
    4
    5
    6
    7
    8
    9
    spring:
    application:
    name: orderservice
    cloud:
    nacos:
    server-addr: localhost:8848 #nacos的服务地址
    discovery:
    cluster-name: hangzhou
    namespace: a67c6300-08fb-4f64-9e07-4969f8e95474 #命名空间的id
  5. 此时启动service会导致此service位于指定的命名空间下

  6. 访问此orderservice由于namespace不同,会导致找不到userservice

    image-20210924004140684

image-20210924004158405

Nacos注册中心细节分析

image-20210924004808150

临时实例和非临时实例

服务注册到Nacos时,可以选择临时实例或非临时实例,通过配置SpringBoot核心配置文件:

1
2
3
4
5
spring:
cloud:
nacos:
discovery:
ephemeral: false #是否临时实例

image-20210924005316140

Nacos配置管理

配置Nacos配置

  1. 在Nacos管理页面添加配置

    image-20210924134339815

  2. 填写配置信息

    image-20210924134537042

读取Nacos配置

  1. 引入Nacos的配置管理客户端依赖

    1
    2
    3
    4
    5
    <!--nacos的配置管理依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  2. 在service的resources目录中添加一个bootstrap.yml引导文件,优先级高于application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    application:
    name: userservice #服务名
    profiles:
    active: dev #配置环境名
    cloud:
    nacos:
    server-addr: localhost:8848 #nacos地址
    config:
    file-extension: yaml #配置文件后缀名
  3. 读取Nacos配置信息

    1
    2
    3
    4
    5
    6
    7
    @Value("${pattern.dateformat}")
    private String dateFormat;

    @RequestMapping("now")
    public String getNow() {
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat));
    }

image-20210924165637880

热更新Nacos配置

Nacos中的配置文件变更后,微服务无需重启就可以感知。不过需要下面两种配置实现:

  • 方式一:在@Value注入的变量所在的类上打上@RefreshScope注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Slf4j
    @RestController
    @RefreshScope
    @RequestMapping("/user")
    public class UserController {
    @Autowired
    private UserService userService;
    @Value("${pattern.dateformat}")
    private String dateFormat;
    @RequestMapping("now")
    public String getNow() {
    return LocalDateTime.now().
    format(DateTimeFormatter.ofPattern(dateFormat));
    }
    }
  • 方式二:使用@ConfigurationProperties注解读取配置文件到配置类中,然后从配置类取得配置项的值

    1. 读取到配置类中,并标记此配置类是一个Bean以方便其他地方注入使用

      1
      2
      3
      4
      5
      6
      @Data
      @Component
      @ConfigurationProperties(prefix = "pattern")
      public class PatternProperties {
      private String dateFormat;
      }
    2. 注入配置类Bean并使用配置项的值

      1
      2
      3
      4
      5
      6
      7
      8
      @Autowired
      private PatternProperties properties; //注入配置类的Bean

      @RequestMapping("now")
      public String getNow() {
      return LocalDateTime.now().
      format(DateTimeFormatter.ofPattern(properties.getDateFormat()));
      }

      注意:这种方式不需要@RefreshScope注解

image-20210924171537858

多环境配置共享

微服务在启动时会从Nacos读取多个配置文件

  • [spring.application.name]-[spring.profiles.active].yaml,例如userservice-dev.yaml
  • [spring.application.name].yaml,例如userservice.yaml。这种情况下与环境无关

无论环境如何变化,[spring.application.name]不会变化,所以[spring.application.name].yaml一定会加载

  • 服务名.yaml可以被所有spring.application.name相同的服务读取到,属于共享配置

  • 多种配置优先级:在本地application.yml、Nacos配置的某环境yaml、Nacos配置的通用共享yaml中:

    服务名-环境名.yaml > 服务名.yaml > 本地配置.yaml image-20210924174006951

Nacos集群

1.Nacos集群结构图

image-20210925001827900

2.搭建集群:步骤

  • 搭建数据库集群,初始化数据库表结构

    • Nacos默认数据存储在内嵌的Derby数据库中,不属于生产可用的数据库
    • 官方推荐的最佳实践是使用带有主从的高可用数据库集群,主从模式的高可用数据库自学
    • 这里以单点的数据库为例:
      1. 新建一个数据库,命名为nacos
      2. 导入nacos建表有关SQL(注意字段为datetime类型赋默认值需要MySQL5.6版本以上)
  • 下载nacos安装包

  • 配置nacos

    • 配置nacos安装目录下的conf目录下的cluster.conf,设置集群的节点

      1
      2
      3
      127.0.0.1:8845
      127.0.0.1.8846
      127.0.0.1.8847
    • 配置nacos安装目录下的conf目录下的application.properties,配置数据库

      1
      2
      3
      4
      5
      6
      7
      8
      9
      #*************** Config Module Related Configurations ***************#
      ### If use MySQL as datasource:
      spring.datasource.platform=mysql
      ### Count of DB:
      db.num=1
      ### Connect URL of DB:
      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=199988
    • 配置多个nacos服务器的端口(8845、8846、8847)

      1
      2
      ### Default web server port:
      server.port=8845

      JVM内存不够的问题!

      配置nacos安装目录下的bin目录下的启动脚本,修改cluster模式下的JVM参数

      1
      2
      3
      4
      5
      6
      7
      8
      rem if nacos startup mode is cluster
      if %MODE% == "cluster" (
      echo "nacos is starting with cluster"
      if %EMBEDDED_STORAGE% == "embedded" (
      set "NACOS_OPTS=-DembeddedStorage=true"
      )
      set "NACOS_JVM_OPTS=-server -Xms512m -Xmx512m -Xmn256m 后面省略了
      )
  • 启动nacos集群

    • 直接运行startup.cmd不需要再设置以单击启动的参数了
  • 配置nginx反向代理

    • 编辑nginx安装目录下的conf目录,在http标签内部加入nginx配置:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      upstream nacos-cluster {
      server 127.0.0.1:8845;
      server 127.0.0.1:8846;
      server 127.0.0.1:8847;
      }
      server {
      listen 80;
      server_name localhost;
      location /nacos {
      proxy_pass http://nacos-cluster;
      }
      }
    • 启动nginx

      • 直接运行start nginx.exe
      • 停止nginx:nginx.exe -s stop或者nginx.exe -s quit,建议用quit优雅退出
    • 访问http:localhost:80/nacos即可代理到nacos-cluster所定义的几个负载均衡结点上

3.测试Nacos集群

  • 访问http:localhost:80/nacos会被代理到负载均衡的节点上
  • 在Nacos管理页面创建配置会保存到本地MySQL的config_info数据库中

image-20210925012202746

Feign远程调用

RestTemplate方式调用存在的问题

1
2
User user = restTemplate.
getForObject("http://userservice/user/" + userId, User.class);
  • 代码可读性差,编程体验不统一
  • 参数复杂时URL难以维护

Feign的概念

Feign是一个声明式的http客户端,其作用就是帮助我们优雅的实现http请求的发送,解决以上的问题

快速入门

  1. 引入起步依赖

    1
    2
    3
    4
    5
    <!--Feign客户端-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 在service的启动类上添加@EnableFeignClients注解开启Feign的功能

    1
    2
    @EnableFeignClients
    public class OrderApplication { ... }
  3. 编写Feign客户端的接口

    1
    2
    3
    4
    5
    @FeignClient("userservice") //服务名称
    public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
    }

    基于SpringMVC的注解来声明远程调用的信息,例如:

    • 服务名称:userservice
    • 请求方式:GET
    • 请求路径:/user/{id}
    • 请求参数:Long id
    • 返回值类型:User
  4. 用Feign客户端代替RestTemplate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Autowired
    private UserClient userClient; //注入Feign客户端接口
    public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    // 2.用Feign远程调用
    User user = userClient.findById(order.getUserId());
    // 3.封装数据
    order.setUser(user);
    // 4.返回
    return order;
    }

image-20210925021404029

自定义Feign的配置

Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:

image-20210925022012411

方式一:配置文件的方式

  • 全局生效

    1
    2
    3
    4
    5
    feign:
    client:
    config:
    default: #对于所有的feign客户端生效
    loggerLevel: FULL
  • 局部生效

    1
    2
    3
    4
    5
    feign:
    client:
    config:
    userservice: #只对userservice的feign客户端生效
    loggerLevel: FULL

方式二:Java代码的方式,需要声明一个Bean

1
2
3
4
5
6
7
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel() {
//BASIC等级日志只记录请求方法和URL以及响应状态代码和执行时间。
return Logger.Level.BASIC;
}
}
  • 全局配置:把配置类的class放到启动类的@EnableFeignClients注解中

    1
    @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
  • 局部配置:把配置类的class放到某个Feign客户端的@FeignClient注解中

    1
    @FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class) 

image-20210925023446855

Feign的性能优化

  • Feign底层的客户端实现:
    • URLConnection:默认实现,不支持连接池
    • Apache HttpClient:支持连接池
    • OKHttp:支持连接池
  • 优化Feign的性能主要包括:
    • 使用连接池代替默认的URLConnection
    • 日志级别最好用BASIC或NONE

步骤:

  1. 引入HttpClient依赖

    1
    2
    3
    4
    5
    <!--HttpClient-->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    </dependency>
  2. 配置连接池

    1
    2
    3
    4
    5
    feign:
    httpclient:
    enabled: true #支持httpclient的开关
    max-connections: 200 #最大连接数
    max-connections-per-route: 50 #单个路径的最大连接数

image-20210925121436208

Feign的最佳实践

方式一(继承):给消费者的FeignClient和提供者的Controller定义统一的父接口作为标准

image-20210925122047188

方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块,提供给所有消费者使用

image-20210925122506478

image-20210925122544884

实现方式二(抽取FeignClient):

  1. 新建一个module,命名为feign-api,然后引入feign的starter起步依赖

    1
    2
    3
    4
    5
    6
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    </dependencies>
  2. 将service中编写的FeignClient、POJO、DefaultFeignConfiguration复制到feign-api项目中

    image-20210925125205808
  3. 在service中引入编写好的feign-api依赖

    1
    2
    3
    4
    5
    6
    <!--自己写的feign-api-->
    <dependency>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
    </dependency>
  4. 修改service中与上述三个组件相关的import部分,改为从feign-api依赖中引入

    image-20210925125334508
  • 此时启动service会导致引入的FeignClient客户端Bean无法被自动装载

    image-20210925125513417

    原因:引入的feign-api依赖下的FeignClient客户端位于cn.itcast.feign.clients包下,不属于当前service的@SpringBootApplication默认扫描包范围

    解决方法:

    • 方式一:指定FeignClient所在包

      1
      @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
    • 方式二:指定FeignClient的字节码

      1
      @EnableFeignClients(clients = UserClient.class)

image-20210925131403334

Gateway服务网关

网关的功能

image-20210925131840527

网关技术的实现

在SpringCloud中网关的实现包括两种:

  • Gateway
  • Zuul

Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloud Gateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能

image-20210925132058849

快速入门:搭建网关

  1. 创建新的module,引入SpringCloud Gateway的依赖和Nacos依赖,创建网关的SpringBoot启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <!--Nacos服务发现依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--网关依赖-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    </dependencies>
  2. 编写路由配置以及nacos地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    server:
    port: 10100
    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: localhost:80 #nacos地址
    gateway:
    routes:
    - id: user-service #路由标识
    uri: lb://userservice #路由的目标地址
    predicates: #断言,判断请求是否符合规则
    - Path=/user/** #断言是否是以/user/开头的请求
    - id: order-service
    uri: lb://orderservice
    predicates:
    - Path=/order/**

    启动访问http://localhost:10100/user/{id}或者http://localhost:10100/order/{orderId}即可

    image-20210925133523283

image-20210925133647641

路由断言工厂:Route Predicate Factory

网关路由可以配置的内容包括:

  • 路由id:路由的唯一标识
  • uri:路由目的地,支持lb(负载均衡)和http两种
  • predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地
  • filters:路由过滤器,处理请求或响应

我们再配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转为路由判断条件

例如:Path=/user/**是按照路径匹配,这个规则是由类:org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory处理

像这样的断言工厂还有:

image-20210925134110998

组合断言:使用多个断言,如果不满足其中一个那么无法路由(404)

1
2
3
4
5
6
7
8
9
10
11
12
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:80 #nacos地址
gateway:
- id: order-service #路由的目标地址
uri: lb://orderservice
predicates: #断言,判断请求是否符合规则
- Path=/order/** #断言是否是以/order/开头的请求
- Method=GET #请求方式必须为GET

image-20210925135753346

路由过滤器:GatewayFilter

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理

过滤器工厂GatewayFilterFacotry

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

image-20210925140135506

案例:给所有进入userservice的请求添加一个请求头:TAOYYZ=likeccz

实现方式:在gateway的SpringBoot核心配置文件中给userservice的路由添加过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:80 #nacos地址
gateway:
routes:
- id: user-service #路由标识
uri: lb://userservice #路由的目标地址
predicates: #断言,判断请求是否符合规则
- Path=/user/** #断言是否是以/user/开头的请求
filters: #过滤器
- AddRequestHeader=TAOYYZ,likeccz #加入请求头

默认过滤器:如果要对所有的路由都生效此过滤器,则可以将过滤器工厂写到default-filters下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:80 #nacos地址
gateway:
routes:
- id: user-service #路由标识
uri: lb://userservice #路由的目标地址
predicates: #断言,判断请求是否符合规则
- Path=/user/** #断言是否是以/user/开头的请求
default-filters:
- AddRequestHeader=TAOYYZ,likeccz #对所有请求都加入请求头

全局过滤器:GlobalFilter

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样

区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自己写代码实现

定义方式:实现GlobalFilter接口

1
2
3
public interface GlobalFilter {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

案例:定义全局过滤器,拦截并判断用户身份

需求:定义全局过滤器,判断请求参数是否满足以下条件

  • 参数中有lover
  • lover参数值为ccz

如果满足则放行,否则拦截

实现:定义一个全局过滤器的实现类,实现filter()方法,并且指定为Spring组件,并定义Order执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Component //标记为Spring的Bean
//@Order(-1) //设置执行顺序,也可以实现Ordered接口的getOrder()方法返回一个int值
public class LoverFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
//2.获取请求参数中的lover参数
MultiValueMap<String, String> queryParams = request.getQueryParams();
//3.判断参数值是否为ccz
String lover = queryParams.getFirst("lover");
//4.是的话放行
if ("ccz".equals(lover)) {
return chain.filter(exchange);
}
//5.否则拦截
//5.1为了给用户一个提示,设置一个响应状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
@Override
public int getOrder() {
return -1;
}
}

此时访问http://localhost:10100/order/101会被拦截并返回401状态码,因为参数没有lover=ccz

image-20210925143511913

过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter合并到一个过滤器链中,排序后依次执行每个过滤器

  • 当前路由过滤器和DefaultFilter都属于GatewayFilter
  • GlobalFilter通过GatewayFilterAdapter适配成GatewayFilter

image-20210925145341333

排序:

  • 每一个过滤器都必须指定一个int类型的Order值,Order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口或@Order注解来指定Order值,由我们自己指定
  • 路由过滤器和DefaultFilter的Order值由Spring指定,默认是按照声明顺序从1开始递增
  • 当过滤器的Order值一样时:DefaultFilter > 路由过滤器 > GlobalFilter

image-20210925150025819

image-20210925150035328

跨域问题处理

跨域是域名不一致,主要包括:

  • 域名不同:www.taobao.comwww.taobao.org
  • 域名相同,端口不同:localhost:8081localhost:8081

跨域问题浏览器禁止请求的发起者与服务器端发生跨域的AJAX请求,请求被浏览器拦截的问题

解决方案:CORS

网关处理跨域采用的是CORS方案,只需要简单配置即可实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.leyou.com"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期

image-20210925152048065

RabbitMQ消息队列

同步和异步

同步调用存在的问题

image-20210929010842471

image-20210929010920818

异步调用方案

image-20210929011140311

事件驱动优势

  • 服务解耦

  • 性能提升,吞吐量提高

  • 服务没有强依赖,不担心级联失败问题

  • 流量削峰 image-20210929011726020

image-20210929011911137

常用的消息队列

MQ(MessageQueue)消息队列,字面上来看就是存放消息的队列。也就是事件驱动架构中的Broker

image-20210929012919073

RabbitMQ

RabbitMQ是基于Erlang语言开发的开源消息通信中间件

image-20210930172348642

image-20210930172431626

安装RabbitMQ

  • 单机部署:

    1. 加载RabbitMQ镜像到docker:

      • 在线拉取:docker pull rabbitmq:3-management
      • 从本地加载镜像的tar包:docker load -i mq.tar
    2. 运行RabbitMQ到容器:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      docker run \
      -e RABBITMQ_DEFAULT_USER=account \
      -e RABBITMQ_DEFAULT_PASS=password \
      --name mq \
      --hostname mq1 \
      -p 15672:15672 \
      -p 5672:5672 \
      -d \
      rabbitmq:3-management

常见消息模型

image-20210930172958391

1. HelloWorld案例——简单队列模型

image-20210930173119852

image-20210930175724961

SpringAMQP

概念

image-20210930180247019

快速入门

案例:利用SpringAMQP实现HelloWorld中的简单队列模型的基础消息队列功能

  1. 在父工程中引入spring-amqp的起步依赖

    1
    2
    3
    4
    5
    <!--AMQP依赖,包含RabbitMQ-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
  2. 在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列

    • 在publisher服务的SpringBoot核心配置文件中配置mq连接信息:

      1
      2
      3
      4
      5
      6
      7
      spring:
      rabbitmq:
      host: 192.168.220.12 #RabbitMQ主机,默认localhost
      port: 5672 #端口,默认为5672,如果启用SSL,则为5671
      virtual-host: / #虚拟主机
      username: itcast #默认guest
      password: 12345 #默认guest
    • 在publisher服务中新建一个类来测试方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @SpringBootTest
      public class SpringAmqpTest {
      @Autowired
      private RabbitTemplate rabbitTemplate;
      @Test
      public void test() {
      String queueName = "simple.queue";
      String message = "hello springAmqp by taoyyz2";
      rabbitTemplate.convertAndSend(queueName,message);
      }
      }
  3. 在consumer服务中编写消费逻辑,绑定simple.queue这个队列

    • 在consumer服务的SpringBoot核心配置文件中配置mq连接信息:

      1
      2
      3
      4
      5
      6
      7
      spring:
      rabbitmq:
      host: 192.168.220.12 #RabbitMQ主机,默认localhost
      port: 5672 #端口,默认为5672,如果启用SSL,则为5671
      virtual-host: / #虚拟主机
      username: itcast #默认guest
      password: 12345 #默认guest
    • 在consumer服务中新建一个消息监听器类(注册为Bean),编写消费逻辑:

      1
      2
      3
      4
      5
      6
      7
      @Component
      public class SpringAmqpListenerTest {
      @RabbitListener(queues = "simple.queue")
      public void listenSimpleQueueMessage(String msg) {
      System.out.println("接受到的消息:" + msg);
      }
      }
image-20210930210805550 image-20210930220240837

2. Work Queue工作队列

工作队列可以提高消息处理速度,避免队列消息堆积

image-20210930220634785

案例:模拟Work Queue,实现一个队列绑定多个消费者

思路:

  • 在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void test2() throws InterruptedException {
    String queueName = "simple.queue";
    String message = "hello springAmqp__";
    for (int i = 0; i < 50; i++) {
    rabbitTemplate.convertAndSend(queueName, message + i + " ");
    Thread.sleep(20);
    }
    System.out.println("推送消息完成");
    }
  • 在consumer服务中定义两个消息监听者,都监听simple.queue队列

  • 消费者1每秒处理50条消息,消费者2每秒处理10条消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg) throws InterruptedException {
    System.out.println("【1】接受到的消息是:" + msg + LocalTime.now());
    Thread.sleep(20);
    }

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage2(String msg) throws InterruptedException {
    System.err.println("【2】接受到的消息是:" + msg + LocalTime.now());
    Thread.sleep(100);
    }

问题由于消费者2处理能力差,在获取消息时与消费者1都分别预取了25条消息,导致消费者2消费25条消息需要大量的时间

消费预取限制

修改SpringBoot核心配置文件,设置preFetch值,可以控制预取消息的上限:

1
2
3
4
5
spring:
rabbitmq:
listener:
simple:
prefetch: 1 #每个消费者可以未确认的最大未确认消息数。

image-20210930233018894

发布(Publish)、订阅(Subscribe)

发布订阅模式与之前的区别就是允许将同一消息发送给多个消费者,实现方式是加入了exchange(交换机)

image-20211001003507962

3. FanoutExchange

Fanout Exchange会将接受到的消息路由到每一个跟其绑定的queue,称为广播模式

image-20211001003712624

案例:利用SpringAMQP演示FanoutExchange的使用

思路: image-20211001004124505

  • 在consumer服务中,利用代码声明队列、交换机,并将两者绑定

    • 在consumer服务中声明Exchange、Queue、Binding

      SpringAMQP提供了声明交换机、队列、绑定关系的API,例如:

      image-20211001004924838
    • 在consumer服务中编写一个配置类,用于产生FanoutExchange、Queue和绑定关系对象Binding

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @Configuration
    public class FanoutConfig {
    @Bean
    public FanoutExchange fanoutExchange() {
    //产生交换机Bean
    return new FanoutExchange("test.fanout");
    }

    @Bean
    public Queue fanoutQueue1() {
    //产生第一个队列的Bean
    return new Queue("fanout.q1");
    }

    @Bean
    public Queue fanoutQueue2() {
    //产生第二个队列的Bean
    return new Queue("fanout.q2");
    }

    @Bean
    public Binding fanoutBinding1() {
    //绑定队列1到交换机,这里可以通过调方法的返回值获取队列和交换机
    return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
    }
    @Bean
    public Binding fanoutBinding2(Queue fanoutQueue2,FanoutExchange fanoutExchange) {
    //绑定队列2到交换机,这里通过参数注入这俩Bean
    return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
    }
  • 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RabbitListener(queues = "fanout.q1")
    public void listenSimpleQueueMessage3(String msg) {
    System.out.println("【queue1】接受到的消息是:" + msg + LocalTime.now());
    }

    @RabbitListener(queues = "fanout.q2")
    public void listenSimpleQueueMessage4(String msg) {
    System.err.println("【queue2】接受到的消息是:" + msg + LocalTime.now());
    }
  • 在publisher中编写测试方法,向交换机发送消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void testSendFanoutExchange() {
    //交换机名称
    String exchangeName = "test.fanout";
    //消息
    String msg = "hello,everyOne!";
    //发送消息
    rabbitTemplate.convertAndSend(exchangeName,"", msg);
    System.out.println("发送完成!");
    }

    踩坑!!!交换机无法接收到publisher中推送的消息,消费者也无法订阅到消息

    原因:RabbitTemplate对象的convertAndSend()方法选择了错误的重载: image-20211001154451836

image-20211001164327103

4. DirectExchange

Direct Exchange会将接受到的消息根据规则路由到指定的Queue,称为路由模式(routes)

image-20211001165453866

案例:利用SpringAMQP演示DirectExchange的使用

image-20211001165830038

思路:

  • 利用@RabbitListener声明Exchange、Queue、routingKey,代码同下一步

  • 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.q1"), //绑定的队列名
    exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),//默认为direct
    key = {"red", "blue"} //routingKey路由键
    ))
    public void listenDirectQueue1(String msg) {
    System.out.println("【queue1】接受到的消息是:" + msg);
    }

    @RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.q2"), //绑定的队列名
    exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),//默认为direct
    key = {"red", "yellow"} //routingKey路由键
    ))
    public void listenDirectQueue2(String msg) {
    System.out.println("【queue2】接受到的消息是:" + msg);
    }
  • 在publisher中编写测试方法,向itcast.direct发送消息

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void testSendDirectExchange() {
    String exchangeName = "itcast.direct";
    String msg = "hello direct!!";
    rabbitTemplate.convertAndSend(exchangeName, "red", msg); //监听了red的routingKey的队列均可收到
    System.out.println("发送完成!");
    }

    image-20211001171259343

5. TopicExchange

TopicExchange的routingKey必须是多个单词的列表,并且以.分隔

Queue与Exchange指定BindingKey时可以使用通配符:

#:代表0个或多个单词

*:代表一个单词

image-20211001180909842

案例:利用SpringAMQP演示TopicExchange的使用

image-20211001181140583

思路:

  • 利用@RabbitListener声明Exchange、Queue、routingKey,代码同下一步

  • 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.q1"),
    exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
    key = "china.#"
    ))
    public void listenTopicQueue1(String msg) {
    System.out.println("【topic.q1】接受到的消息是:" + msg);
    }

    @RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.q2"),
    exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),
    key = "#.news"
    ))
    public void listenTopicQueue2(String msg) {
    System.out.println("【topic.q2】接受到的消息是:" + msg);
    }
  • 在publisher中编写测试方法,向itcast.topic发送消息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Test
    public void testSendTopicExchange() {
    String exchangeName = "itcast.topic";
    String msg = "hello topic china!!";
    // china.# 和 #.news都能收到
    rabbitTemplate.convertAndSend(exchangeName, "china.news", msg);

    // china.# 能收到
    rabbitTemplate.convertAndSend(exchangeName, "china.whether", msg);

    // #.news 能收到
    rabbitTemplate.convertAndSend(exchangeName, "japan.news", msg);
    System.out.println("发送完成!");
    }

消息转换器

在SpringAMQP的发送方法中,消息的类型是Object,也就是说可以发送任意对象的消息,SpringAMQP会序列化为字节后发送

Spring对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的

而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。这种方式性能较差。

自定义MessageConverter的Bean即可,推荐使用json序列化:

  • 发送消息:

    1. 在publisher服务中引入jackson依赖:

      1
      2
      3
      4
      5
      <!--jackson依赖-->
      <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      </dependency>
    2. 在publisher服务中声明MessageConverter的Bean,使得发送消息时可以以JSON格式发送

      1
      2
      3
      4
      @Bean
      public MessageConverter messageConverter() {
      return new Jackson2JsonMessageConverter();
      }
    3. 在publisher服务中发送消息:

      1
      2
      3
      4
      5
      6
      7
      @Test
      public void testSendObject() {
      Map<String, Object> msg = new HashMap<>();
      msg.put("name", "菜菜子");
      msg.put("age", 21);
      rabbitTemplate.convertAndSend("object.queue",msg);
      }
  • 接收消息:

    1. 在consumer服务中也引入jackson依赖

    2. 在consumer服务中声明MessageConverter的Bean,使得接受消息时可以以JSON格式解析

      1
      2
      3
      4
      @Bean
      public MessageConverter messageConverter() {
      return new Jackson2JsonMessageConverter();
      }
    3. 在consumer服务中定义一个监听者监听队列接收消息

      1
      2
      3
      4
      @RabbitListener(queues = "object.queue")
      public void listenObjectQueue(Map<String, Object> msg) {
      msg.forEach((s, o) -> System.out.println(s + " --> " + o));
      }

image-20211001214355743