SpringCloud微服务技术栈.黑马跟学

Posted 心向阳光的天域

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringCloud微服务技术栈.黑马跟学相关的知识,希望对你有一定的参考价值。

SpringCloud微服务技术栈.黑马跟学 十二

今日目标

服务异步通信-高级篇

消息队列在使用过程中,面临着很多实际问题需要思考:

1.消息可靠性

消息从发送,到消费者接收,会经理多个过程:

其中的每一步都可能导致消息丢失,常见的丢失原因包括:

  • 发送时丢失:
    • 生产者发送的消息未送达exchange
    • 消息到达exchange后未到达queue
  • MQ宕机,queue将消息丢失
  • consumer接收到消息后未消费就宕机

针对这些问题,RabbitMQ分别给出了解决方案:

  • 生产者确认机制
  • mq持久化
  • 消费者确认机制
  • 失败重试机制

下面我们就通过案例来演示每一个步骤。
首先,导入课前资料提供的demo工程:

项目结构如下:

用docker启动即可

docker start mq

要创建一个队列起名simple.queue

然后在交换机中把amq.topic交换机,和上面创建的队列simple.queue绑定,我们手动配置

进入amq.topic交换机后,绑定队列

绑定后如图:

1.1.生产者消息确认

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。

返回结果有两种方式:

  • publisher-confirm,发送者确认
    • 消息成功投递到交换机,返回ack
    • 消息未投递到交换机,返回nack
  • publisher-return,发送者回执
    • 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。


注意:

1.1.1.修改配置

首先,修改publisher服务中的application.yml文件,添加下面的内容:

spring:
  rabbitmq:
    publisher-confirm-type: correlated
    publisher-returns: true
    template:
      mandatory: true

说明:

  • publish-confirm-type:开启publisher-confirm,这里支持两种类型:
    • simple:同步等待confirm结果,直到超时
    • correlated⭐:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
  • publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
  • template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息

1.1.2.定义Return回调

每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置:

修改publisher服务,添加一个:

package cn.itcast.mq.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException 
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> 
            // 投递失败,记录日志
            log.info("消息发送失败,应答码,原因,交换机,路由键,消息",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        );
    

1.1.3.定义ConfirmCallback

ConfirmCallback可以在发送消息时指定,因为每个业务处理confirm成功或失败的逻辑不一定相同。

在publisher服务的cn.itcast.mq.spring.SpringAmqpTest类中,定义一个单元测试方法:

public void testSendMessage2SimpleQueue() throws InterruptedException 
    // 1.消息体
    String message = "hello, spring amqp!";
    // 2.全局唯一的消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 3.添加callback
    correlationData.getFuture().addCallback(
        result -> 
            if(result.isAck())
                // 3.1.ack,消息成功
                log.debug("消息发送成功, ID:", correlationData.getId());
            else
                // 3.2.nack,消息失败
                log.error("消息发送失败, ID:, 原因",correlationData.getId(), result.getReason());
            
        ,
        ex -> log.error("消息发送异常, ID:, 原因",correlationData.getId(),ex.getMessage())
    );
    // 4.发送消息
    rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);

    // 休眠一会儿,等待ack回执
    Thread.sleep(2000);

全部配置完后,运行测试类SpringAmqpTest.java,这说明消息发送成功

然后呢,我们来一个消息发送失败的情况,我们故意填错交换机的名字

调用后,后台打印日志如下:

然后我们尝试填错,routingKey看一下

报错信息如下:

之后我们恢复代码,都保证正确即可

总结:
SpringAMQP中处理消息确认的几种情况:
● publisher-comfirm:

  • 消息成功发送到exchange,返回ack
  • 消息发送失败,没有到达交换机,返回nack
  • 消息发送过程中出现异常,没有收到回执

● 消息成功发送到exchange, 但没有路由到queue,

  • 调用ReturnCallback

1.2.消息持久化

生产者确认可以确保消息投递到RabbitMQ的队列中,但是消息发送到RabbitMQ以后,如果突然宕机,也可能导致消息丢失。

要想确保消息在RabbitMQ中安全保存,必须开启消息持久化机制。

  • 交换机持久化
  • 队列持久化
  • 消息持久化

1.2.1.交换机持久化

RabbitMQ中交换机默认是非持久化的,mq重启后就丢失。

我们通过命令
重启mq

docker restart mq

然后查看队列、交换机的情况,比如我们创建的是持久化队列

SpringAMQP中可以通过代码指定交换机持久化:

@Bean
public DirectExchange simpleExchange()
    // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
    return new DirectExchange("simple.direct", true, false);

事实上,默认情况下,由SpringAMQP声明的交换机都是持久化的。

可以在RabbitMQ控制台看到持久化的交换机都会带上D的标示:

