叠甲:配合黑马的微服务视频享用

目录

入门

Eureka

Nacos

http客户端Feign

网关GateWay

RabbitMQ


入门

单体架构:将业务的所有功能集中在一个项目开发,打成一个包部署

分布式架构:根据业务系统对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务

而微服务是一种经过良好架构设计的分布式架构方案,微服务结构特征:

单一职责:微服务拆分力度更小,每一个服务对应唯一的业务能力,做到单一职责,避免重复业务开发。

面向服务:微服务对外暴露接口

自治:队独立,技术独立,数据独立,部署独立

隔离性强:服务调用做好隔离,容错,降级,避免出现级联问题(

隔离性:一个服务挂了 / 卡了,别把整个系统拖死,其他服务还能正常用。

级联问题(雪崩效应):一个服务故障 → 调用它的所有服务都卡死 → 整个系统全部崩溃。

微服务结构

在微服务架构中,配置中心和注册中心是两个重要的组件。

配置中心用来统一管理项目中所有配置,各种参数、各种开关,全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。当各个服务需要获取配置的时候,就来「配置中心」的接口拉取。

注册中心则是用来管理服务实例的注册和发现的。各个服务实例在启动时会向注册中心注册自己的信息(如IP地址和端口号),其他服务实例可以通过注册中心来发现并调用这些服务12。

微服务技术对比

微服务远程调用

1.基于RestTemplate发起的http请求实现远程调用

2.http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可。


RestTemplate 是 Spring 提供的、专门用来发送 HTTP 请求的工具类,作用等同于:Java 代码里的浏览器 / Postman

你在微服务里服务 A 调用服务 B,本质就是发 HTTP 请求,RestTemplate 就是帮你简化这件事的工具,不用你手写原生的 HTTP 连接、流处理、JSON 转换,一行代码就能调用别的服务接口。要想使用restTemplate,先把它注册到IOC容器中

@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
@Service
public class OrderService {

    @Autowired
    private RestTemplate restTemplate;

    public User getUser(Long userId) {
        // 调用远程用户服务的接口,一行代码完成
        String url = "http://localhost:8081/user/" + userId;
        
        // getForObject = 发送GET请求,把返回JSON自动转成User对象
        User user = restTemplate.getForObject(url, User.class);
        return user;
    }
}

等价于代码里打开浏览器访问 http://localhost:8081/user/1001

常用方法

和 HTTP 方法一一对应:

  • getForObject:发送 GET 请求,获取返回对象
  • postForObject:发送 POST 请求,传 JSON / 表单
  • exchange:通用方法(支持 GET/POST/PUT/DELETE,更灵活)
  • delete:发送 DELETE 请求

它自动帮你做:

  • 建立 HTTP 连接
  • 发送请求参数
  • 接收 JSON 结果
  • 自动转成 Java 对象(不需要你手动解析)


提供者与消费者

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

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

Eureka

消费者该如何获取服务提供者具体信息?

①:服务提供者启动时向eureka注册自己的信息

②:eureka保存这些信息

③:消费者根据服务名称向eureka拉取提供者信息

如果有多个服务提供者,消费者该如何选择?

①:服务消费者利用负载均衡算法,从服务列表中挑选一个

消费者如何感知服务提供者健康状态?

①:服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态

②:eureka会更新记录服务列表信息,心跳不正常会被剔除

③:消费者就可以拉取到最新的信息

要想成为Eureka注册中心,现在启动类使用@EnableEurekaClient的注解

然后将一些配置写在yml中

spring:
  application:
    name: eurekaserver  #Eureka的服务名称

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

如果需要注册服务,需要经过差不多的步骤,引依赖+配置

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

如果想启动多个springboot实例我们可以采用复制配置后添加虚拟机配置,添加-Dserver.port=????的指令。

服务拉取是基于服务名称获取服务的列表,然后队服务列表做负载均衡。负载均衡注解为

@LoadBanlad,默认为轮询。

Ribbon负载均衡

发起调用请求后会被拦截器拦截,然后根据请求的服务名称去寻找对应的服务列表,默认采用轮询的方式选择服务后再发起真正的请求。

如果想改变负载均衡的策略

ribbon默认采用懒加载,即第一次访问时才创建LoadBalanceClient,请求时间会很长。而饥饿加载会在项目启动时创建,降低第一次访问的耗时

配置如下

ribbon:
  eager-load:
    enabled: true
    clients: userservice

Nacos

Nacos是阿里巴巴的产品,是springcloud的一种组件,相比eureka功能更加丰富,在国内受欢迎程度较高

页面如下

登陆界面默认都是nacos

需要注册服务的配置如下

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848  nacos所在端口
        cluster-name: 深圳    集群名称

nacos服务分级存储模型

一级是服务名 例如userservice

二级是集群名 例如广州、伤害

三级是实例 例如广州部署了一台userservice服务器

服务调用尽可能选择本地集群的服务,跨集群调用延迟较高,本地集群不可访问时,再去访问其他集群。(优先选择本地集群,然后随机发出请求)

Nacos还提供了权重配置,权重越大访问频率越高(0~1之间)

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

环境隔离:

1.namespace用来做环境隔离

2.每个namespace都有唯一id

3.不同namespace下的服务不可见

1.Nacos与eureka的共同点

都支持服务注册和服务拉取

都支持服务提供者心跳方式做健康检测

2.Nacos与Eureka的区别

Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式

临时实例心跳不正常会被剔除,非临时实例则不会被剔除

Nacos支持服务列表变更的消息推送模式,服务列表更新更及时

Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式 ; Eureka采用AP方式

AP 模式(可用性优先)

能访问就行,数据不一定最新。👉 就算部分节点挂了,剩下的还能继续提供服务。👉 适合:服务发现(绝大多数场景)

Eureka 从头到尾都是 AP

Nacos 默认也是 AP(临时实例)

  1. CP 模式(一致性优先)
    数据必须一致才给你访问。
    👉 有节点挂了、数据不一致 → 直接拒绝访问。
    👉 适合:需要强一致的配置、数据库
    Nacos 的 非临时实例(永久实例)= CP 模式

nacos热更新配置

由于我们需要先获取nacos配置再读取本地配置,因此不能把nacos地址放在本地配置中,springcloud提供了bootstrap.yml,我们可以将nacos地址与配置放在里面,项目启动时就能直接读取

多环境配置共享 [服务器名] . 后缀名 可以在不同的环境下共享同一个服务的配置

如果有个配置名重复,则就近原则进行配置

优先级 服务名-环境-后缀 > 服务名-后缀 > 本地配置

nacos集群搭建步骤

1.搭建数据库集群并初始化数据库表(mysql集群实际上是表中的数据一摸一样的多个数据库)

2.修改集群配置(节点信息),数据库配置

3.分别启动多个nacos节点

4.nginx反向代理(Nginx 做的事情:收到请求。看一眼自己配置的 Nacos 集群列表(你写的 upstream)。自动选一台正常运行的 Nacos。把请求转发给它你看到的页面,就是那台 Nacos 的页面)

http客户端Feign

Feign是一个声明式的http客户端

如果不用Feign

// 你要手动构建请求、拼接URL、处理参数、发请求、转JSON
String url = "http://order-service/getOrder?id=" + id;
ResponseEntity<Order> resp = restTemplate.getForEntity(url, Order.class);
Order order = resp.getBody();

使用Feign

// 你只声明:我要调用这个接口
@FeignClient("order-service")
public interface OrderClient {
    @GetMapping("/getOrder")
    Order getOrder(@RequestParam("id") Long id);
}

在服务启动类上加上@EnableFrignClients的注解

@FeignClient("userservice")
public interface UserClient {
    
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id);
}