1.2.2.队列持久化

RabbitMQ中队列默认是非持久化的,mq重启后就丢失。
SpringAMQP中可以通过代码指定交换机持久化:

我们可以先去mq图形化界面把simple.queue删除

@Bean
public Queue simpleQueue()
    // 使用QueueBuilder构建队列,durable就是持久化的
    return QueueBuilder.durable("simple.queue").build();

事实上,默认情况下,由SpringAMQP声明的队列都是持久化的。
可以在RabbitMQ控制台看到持久化的队列都会带上D的标示:

这些做完后,我们启动ConsumerApplication.java,然后查看mq的图形化界面
交换机是持久的

队列是持久的

1.2.3.消息持久化

首先把consumer服务停了,不要消费我们的消息
我们在mq的图形化界面,点击simple.queue队列,然后编辑消息,点击发送

查看有1条消息

然后我们重启docker中的mq

docker restart mq

然后再回来看mq的图形化界面,发现队列还在,但是消息没了

利用SpringAMQP发送消息时,可以设置消息的属性(MessageProperties),指定delivery-mode:

  • 1:非持久化
  • 2:持久化

用java代码指定:

默认情况下,SpringAMQP发出的任何消息都是持久化的,不用特意指定。
运行测试类SpringAmqpTest.java之后,查看mq的图形化界面

查看一下具体消息

然后我们重启一下docker的mq容器

docker restart mq

注意:AMQP中创建的交换机、队列、消息默认都是持久的
交换机:

队列:

消息:

1.3.消费者消息确认

RabbitMQ是阅后即焚机制,RabbitMQ确认消息被消费者消费后会立刻删除。
而RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向RabbitMQ发送ACK回执,表明自己已经处理消息。

设想这样的场景:

  • 1)RabbitMQ投递消息给消费者
  • 2)消费者获取消息后,返回ACK给RabbitMQ
  • 3)RabbitMQ删除消息
  • 4)消费者宕机,消息尚未处理

这样,消息就丢失了。因此消费者返回ACK的时机非常重要。

而SpringAMQP则允许配置三种确认模式:

  • manual:手动ack,需要在业务代码结束后,调用api发送ack。
  • auto⭐:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack。
  • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

由此可知:

  • none模式下,消息投递是不可靠的,可能丢失
  • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
  • manual:自己根据业务情况,判断什么时候该ack

一般,我们都是使用默认的auto即可。

1.3.1.演示none模式

修改consumer服务的application.yml文件,添加下面内容:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 关闭ack

修改consumer服务的SpringRabbitListener类中的方法,模拟一个消息处理异常:
修改SpringRabbitListener.java

@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) 
    log.info("消费者接收到simple.queue的消息:【】", msg);
    // 模拟异常
    System.out.println(1 / 0);
    log.debug("消息处理完成!");

测试可以发现,当消息处理抛异常时,消息依然被RabbitMQ删除了。
dubug启动Consumer
发现消息还没接收呢,直接就没了


也就是说,消费者虽然接收到了消息,但是假如消费者还没有读取,发生了报错或者宕机,这个消息就会丢失

1.3.2.演示auto模式

再次把确认机制修改为auto:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto # 关闭ack

我们去mq的图形化界面创建消息

发送后,我们看到图形化界面中有1条消息

IDEA后台因为我们认为写了1/0的错误算数运算,导致IDEA不停重发请求重试消息的推送,这显然也不符合我们的要求

在异常位置打断点,再次发送消息,程序卡在断点时,可以发现此时消息状态为unack(未确定状态):

抛出异常后,因为Spring会自动返回nack,所以消息恢复至Ready状态,并且没有被RabbitMQ删除:

1.4.消费失败重试机制

当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:

怎么办呢?

1.4.1.本地重试

我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

修改consumer服务的application.yml文件,添加内容:

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初始的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 4 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

修改SpringRabbitListener.java
修改为日志打印的形式

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) 
        log.debug("消费者接收到simple.queue的消息:【" + msg + "】");
        System.out.println(1 / 0);
        log.info("消费者处理消息成功!");
    

重启consumer服务,重复之前的测试。可以发现:

  • 在重试4次后,SpringAMQP会抛出异常


AmqpRejectAndDontRequeueException,说明本地重试触发了

  • 查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是ack,mq删除消息了

结论:

  • 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
  • 重试达到最大次数后,Spring会返回ack,消息会被丢弃

1.4.2.失败策略

在之前的测试中,达到最大重试次数后,消息会被丢弃,这是由Spring内部机制决定的。

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机⭐

比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。

1)在consumer服务中定义处理失败消息的交换机和队列

@Bean
public DirectExchange errorMessageExchange()
    return new DirectExchange("error.direct");

@Bean
public Queue errorQueue()
    return new Queue("error.queue", true);

@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange)
    return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");

2)定义一个RepublishMessageRecoverer,关联队列和交换机

@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate)
    return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");

完整代码:

package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;

@Configuration
public class ErrorMessageConfig 
    @Bean
    public DirectExchange errorMessageExchange()
        return new DirectExchange("error.direct");
    
    @Bean
    public Queue errorQueue()
        return new Queue("error.queue", true);
    
    @Bean
    public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange)
        return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
    

    @Bean
    public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate)
        return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
    

以上配置完之后,我们再重复步骤发送消息

发送后我们看到失败交换机有了

队列也有了

看一下IDEA的后台

看一下error.queue中的消息,很清晰把错误栈都输出了

1.5.总结

如何确保RabbitMQ消息的可靠性?

  • 开启生产者确认机制,确保生产者的消息能到达队列
  • 开启持久化功能,确保消息未消费前在队列中不会丢失
  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
  • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理

2.死信交换机

2.1.初识死信交换机

2.1.1.什么是死信交换机

什么是死信?

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息满了,无法投递

如果这个包含死信的队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,检查DLX)。

如图,一个消息被消费者拒绝了,变成了死信:

因为simple.queue绑定了死信交换机 dl.direct,因此死信会投递给这个交换机:

如果这个死信交换机也绑定了一个队列,则消息最终会进入这个存放死信的队列:

另外,队列将死信投递给死信交换机时,必须知道两个信息:

  • 死信交换机名称
  • 死信交换机与死信队列绑定的RoutingKey

这样才能确保投递的消息能到达死信交换机,并且正确的路由到死信队列。

2.1.2.利用死信交换机接收死信(拓展)

在失败重试策略中,默认的RejectAndDontRequeueRecoverer会在本地重试次数耗尽后,发送reject给RabbitMQ,消息变成死信,被丢弃。

我们可以给simple.queue添加一个死信交换机,给死信交换机绑定一个队列。这样消息变成死信后也不会丢弃,而是最终投递到死信交换机,路由到与死信交换机绑定的队列。

我们在consumer服务中,定义一组死信交换机、死信队列:

// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
@Bean
public Queue simpleQueue2()
    return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
        .deadLetterExchange("dl.direct") // 指定死信交换机
        .build();

// 声明死信交换机 dl.direct
@Bean
public DirectExchange dlExchange()
    return new DirectExchange("dl.direct", true, false);

// 声明存储死信的队列 dl.queue
@Bean
public Queue dlQueue()
    return new Queue("dl.queue", true);

// 将死信队列 与 死信交换机绑定
@Bean
public Binding dlBinding()
    return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");

2.1.3.总结

什么样的消息会成为死信?

  • 消息被消费者reject或者返回nack
  • 消息超时未消费
  • 队列满了

死信交换机的使用场景是什么?

  • 如果队列绑定了死信交换机,死信会投递到死信交换机;
  • 可以利用死信交换机收集所有消费者处理失败的消息(死信),交由人工处理,进一步提高消息队列的可靠性。

2.2.TTL

一个队列中的消息如果超时未消费,则会变为死信,超时分为两种情况:

  • 消息所在的队列设置了超时时间
  • 消息本身设置了超时时间

2.2.1.接收超时死信的死信交换机

在consumer服务的SpringRabbitListener中,定义一个新的消费者,并且声明 死信交换机、死信队列:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "dl.ttl.queue", durable = "true"),
    exchange 
        
                

SpringCloud概述及微服务技术栈的使用

1、SpringCloud的简介

SpringCloud是一系列框架的有序集合。它利用SpringBoot的开发便利性巧妙地简化了分布式系统基础设置的开发,如服务发现与注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用SpringBoot的开发风格做到一键启动和部署。SpringCloud并没有重复制造轮子,它只是将目前各家公司开发比较成熟、经得起考研的服务框架组合起来,通过SpringBoot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者流下了一套简单易懂、易部署和易维护的分布式系统开发工具。

1.1、SpringCloud中的五大核心组件

Spring Cloud的本质是在SpringBoot的基础上,增加了一堆微服务相关的规范,并对应用上下文(ApplicationContext)进行功能增强,既然SpringCloud是规范,那么就需要去实现,目前Spring Cloud规范已有Spring官方,Spring Cloud Netflix,Spring Cloud Alibaba等是实现。 通过组件化的方式,Spring Cloud将这些实现整合到一起构成全家桶式的微服务技术栈。

SpringCloud Netflix组件

组件名称作用
Eureka服务注册中心
Ribbon客户端负载均衡
Feign声明式服务端调用(基于Ribbon,将调用方式RestTemplate,改为service接口调用)
Hystrix客户端容错报保护(熔断降级服务)
ZuulAPI服务网关