不需要自己写实现类,Feign会根据你提供的url地址和服务接口自动写成动态实现类

自定义配置Feign

Feign的性能优化

Feign底层的客户端实现
URLConnection: 默认实现,不支持连接池
Apache HttpClient: 支持连接池
OKHttp: 支持连接池

因此优化Feign的性能主要包括:
使用连接池代替默认的URLConnection
日志级别,最好用basic或none

对比项

默认 URLConnection

连接池(Apache HttpClient / OKHttp)

连接方式

每次请求都新建 TCP 连接,请求结束后立即断开

预先创建一批连接放在池里,请求时直接复用,用完归还

性能开销

每次都要经历 TCP 三次握手、四次挥手,高并发下开销巨大

避免频繁创建 / 销毁连接,大幅减少网络 IO 和 CPU 开销

响应时间

高并发下明显变长(大量时间浪费在连接建立上)

平均响应时间显著缩短,尤其是高并发场景

资源占用

频繁创建连接会导致端口、线程资源耗尽,容易出现连接超时

连接数可控,资源利用率更高,稳定性更好


Feign的最佳实践

1.让controller和FeignClient继承统一接口

2.将FrignClient,pojo,feign的默认配置都定义到一个项目中,供所有消费者使用。

网关GateWay

server:
  port: 10010 #网管端口
  
spring:
  application:
    name: gateway #网关名称
    
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #nacos地址
    gateway:
      routes: #网关路由配置
        - id: user-service #路由id,自定义,只需一个即可
          uri: lb://userservice  # 路由的目标地址,lb是负载均衡,后面跟服务名称
          predicates: #路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/user/**  #这个按照路径匹配,只要以/user/开头就符合要求
      