Spring Cloud Alibaba组件

组件名称作用
Nacos服务注册中心
Sentinel客户端容错保护

Spring Cloud原生及其他组件

组件作用
Consul(Eureka替代者)服务注册中心
Config分布式配置中心
Gateway(Zuul替代者)API服务网关
Sleuth分布式链路追踪

1.2、SpringCloud的架构


从上图可以看出SpringCloud各个组件的相互配合,合作支持了一套完整的微服务架构。

  • 注册中心: 负责服务的注册与发现,很好的将个服务连接起来
  • 断路器: 负责监控服务之间的调用情况,连续多次的失败,将进行熔断降级保护
  • API网关: 负责转发所有对外的请求和服务
  • 配置中心: 提供了统一的配置信息管理服务,可以实时的通知各个服务获取最新的配置信息
  • 链路追踪技术: 可以将所有的数据记录下来,方便我们进行后续分析
  • 各个组件又提供了功能完善的dashboard监控平台,可以当便的监控各组件的运行状况

1.3、微服务与微服务架构

微服务:

强调的是服务的大小,它关注的是某个一个点,是具体解决某一个问题/提供落地式对应服务的一个服务应用,狭义的看,可以看做是IDEA中的一个个微服务工程或者Moudle模块。IDEA工具里面使用Maven开发的一个个独立的Moudel,它具体是使用SpringBoot开发的一个小模块,专业的事情交给专业的模块来做,一个模块就做一件事情,强调的是一个个个体,每个个体完成一个具体的任务或者功能。

微服务架构:

一种新的架构形式,Martin Fowler于2014年提出。
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间相互协调互相配合,为用户提供最终价值,每个服务运行在其独立的进程中,服务与服务之间采用轻量级的通信机制(如HTTP协议)互相协作,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中,另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具(如maven)对其进行统一构建。

1.4、微服务技术栈概括

微服务技术条目技术支持
服务开发Spring、SpringBoot、SpringMVC
服务配置与管理Netflix公司的Archaius、阿里的Diamond等
服务注册与发现Eureka、Consul、Zookeeper
服务调用Rset、RPC、gRPC
服务熔断器Hystrix、Envoy等
负载均衡Ribbon、Nginx等
服务接口调用(客户端调用服务的简化工具)Feign等
消息队列RabbitMQ、ActiveMQ、Kafka等
服务配置中心管理SpringCLoudConfig、Chef等
服务路由(API网关)Zuul等
服务监控Zabbix、Nagios、Metrics、Specatator等
全链路追踪Zipkin、Brave、Dapper等
数据流操作开发包SpringCloud Stream(封装与Redis、Rabbit、Kafka等发送接收消息)
时间消息总栈SpringCloud Bus
服务部署Docker、OpenStack、Kuberneters等

1.5、为什么选择SpringCloud作为微服务架构?

选型一依据

  • 整体解决方案和框架成熟度
  • 社区热度
  • 可维护性
  • 学习曲线

当前各大IT公司用的微服务架构有哪些?

  • 阿里:dubbo+HFS
  • 京东:JFS
  • 新浪:Motan
  • 当当网:DubboX

1.6、SpringCloud与SpringBoot的关系

  • SpringBoot专注于快速方便的开发出当个个体微服务
  • SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务,整合并管理起来,为各个微服务之间提供:配置管理、服务发现、断路器、路由、代理、事件总栈、决策竞选、分布式会话等等集成服务
  • SpringBoot可以离开SpringCloud独立使用,开发项目,但是SpringCloud离不开SpringBoot,属于依赖关系

1.7、Dubbo和SpringCloud技术选型

1、分布式+服务治理Dubbo

  • 目前成熟的互联网架构,应用服务化拆分+消息中间件

2、Dubbo与SpringCloud对比
可以看一下社区活跃度:DubboSpringCloud

DubboSpringCloud
服务注册中心ZookeeperSpring Cloud Netflix Eureka
服务调用方式RPCREST API
服务监控Dubbo-monitorSpringBoot Admin
断路器不完善SpringCloud Netflix Hystrix
服务网关Spring Cloud Netflix Zuul
分布式配置Spring Cloud Config
服务追踪Spring Cloud Sleuth
消息总栈Spring Cloud Bus
数据流Spring Cloud Stream
批量任务Spring Cloud Task

两者最大的区别在于通信方式:
SpringCloud抛弃了Dubbo的RPC通信,采用的是基于轻量级HTTP协议的REST API方式。
严格来说,这两种方式各有优劣,在性能上RPC要优于REST,但是在灵活度上REST相比RPC是更灵活,服务提供方和调用方只需要一致契约,不存在代码级别的强依赖,这个优点在当下强调快速演化的微服务环境下,显得更加合适。

二者解决的问题域不同:Dubbo的定位是一款RPC框架,而SpringCloud的目标是微服务架构下的一站式解决方案。

1.8、SpringCloud可以做什么?

  • Distributed/Versioned configuration 分布式/版本控制系统
  • Service registration and discovery 服务注册与发现
  • Routing 路由
  • Service-to-service calls 服务到服务之间的调用
  • Load balancing 负载均衡策略
  • Circuit Breakers 断路器
  • Distributed messaging 分布式消息管理

1.9、SpringCloud官网下载

SpringCloud官网

SpringCloud没有采用数字编号的方式命名版本号,而是采用了伦敦地铁站的名称,同时根据字母表的顺序来对应版本时间顺序:

  • 最早的Realse版本:Angel,
  • 第二个Realse版本:Brixton,
  • 然后依次是Camden、Dalston、Edgware,
  • 目前最新的是Hoxton SR4 CURRENT GA通用稳定版。

1.10、SpringCloud版本选择

大版本说明

SpringBootSpringCloud关系
1.2XAngel版本兼容SpringBoot1.2X
1.3XBrixton版本(布里克斯顿)兼容SpringBoot1.3X,也兼容SpringBoot1.4X
1.4XCamden版本(卡姆登)兼容SpringBoot1.4X,也兼容SpringBoot1.5X
1.5XDalston版本(多尔斯顿)兼容SpringBoot1.5X,不兼容SpringBoot2.0X
1.5XEdgware版本(埃奇韦尔)兼容SpringBoot1.5X,不兼容SpringBoot2.0X
2.0XFinchley版本(芬奇利)不兼容SpringBoot1.5X ,兼容SpringBoot2.0X
2.1XGreenwich版本(格林威治)

实际开发版本关系

spring-boot-starter-parentspring-cloud-dependencies
版本号发布日期版本号发布日期
1.5.2.RELEASE2017-03Dalston.RC12017-x
1.5.9.RELEASE2017-11Edgware.RELEASE2017-11
1.5.16.RELEASE2018-04Edgware.SR52018-10
1.5.20.RELEASE2018-09Edgware.SR52018-10
2.0.2.RELEASE2018-05Fomchiey.BULD-SNAPSHOT2018-x
2.0.6.RELEASE2018-10Fomchiey-SR22018-10
2.1.4.RELEASE2019-04Greenwich.SR12019-03

2、基于Eureka注册中心的案例搭建与分析

SpringCloud系列(一)、服务注册中心Eureka基础【详细教程】

3、Eureka的替换方案Consul

Eureka的闭源影响

在Euraka的GitHub上,宣布Eureka 2.x闭源。近这意味着如果开发者继续使用作为 2.x 分支上现有工作repo 一部分发布的代码库和工件,风险则将自负。

Eureka的替换方案如下:

  • ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等
  • Consul是近几年比较流行的服务发现工具,工作中用到,简单了解一下。consul的三个主要应用场景:服务发现、服务隔离、服务配置
  • Nacos是阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

3.1、Consul概述


Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案,Consul 的方案更“一站式”,

  • 内置了服务注册与发现框 架
  • 分布一致性协议实现
  • 健康检查
  • Key/Value 存储
  • 多数据中心方案

不再需要依赖其它工具(比如 ZooKeeper 等),使用起来也较 为简单。Consul 使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与 Docker 等轻量级容器可无缝配合。

3.2、Consul的优势

  • 使用Raft算法来保证一致性,比复杂的Paxoa算法更直接,相比较而言,Zookeeper采用的是Paxos,而etcd使用的则是Raft
  • 支持多数据中心,内外网的服务采用不同的端口监听。多数据中心集群可以避免单数据中心的单点故障,而其部署则需要考虑网络延迟,分片等情况,zookeeper和etcd均不提供多数据中心功能的支持
  • 支持健康检测,etcd不提供此功能
  • 支持HTTP和DNS协议接口。 zookeeper的继承较为复杂,etcd只支持http协议
  • 官方提供web管理界面,etcd无此功能
  • 综合比较,Consul作为服务注册和配置管理,比较值得关注和研究

Consul的特征:

  • 服务发现
  • 多数据中心
  • key/value存储
  • 健康检测

3.3、Consul与Eureka的区别

Consul具有强一致性(CP):

  • 服务注册相比Eureka会稍慢一些,因为Consul的Raft协议要求必须过半数的节点都写入成功才认为注册成功
  • Leader挂点后,重新选举期间整个Consul不可用,保证了强一致性,但牺牲了可用性