配置内容

路由id:路由唯一标识

url:路由目的地,支持lb和http两种

predicates:路由断言,判断请求是否符合要求,符合则转发到路由目的地

filters: 路由过滤器,处理请求或响应

路由断言工厂

我们在配置文件中写的断言规则知识字符串,这些字符串会被predicate factory读取并处理,转变为路由判断的条件

路由过滤器 GateWayFilter

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

此作用是给请求添加了名为Truth 值为Itcast is freaking awesome的请求头

全局过滤器GlobalFilter

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

@order注解表示过滤器优先级

请求进入网关后会碰见三种过滤器:当前路由的过滤器,DefaultFilter,GlobalFilter

请求路由后会将这三个过滤器合并到一个过滤器链(集合)中,排序后依次经过过滤器

每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前

GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定

路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增

当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器>GlobalFilter的顺序执行

跨域:域名不一样就是跨域,主要为跨域不同或端口不通( 只有浏览器发请求才会跨域。 )

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

跨域配置

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 #是否携带token 
            maxAge: 360000 #这次跨域检测的有效期

RabbitMQ

同步通讯与异步通讯

同步存在的优点:

时效性较强,能够立即得到结果

同步存在的问题:

耦合度高:每次加入新的需求,都要修改原来的代码

性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于各个调用的时间之和。

资源浪费:调用链中的每个服务在等待响应过程中,不能释放占用的资源,高并发场景下会极度浪费系统资源。

级联失败:如果服务提供者出现问题,所有调用方都跟着出问题如同多米诺骨牌,迅速导致整个服务器崩溃。

异步通信优点:

耦合度低,吞吐量提升,故障隔离,流量削峰。

异步通信缺点:

依赖于Broker的可靠性,安全性,吞吐能力。

架构复杂,业务没有明显的流程线,不好追踪管理。


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

创建rabbitmq容器

15672是rabbitmq访问端端口

5672是客户端端口

这是Rabbitmq管理端的界面

Channel:操作MQ的工具

exchange:路由消息到队列中

quene:缓存消息

virtual host:虚拟主机,是对quene,exchange等资源的逻辑分组。

publisher:消息发布者,将消息发送到队列quene

quene:消息队列,负责缓存并接收消息

consumer:订阅队列,处理队列中的消息

@Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.150.101");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("itcast");
        factory.setPassword("123321");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:【" + message + "】");

        // 5.关闭通道和连接
        channel.close();
        connection.close();

    }
public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.136.130");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("itcast");
        factory.setPassword("123456");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }

基本队列的消息发送流程

1.建立connection

2.创建channel

3.利用channel声明队列

4.利用channel向队列发送消息

基本队列的消息接收流程

1.建立connection

2.创建channel

3.利用channel声明队列

4.定义consumer的消费行为handleDelivery

5.利用channel将消费者与队列绑定

都需要利用channel声明同一个消息队列是因为避免任意一方的channel找不到对应的消息队列。(启动顺序可能存在差异)


SpringAMQP

AMPQ是应用于应用程序或之间传递业务消息的开放标准,该协议与平台无关,更符合微服务中独立性的要求。SpringAMPQ是基于AMPQ协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,spring-amqp是基础抽象,spring-rabbit是底层的默认实现。

特征

1.侦听器容器,用于异步处理入站消息

2.用于发送和接收消息的rabbittemplate

3,RabbitAdmin用于自动声明队列,交换和绑定

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void testSimpleQuene(){
        String queneName = "Simple.quene";
        String message = "hello my baby!";
        rabbitTemplate.convertAndSend(queneName,message);
    }
}
@RabbitListener(queues = "simple.queue")
    public void listenSimpleQueneMessage(String msg) throws  InterruptedException{
        System.out.println("spring 消费者接收到消息 :[ " + msg + "] ");
    }

SpringAMPQ默认不会自己创建队列,需要提前创建好。

而且消息一旦被消费就会从队列中删除,RabbitMq没有消息回溯功能。

Work Quene 工作队列

Rabbitmq为了预防一个监听者拿走所有消息,存在着预取机制,为控制消费者一次拿多少条消息 。

发布,订阅模式

发布订阅模式允许将同一消息发送给多个消费者。实现方式是加入了exchange(交换机)

交换机不能缓存消息,如果路由失败则消息丢失

常见的exchange:广播(Fanout),路由(Direct),话题(topic)

Fanout exchange 会将收到的消息路由到每一个跟其绑定的queue

Direct exchange会将接收到的消息根据规则路由到指定的队列,因此成为路由模式

每一个队列和交换机设置一个Binding key

发布者发送消息时,会指定消息的routingKey

交换机将消息路由到BindingKey与消息RoutingKey一致的队列

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