Eureka保证高可用性和最终一致性(AP):

  • 服务注册相对要快,因为不需要等注册信息replicate到其他节点上,也不保证注册信息是否replicate成功
  • 当数据出现不一致时,虽然A、B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时,如果请求A查不到,但请求B可以查到(但内容不一定一致),如此保证了高可用性,但是牺牲了一致性

开发语言和使用:

  • Eureka就是个servlet程序,跑在servlet容器中
  • Consul则是go编写而成,安装启动即可

3.4、Consul的下载与安装

访问 Consul 官网下载 Consul 的最新版本,Consul 需要单独安装,我这里是consul1.5x。根据不同的系统类型选择不同的安装包,从下图也可以看出 Consul 支持所有主流系统。

在Linux虚拟机在中安装Consul服务:

## 从官网下载最新版本的Consul服务
wget https://releases.hashicorp.com/consul/1.5.3/consul_1.5.3_linux_amd64.zip
##使用unzip命令解压
unzip consul_1.5.3_linux_amd64.zip
##将解压好的consul可执行命令拷贝到/usr/local/bin目录下
cp consul /usr/local/bin
##测试一下
consul

启动Consul服务:

##已开发者模式快速启动,-client指定客户端可以访问的ip地址
[root@node01 ~]# consul agent -dev -client=0.0.0.0
==> Starting Consul agent...
     Version: 'v1.5.3'
     Node ID: '49ed9aa0-380b-3772-a0b6-b0c6ad561dc5'
    Node name: 'node01'
   Datacenter: 'dc1' (Segment: '<all>')
     Server: true (Bootstrap: false)
   Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
  Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
     Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false,
Auto-Encrypt-TLS: false

启动成功之后访问: http://IP地址:8500 ,可以看到 Consul 的管理界面:

我们此处暂时先使用windows下的版本启动Consul服务:从官网下载windows版本的zip,解压后免安装:

输入启动Consu命令:

#-client=0.0.0.0 是为了开放所有ip访问
consul agent -dev -client=0.0.0.0

启动之后,访问地址栏:localhost:8500

3.5、Consul的K/V存储

可以参照Consul提供的KV存储的 API完成基于Consul的数据存储

含义请求路径请求方式
查看keyv1/kv/:keyGET
保存或更新v1/kv/:keyPUT
删除v1/kv/:keyDELETE
  • key值中可以带/, 可以看做是不同的目录结构。
  • value的值经过了base64_encode加密,获取到数据后base64_decode解密才能获取到原始值。数据不能大于512Kb
  • 不同数据中心的kv存储系统是独立的,使用dc=?参数指定。

3.6、基于Consul的服务注册案例

工程配置仍然和Eureka保持一致(可做参考):
SpringCloud系列(一)、服务注册中心Eureka基础【详细教程】

ebuy-consul-parent(父模块)
---ebuy-consul-product(商品微服务)
---ebuy-consul-order(订单微服务)

修改商品和订单微服务模块的pom文件:

	<!--SpringCloud提供的基于Consul的服务发现-->
	<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
	<!--actuator用于心跳检查-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

配置服务注册
ebuy-consul-product的application.yml

server:
  port: 9011 #端口号

spring:
  application:
    name: ebuy-product #商品模块服务名称
  datasource:
    username: root #数据库用户名
    password: root #数据库密码
    driver-class-name: com.mysql.jdbc.Driver #mysql加载驱动
    url: jdbc:mysql://localhost:3306/ebuy?useUnicode=true&characterEncoding=utf8
  cloud:
    consul:
      host: 127.0.0.1 #指定consul服务地址
      port: 8500 #指定consul服务端口号
      discovery:
        register: true #是否注册
        instance-id: ${spring.application.name}-1 #指定实例id名
        server-name: ${spring.application.name} #服务实例名称
        port: ${server.port} #服务实例端口号
        health-check-path: /actuator/health  #健康检测路径
        health-check-interval: 15s #指定健康检测时间间隔
        prefer-ip-address: true #开启ip地址注册
        ip-address: ${spring.cloud.client.ip-address} #实例请求ip

#mybatis相关配置
mybatis:
  type-aliases-package: com.ebuy.product.pojo  #mybatis简化pojo实体类别名
  mapper-locations: com/ebuy/product/mapper/*.xml #mapper映射文件路径

#打印日志
logging:
  level:
    com.ebuy: DEBUG #日志级别

ebuy-consul-order的application.yml

server:
  port: 9013 #端口号
  address: 127.0.0.1
  tomcat:
    max-threads: 10 #最大线程数(默认为200台)

spring:
  application:
    name: ebuy-order #服务名
  cloud:
    consul:
      host: 127.0.0.1 #指定consul服务地址
      port: 8500 #指定consul服务端口号
      discovery:
        register: true #是否注册
        instance-id: ${spring.application.name}-1 #指定实例id名
        server-name: ${spring.application.name} #服务实例名称
        port: ${server.port} #服务实例端口号
        health-check-path: /actuator/health  #健康检测路径
        health-check-interval: 15s #指定健康检测时间间隔
        prefer-ip-address: true #开启ip地址注册
        ip-address: ${spring.cloud.client.ip-address} #实例请求ip
        health-check-url: http://${server.address}:${server.port}/**/health
        health-check-critical-timeout: 30s #check失败后,多少秒剔除该服务

#打印日志
logging:
  level:
    com.ebuy: DEBUG

其中 spring.cloud.consul 中添加consul的相关配置:

  • host:表示Consul的Server的请求地址
  • port:表示Consul的Server的端口
  • discovery:服务注册与发现的相关配置
    • instance-id : 实例的唯一id(推荐必填),spring cloud官网文档的推荐,为了保证生成一个唯一的id ,也可以换成${spring.application.name}:${spring.cloud.client.ipAddress}
    • prefer-ip-address:开启ip地址注册
    • ip-address:当前微服务的请求ip

启动两个微服务:查看Consul监控中心

基于微服务的发现:
由于SpringCloud对Consul进行了封装。对于在消费者端获取服务提供者信息和Eureka是一致的。同样使用 DiscoveryClient完成调用获取微服务实例信息,其余用法基本都和Eureka保持一致。

4、Ribbon:基于客户端服务调用(负载均衡)

经过以上的学习,已经实现了服务的注册和服务发现。当启动某个服务的时候,可以通过HTTP的形式将信息注册到注册中心,并且可以通过SpringCloud提供的工具获取注册中心的服务列表。但是服务之间的调用还存在很多的问题,如何更加方便的调用微服务,多个微服务的提供者如何选择,如何负载均衡等。

4.1、什么是Ribbon?

Ribbon是Netflix发布的一个负载均衡器,有助于控制HTTP和TCP客户端行为,在SpringCloud中,Eureka一般配合Ribbon进行使用,Ribbon提供了客户端负载均衡的功能,Ribbon利用从Eureka或者Consul中读取到的服务信息,在调用服务节点提供的服务时,会合理的进行负载,默认为轮询策略。

在SpringCloud中可以将注册信息和Ribbon配合使用,Ribbon自动的从注册中心获取服务提供者的列表信息,并基于内置的负载均衡算法,请求服务。

4.2、Ribbon的主要作用

客户端服务调用:

  • 基于Ribbon实现服务调用,是通过拉取到的所有服务列表组成(服务名:请求路径)的一种映射关系,借助于RestTemplate最终实现调用。

负载均衡:

  • 当有多个服务提供者时,Ribbon可以根据负载均衡的算法自动的选择需要调用的服务地址。

4.3、Ribbon的关键组件

  • ServerList:可以响应客户端的特定服务的服务器列表。
  • ServerListFilter:可以动态获得的具有所需特征的候选服务器列表的过滤器。
  • ServerListUpdater:用于执行动态服务器列表更新。
  • Rule:负载均衡策略,用于确定从服务器列表返回哪个服务器。
  • Ping:客户端用于快速检查服务器当时是否处于活动状态。
  • LoadBalancer:负载均衡器,负责负载均衡调度的管理。

4.4、工程改造

上述讲解了Consul替代Eureka,此处我们暂时先将注册中心改为Eureka注册,并配置两台注册中心集群:
application.yml(将8000注册到9000)互相注册,application.yml(将9000注册到8000)即可

server:
  port: 8000 #端口号

spring:
  application:
    name: eureka-server #eurekaServer服务名

eureka:
  #instance:
    #hostname: 127.0.0.1 #服务器ip地址
  client:
    register-with-eureka: true #是否将自己注册到注册中心
    #fetch-registry: false #是否从注册中心获取服务列表
    serviceUrl: #配置暴露给Eureka Client的请求地址
      defaultZone: http://127.0.0.1:9000/eureka/
  server:
    enable-self-preservation: false #关闭自我保护机制(一旦发现有网络不稳定的服务,直接剔除)
    eviction-interval-timer-in-ms: 4000 #剔除时间间隔,单位:毫秒
    #wait-time-in-ms-when-sync-empty: 5

服务提供者和消费者:修改application.yml文件中注册中心配置:

server:
  port: 90XX #端口号
  address: 127.0.0.1
  tomcat:
    max-threads: 10 #最大线程数(默认为200台)

spring:
  application:
    name: ebuy-XXXX #服务名
#  cloud:
#    consul:
#      host: 127.0.0.1 #指定consul服务地址
#      port: 8500 #指定consul服务端口号
#      discovery:
#        register: true #是否注册
#        instance-id: ${spring.application.name}-1 #指定实例id名
#        server-name: ${spring.application.name} #服务实例名称
#        port: ${server.port} #服务实例端口号
#        health-check-path: /actuator/health  #健康检测路径
#        health-check-interval: 15s #指定健康检测时间间隔
#        prefer-ip-address: true #开启ip地址注册
#        ip-address: ${spring.cloud.client.ip-address} #实例请求ip
#        health-check-url: http://${server.address}:${server.port}/**/health
#        health-check-critical-timeout: 30s #check失败后,多少秒剔除该服务

#使用eureka注册中心
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8000/eureka/,http://127.0.0.1:9000/eureka/
  instance:
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    prefer-ip-address: true   #使用ip地址注册(在注册中心显示名字以ip地址显示)
    lease-expiration-duration-in-seconds: 10 #eureka client发送心跳给eureka server服务端后,续约到期时间(默认为90秒)
    lease-renewal-interval-in-seconds: 5 #发送心跳续约时间间隔

#打印日志
logging:
  level:
    com.ebuy: DEBUG

4.5、服务调用Ribbon高级,什么是负载均衡?

在搭建网站时,如果节点的web服务性能和可靠性都无法达到要求,或者是在使用外网服务时,经常担心被人攻击,一不小心就会有打开外网端口的情况,通常这个时候加入负载均衡就能有效的解决服务访问问题。

负载均衡是一种基础的网络服务,其原理是通过运行在前面的负载均衡服务,按照指定的负载均衡算法,将流量分配到后端服务集群上,从而为系统提供并行扩展的能力。

负载均衡的应用场景包括流量包、转发规则以及后端服务,由于服务有内外网个例,健康检查等功能,能够有效提高系统的安全性和可靠性。

4.6、客户端负载均衡和服务端负载均衡

客户端负载均衡:

  • 客户端从注册中心会获取到一个服务提供者的服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡,即在客户端就进行负载均衡算法分配。

服务端负载均衡:

  • 先发送请求到负载均衡服务器或软件,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行服务在均衡算法分配

4.7、基于Ribbon实现负载均衡

首先要搭载多态服务器,上述已经搭建好Eureka注册中心的集群,然后再搭建两台ebuy-product和一台ebuy-order即可,如下:

1、服务提供者ebuy-product

服务提供者:修改ebuy-product模块下的ProductController#findById()方法:

@RestController
@RequestMapping("/product")
public class ProductController {

	/**
	 * 回去客户端ip地址
	 */
	@Value("${spring.cloud.client.ip-address}")
	private String ip;

	/**
	 * 获取客户端的端口号
	 */
	@Value("${server.port}")
	private String port;

	@Autowired
	private EasybuyProductService productService;

	@RequestMapping(value = "/{id}",method = RequestMethod.GET)
	public EasybuyProduct findById(@PathVariable Long id) {
		EasybuyProduct product = productService.selectByPrimaryKey(id);
		product.setEpDescription("调用ebuy-product服务,ip:"+ip+",服务提供者端口:"+port);
 		return product;
	}
}

ebuy-product服务提供者启动两台:9011,9012
ebuy-order服务消费者启动一台:9013

2、服务消费者ebuy-order

然后在ebuy-order的EbuyOrderApplication启动类处,创建RestTemplate方法,并添加@LoadBalanced注解实现与Ribbon搭配的负载均衡:

@SpringBootApplication
@EnableEurekaClient  //开启Eureka客户端服务注册
@EnableDiscoveryClient  //开启服务发现
public class EbuyOrderApplication {

    /**
     * @Bean 配置RestTemplate交给spring管理
     * @LoadBalanced 实现负载均衡(Ribbon原理)
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

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

在ebuy-order服务模块的OrderController下添加下单方法:

	@Autowired
    RestTemplate restTemplate;

    
    @RequestMapping(value = "/buy/{id}",method = RequestMethod.GET)
    public EasybuyProduct findById(@PathVariable Long id) {
        EasybuyProduct easybuyProduct=new EasybuyProduct();
        //easybuyProduct=restTemplate.getForObject("http://127.0.0.1:9011/product/"+id,EasybuyProduct.class);<

以上是关于SpringCloud微服务技术栈.黑马跟学的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud微服务技术栈.黑马跟学

Github一夜爆火!阿里微服务全栈实录开源,实战部署齐飞

微服务实用篇--学习笔记

微服务框架 SpringCloud微服务架构 多级缓存 48 多级缓存 48.7 Redis 缓存预热

Nacos源码分析.黑马跟学笔记

SpringCloud系列SpringCloud概述及微服务技术栈的使用

(c)2006-2024 SYSTEM All Rights Reserved IT常识