微服务面试题一

1.SOA、分布式、微服务之间有什么关系和区别?

  • 分布式架构是指将单体架构中的各个部分拆分,然后部署不同的机器或进程中去,SOA和微服务基
    本上都是分布式架构的
  • SOA是⼀种⾯向服务的架构,系统的所有服务都注册在总线上,当调⽤服务时,从总线上查找服务
    信息,然后调⽤
  • 微服务是⼀种更彻底的⾯向服务的架构,将系统中各个功能个体抽成⼀个个⼩的应⽤程序,基本保
    持⼀个应⽤对应的⼀个服务的架构

2.怎么拆分微服务?

拆分微服务的时候,为了尽量保证微服务的稳定,会有⼀些基本的准则:

  • 微服务之间尽量不要有业务交叉。
  • 微服务之前只能通过接⼝进⾏服务调⽤,⽽不能绕过接⼝直接访问对⽅的数据。
  • ⾼内聚,低耦合

3.Eureka

3.1 什么是服务注册和服务发现

以Eureka为例
服务注册:服务提供者需要把自己的信息注册到Eureka,Eureka来保存这些信息(服务名称,ip,端口号等)。
服务发现:消费者向Eureka拉取服务列表,如果服务者是集群,则消费者会利用负载均衡算法选择一个。

3.2 启动流程

  1. Eureka客户端(以下简称客户端)启动后,定时向Eureka服务端(以下简称服务端)注册自己的服务信息(服务名、IP、端口等)。这就是服务注册。
  2. 客户端启动后,根据名称定时拉取服务端保存注册信息。这就是服务发现或服务拉取。
  3. 之后,消费者就可以远程调用提供者。

3.3 消费者如何感知提供者健康状态

  1. 提供者每隔30秒向注册中心发起请求,报告自己的健康状态——称为心跳。
  2. Eureka会更新服务列表信息(注册信息),如果Eureka90秒没有接收到心跳,会被剔除
  3. 消费者就可以拉取到新的信息

3.4 如何搭建注册中心

  1. 引用服务端依赖
  2. 服务启动类上添加@EnableEurekaServer注解
  3. 在application.yml编写配置
server:
  port: 10086
spring:
  application:
    name: eureka-server
eureka:
  client:
    service-url: 
      defaultZone: http://127.0.0.1:10086/eureka

3.5如何完成服务注册和服务发现

  1. 引入客户端依赖
  2. 编写配置文件 (服务发现也需要知道eureka地址,因此与服务注册一致,都是配置eureka信息)
spring:
  application:
    name: orderservice
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka

3.6 为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。

LoadBalancerInterceptor里面有个interceptor方法,该方法对RestTemplate的请求进行拦截,(获取请求的url,找到主机名(就是服务id))然后从Eureka根据名称获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。

3.7 负载均衡算法有哪些

  1. 轮询法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每⼀台服务器,而不关⼼服务器实际的连接数和当前的系统负载。
  2. 随机法:随机选择一个可用的服务器。
  3. 加权轮询法:为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
  4. 加权随机法:与加权轮询法⼀样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。
  5. 最小连接数法:最⼩连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的⼀台服务器来处理当前的请求,尽可能地提⾼后端服务的利⽤效率,将负责合理地分流到每⼀台服务器.
  6. 区域敏感策略ZoneAvoidanceRule(默认):以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。

3.8 负载均衡如何实现

主要使用Ribbon,比如,使用Feign远程调用时,底层的负载均衡使用的就是Ribbon

3.9 自定义负载均衡策略

  1. 方法一(全局):通过定义IRule实现可以修改负载均衡规则,交给Spring容器管理
@Bean
public IRule randomRule(){
    return new RandomRule();
}
  1. 方法二(局部):通过配置文件
userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务名
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 

3.10 Ribbon负载均衡流程

在这里插入图片描述

3.11 饥饿加载和懒加载

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

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

4.Nacos

4.1 什么是Nacos

Nacos是阿里巴巴开源的一个微服务组件,它有配置管理,服务发现,动态的DNS服务和服务管理平台
(1)配置管理:支持配置统一管理,配置自动更新等
(2)服务发现:支持DNS和RPC的服务发现,同时进行监控检查。
(3)动态DNS服务:支持路由权重,更容易实现负载均衡、更灵活的路由策略、流量监控等
(4)服务管理:

4.2 nacos如何完成服务注册

  1. 引依赖
  2. 配地址
spring:
  cloud:
    nacos:
      server-addr: localhost:8848

4.3 Eureka和Nacos区别

共同点:

  1. 都支持服务注册,服务拉取
  2. 都支持服务提供者心跳方式做健康检测
    区别:
  3. Nacos支持服务端主动监测提供者状态:临时实例提供心跳模式,非临时实例采用主动检测模式。
  4. 临时实例心跳不正常会被剔除,非临时实例不会被剔除
  5. Nacos支持服务列表变更的消息推送(服务列表发生变更主动告知消费者),服务列表更新更及时
  6. Nacos集群默认采用AP方式,当集群存在非临时实例时,采用CP模式;Eureka采用AP方式
  7. Nacos支持配置中心,Eureka只有注册中心。

4.4 Nacos和Eureka结构

Nacos
Eureka

4.5 如何添加DataId配置

在这里插入图片描述
要引入

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
            <version>3.0.3</version>
        </dependency>
  1. 在配置文件(bootstrap.yml)中添加环境 (热更新并不需要)
spring:
	profiles:
		active: dev
  1. 在Nacos控制台新建配置:
    (1)DataId: 服务名称-环境(dev).文件后缀(yml)
    (2)选择配置格式
    (3)配置内容
  2. 后端获取@Value(“${xxx.xxx}”) 如果类上使用@RefreshScope可以实现热更新

4.6 如何在nacos中完成微服务的配置

4.6.1 在application.yml中

server:
  port: 51601
spring:
  application:
    name: leadnews-app-gateway

4.6.2 在nacos配置列表

让Data ID和微服务名(spring.application.name)保持一致,以及选择相同的文件后缀,在配置内容中完成配置。

在这里插入图片描述

4.7 环境隔离

Namespace来做环境隔离
每个namespace都有唯一的id
不同namespace下服务不可见

5.feign如何使用

  1. 引依赖 (openfign、nacos、loadbalancer)
		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>3.1.5</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2021.0.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            <version>3.1.5</version>
        </dependency>
  1. 在需要远程调用的启动类上加@EnableFeignClients注解,还要指定要扫描的包或指定加载的客户端(如果openfeign单户做了一个模块)
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")@EnableFeignClients(clients = {UserClient.class})
  1. 创建接口,并添加@FeignClient(“服务名称”)(可以单独做一个模块,然后再调用的模块引入该模块)
@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}
  1. 将接口注入到要使用的地方,调用接口里面的抽象方法

6.GateWay

6.1 网关的核心功能 (通过gateway的端口就可以路由的微服务)

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

6.2 项目中如何使用

  1. 引入网关和Nacos服务注册相关依赖
  2. 在配置文件中 编写基础配置和路由规则
server:
 port: 10010 # 网关端口
spring:
 application:
   name: gateway # 服务名称
 cloud:
   nacos:
     server-addr: localhost:8848 # nacos地址
   gateway:
     routes: # 网关路由配置
       - id: user-service # 路由id,自定义,只要唯一即可
         # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
         uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
         predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
           - Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求

路由配置包括:

  1. 路由id:路由的唯一标示
  2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
  3. 路由断言(predicates):判断路由的规则
  4. 路由过滤器(filters):对请求或响应做处理

6.3 过滤器工厂

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

6.4路由过滤器种类(部分)

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

如何使用?只需要修改gateway服务的application.yml文件,添加路由过滤即可

spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/** 
        filters: # 过滤器
        - AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头

6.5 什么事跨域?

跨域:域名不一致就是跨域,
主要包括:域名不同和域名相同但端口不通
如何解决?编写gateway的配置文件

spring:
  cloud:
    gateway:
      # 。。。
      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 # 这次跨域检测的有效期

6.6 网关如何实现全局过滤

@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取request和response对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //2.判断是否是登录
        if(request.getURI().getPath().contains("/login")){
            //放行
            return chain.filter(exchange);
        }


        //3.获取token
        String token = request.getHeaders().getFirst("token");

        //4.判断token是否存在
        if(StringUtils.isBlank(token)){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //5.判断token是否有效
        try {
            Claims claimsBody = AppJwtUtil.getClaimsBody(token);
            //是否是过期
            int result = AppJwtUtil.verifyToken(claimsBody);
            if(result == 1 || result  == 2){
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }
        }catch (Exception e){
            e.printStackTrace();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //6.放行
        return chain.filter(exchange);
    }

    /**
     * 优先级设置  值越小  优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

7消息列队

7.1如何进⾏消息队列选型?

  • Kafka:
    优点: 吞吐量非常⼤,性能非常好,集群高可⽤。
    缺点:会丢数据,功能比较单⼀。
    使⽤场景:⽇志分析、⼤数据采集
  • RabbitMQ:
    优点: 消息可靠性⾼,功能全⾯。
    缺点:吞吐量⽐较低,消息积累会严重影响性能。erlang语⾔不好定制。
    使⽤场景:⼩规模场景。
  • RocketMQ:
    优点:⾼吞吐、⾼性能、⾼可⽤,功能⾮常全⾯。
    缺点:开源版功能不如云上商业版。官⽅⽂档和周边⽣态还不够成熟。客户端只⽀持java。
    使⽤场景:⼏乎是全场景。

7.2 RabbitMQ如何确保消息不丢失

  1. 开启生产者确认机制,确保生产者的消息能够到达队列,如果出现错误,可以先记录到日志在处理
  2. 开启持久化功能。确保消息未消费之前队列不会丢失,其中交换机、队列、和消息都要做持久化。

①交换机持久化

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

②队列持久化

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

③消息持久化,SpringAMQP中的消息持久化是默认的

  1. 开启消费者确认机制为auto,由Spring确认消息消费成功之后返回ack,抛出异常返回nack,当然也要设置重试次数,超过次数就将失败的消息投递到异常交换机,有人工处理。

7.4 RabbitMQ重复消费是怎么解决的

重复消费:当消费者设置了自动确认机制,当服务来没有来得及给MQ确认的时候,服务器宕机了,导致服务重启之后,又消费力一次消息。
解决:给每条消息设置唯一id,当服务器重启后,先到数据库去查询这个数据是否存在,如果不存在说明没有处理过,那么正常处理这条数据就行了,如果存在了说明处理过了,这样就避免了重复消费。

7.5 RabbitMQ死信交换机(延迟队列)

  • 像超时订单、限时优惠、定时发布会用到延迟队列
  • 其中延迟队列就用到了死信交换机和TTL(消息存活时间)实现的
  • 消息超时未消费就会变成死信(消费拒绝被消费,列队满了也会成为死信)

7.6 什么是惰性队列

  • 接收到的消息直接存入磁盘,而不是内存
  • 消费者消费消息时才会读到内存
  • 支持百万条消息存储
    如何实现惰性列队
    方式一:(声明队列时)
@Bean
public Queue lazyQueeu(){
	return QueueBuilder
			.durable("lazy.queue")
			.lazy()//开启x-queue-mode为lazy
			.build();
}

方式二:(注解)

@RabbitListener(queueToDeclare = @Queue(
		name="lazy.queue",
		durable("ture"),
		arguments = @Argument(name = "x-queue-mode",value = "lazy")//添加此行
))
public void listenLazyQueue(Strinf msg){
	log.info("{}",msg)
}

7.7 消息堆积如何解决

  • 增加更多的消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上线,采用惰性队列

7.8 RabbitMQ高可用机制了解吗?

  • 一般情况下,使用镜像模式搭建的集群,共有3个节点
  • 镜像列队结构是一主多从(从就是镜像),所有操作都是主节点完成,然后同步给镜像节点。
  • 主节点宕机后,镜像节点会代替成为新的主节点(如果在主节点同步数据之前宕机,可能存在数据丢失)

7.8.1那丢失怎么办呢?

使用仲裁列队,与镜像列队一样,但是它同步基于Raft协议,强一致
仲裁列队使用也比较简单,不需要额外的配置,只需在声明队列时指定它是仲裁队列就行了

7.9 消息列队的作用(为什么使用消息服务)

  • 应用解耦:如果有订单系统,下单成功之后,需要调用库存系统扣减库存;随着公司业务的发展又需要增加物流系统接收下单信息;此时有需要订单系统增加调用物流系统的代码;但是增加队列后,订单系统只需要将下单信息发送到队列,其他系统需要这个消息自己来订阅就可以了;并且也不会因为库存系统异常导致下单失败
  • 异步提速:用户注册成功后,需要先将数据保存,并且发送邮件通知,邮件通知成功后再发送短息通知,短信通知成功后才响应客户成功消息。用户体验很差。增加消息队列后,只需要保存数据,将发送邮件通知和短信通知消息发送到消息队列,可以极大提升响应速度
  • 流量削峰:如果订单系统每秒只能处理1k的请求量,但是在某一瞬间,比如抢单,每秒可以达到1W甚至更多的请求,这种情况下可能会直接导致订单系统崩溃;增加消息队列后,订单系统没拉取1K请求,可以很平稳的去消费消息。

7.10RabbitMQ工作原理

在这里插入图片描述
原理(流程):
(1)消息发布者向RabbitMQ代理(Broker)指定的虚拟主机服务器发送消息
(2)虚拟主机服务器内部的交换机接收消息,并将消息传递并存储到与之绑定的(Binding)的消息列队中
(3)消费者通过一定的网络连接,与消息代理建立连接,同时为了简化开发,在连接内部使用了复用的信道进行消息的最终消费。
Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker
Virtual host:Virtual host是一个虚拟主机的概念,一个Broker中可以有多个Virtual host,每个Virtual host都有一套自己的Exchange和Queue,同一个Virtual host中的Exchange和Queue不能重名,不同的Virtual host中的Exchange和Queue名字可以一样。这样,不同的用户在访问同一个RabbitMQ Broker时,可以创建自己单独的Virtual host,然后在自己的Virtual host中创建Exchange和Queue,很好地做到了不同用户之间相互隔离的效果。
Connection:publisher/consumer和borker之间的TCP连接
channel:发送消息的通道,如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程 序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客 户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发 消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
Queue:Queue是一个用来存放消息的队列,生产者发送的消息会被放到Queue中,消费者消费消息时也是从Queue中取走消息。
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保 存到 exchange 中的查询表中,用于 message 的分发依据

7.11RabbitMQ常见的工作模式

7.11.1 Work queues(工作列队模式)

不需要设置交换机(会使用默认的交换机进行消息转换),需要指定消息列队,消费者通过轮训的方式接收消息

7.11.2 发布订阅模式

需要配置fanout类型交换机,同时将消息路由到每一条消息列队上,然后每个消息列队都可以对相同的消息进行接收存储,进而由各自消息列队关联的消费者进行消费

7.11.3 Routing(路由模式)

需要配置direct类型交换机,并且指定不同的路由键值(Rounting Key)将消息从交换机路由到不同的消息列队进行存储,由消费者进行各自消费

7.11.4Topics(通配符模式)

需要设置topic类型的交换机,并指定不同的路由键值(Routing Key)将对应的消息从交换机路由到不同的消息列队进行存储,由消费者进行各自消费;Topics模式与Routing模式不同的是:Topics模式的路由键值是包含通配符的,用"."和其他字符连接。

7.12 SpringAmqp的使用

7.12.1 发消息

  1. 引依赖
  2. 编写发布服务的配置文件
spring:
  rabbitmq:
    host: 192.168.150.101 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码
  1. 发布消息
 	@Autowired
    private RabbitTemplate rabbitTemplate;

    public void testSimpleQueue() {
        // 队列名称
        String queueName = "simple.queue";
        // 消息
        String message = "hello, spring amqp!";
        // 发送消息
        rabbitTemplate.convertAndSend(queueName, message);//这里可以设置Routing key比如: rabbitTemplate.convertAndSend(exchangeName, "red", message);
    }

7.12.2接收消息

  1. 编写消费服务的配置文件
spring:
  rabbitmq:
    host: 192.168.150.101 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码

2.接受消息

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueueMessage(String msg) throws InterruptedException {
        System.out.println("spring 消费者接收到消息:【" + msg + "】");
    }
}

7.13 使用注解的方式声明和发布交换机

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue1"),
    exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
    System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "direct.queue2"),
    exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
    key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
    System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}

7.12.3 生产者确认,持久化,ack确认

package com.atguigu.mq.listener;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Slf4j
public class MyMessageListener {

    public static final String EXCHANGE_DIRECT = "exchange.direct.order";
    public static final String ROUTING_KEY = "order";
    public static final String QUEUE_NAME  = "queue.order";

    // 修饰监听方法
    @RabbitListener(
            // 设置绑定关系
            bindings = @QueueBinding(

                // 配置队列信息:durable 设置为 true 表示队列持久化;autoDelete 设置为 false 表示关闭自动删除
                value = @Queue(value = QUEUE_NAME, durable = "true", autoDelete = "false"),

                // 配置交换机信息:durable 设置为 true 表示队列持久化;autoDelete 设置为 false 表示关闭自动删除
                exchange = @Exchange(value = EXCHANGE_DIRECT, durable = "true", autoDelete = "false"),

                // 配置路由键信息
                key = {ROUTING_KEY}
    ))
    public void processMessage(String dataString, Message message, Channel channel) throws IOException {

        // 1、获取当前消息的 deliveryTag 值备用
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            // 2、正常业务操作
            log.info("消费端接收到消息内容:" + dataString);
            
            // System.out.println(10 / 0);

            // 3、给 RabbitMQ 服务器返回 ACK 确认信息
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {

            // 4、获取信息,看当前消息是否曾经被投递过
            Boolean redelivered = message.getMessageProperties().getRedelivered();

            if (!redelivered) {
                // 5、如果没有被投递过,那就重新放回队列,重新投递,再试一次
                channel.basicNack(deliveryTag, false, true);
            } else {
                // 6、如果已经被投递过,且这一次仍然进入了 catch 块,那么返回拒绝且不再放回队列
                channel.basicReject(deliveryTag, false);
            }

        }
    }

}

7.12.4 rabbitMQ 配置

spring:
  rabbitmq:
    host: 192.168.200.100
    port: 5672
    username: guest
    password: 123456
    virtual-host: /
    publisher-confirm-type: correlated #交换机确认机制
    publisher-returns: true  #队列的确认机制
    listener:
      simple:
        acknowledge-mode: manual   #ack手动提交
        prefetch: 1 # 设置每次最多从消息队列服务器取回多少消息

7.12.5 灵活应用Exchange Queue

Queue的参数哦列表有哪些

import org.springframework.amqp.core.Queue;


@Bean
    Queue queue(){
        return new Queue(QueueName,true,false,false,new HashMap<String, Object>(){
            {
                put("x-message-ttl", 60000);
            }
        });
    }
----------------------------相当于----------------------------
@Bean
    public Queue queue(){
        return QueueBuilder.durable(name).withArgument("x-message-ttl",10000)
                .build();
    }   
    参数1:队列名称
    参数2:持久化
    参数3:排他性:为true时 只能有一个消费者和它连接,连接关闭时,队列自动删除
    参数4:autoDelete 为true是 没有消费之连接就自动删除列队
    参数5:参数(Arguments) 是一个Map类型,可以为队列设置一些额外参数
    	  如:
    	  1.消息在列队存活时间(毫秒),超时自动删除或发送到私信队列
    	  2.列队生存时间
    	  3.最大消息数
    	  4.列队占用最大空间
    	  5.死信交换机名称
    	  6.发送到死信交换机的路由键
    	  等
定义交换机    	  
@Bean
    DirectExchange directExchange(){
        return new DirectExchange(ExchangeName,true,false);
    }
绑定队列到交换机    
@Bean
    Binding binding(){
        return BindingBuilder.bind(queue()).to(directExchange()).with(RoutingKey);
    }    

7.12.6 死信交换机 私信队列

在这里插入图片描述

就是一个正常的队列,设置了一个TTL,成为死信之后,消息会发送到死信交换机,在路由到死信队列进一步处理
第一步 定义死信交换机和死信队列

	public static final String ExchangeName = "DLXExchange";
    public static final String QueueName = "DLXQueue";
    public static final String keyName = "red";
	
	@Bean
    DirectExchange dlxdirectExchange(){
        return new DirectExchange(ExchangeName,true,false);
    }
    @Bean
    Queue dlxqueue(){
        return new Queue(QueueName);
    }
    @Bean
    Binding dlxbinding(){
        return BindingBuilder.bind(dlxqueue()).to(dlxdirectExchange()).with(keyName);
    }

第二步:定义一个正常的列队和交换机,只不过设置一个发送死信交换机的条件TTL,然后将队列绑定到死信交换机

 @Bean
   DirectExchange directExchange(){
       return new DirectExchange(ExchangeName,true,false);
   }
   @Bean
   Queue queue(){
       Map<String, Object> args = new HashMap<>();
       args.put("x-message-ttl",0);  //设置发送到死信交换机的条件
       args.put("x-dead-letter-exchange",MqDLXConfig.ExchangeName); //绑定死信交换机  因为死信交换机不止有一个
       args.put("x-dead-letter-routing-key",MqDLXConfig.keyName);//死信交换机路由到死信队列  因为和死信交换机绑定的死信队列不止有一个
       return new Queue(QueueName,true,false,false,args);
   }
   @Bean
   Binding binding(){
       return BindingBuilder.bind(queue()).to(directExchange()).with(KeyName);
   }

第三步:使用注解从死信队列消费就可以了

   @RabbitListener(queues = MqDLXConfig.QueueName)

**注意:**运行一次如果绑定出错,那么需要删除交换机和队列

7.12.7 延迟队列

方式一:使用插件
创建交换机和队列

@Configuration
public class RabbitConfig {

    public static final String DelayExchange = "DelayExchange";
    public static final String DelayQueue = "DelayQueue";
    public static final String delayKey = "red";

    @Bean
    public Queue queue(){
        return new Queue(DelayQueue);
    }

    /**
     * 使用Spring提供的交换机CustomExchange
     * CustomExchange,第二个参数决定消息如何路由到队列 也是交换机类型 是固定的 延迟交换机
     * args制定了分发消息的类型 是路由模式
     * @return
     */
    @Bean
    public CustomExchange customExchange(){
        Map<String,Object> args = new HashMap<>();
        args.put("x-delayed-type","direct"); //实际的交换机类型 ,不可以直接使用DirectExchange,因为他不能指定消息延迟发送到队列
        return new CustomExchange(DelayExchange,"x-delayed-message",true,false,args);
    }
    @Bean
    public Binding binding(){
        return BindingBuilder.bind(queue()).to(customExchange()).with(delayKey).noargs();
    }

}

消费者监听

@Component
public class MsqConsumer {
    @RabbitListener(queues = RabbitConfig.DelayQueue)
    public void handle(String msg){
        System.out.println(new Date() + msg);
    }
}

发送消息

 @Resource
    private RabbitTemplate rabbitTemplate;

    @Test
    void contextLoads() throws UnsupportedEncodingException, InterruptedException {
        int i = 0;
        while (i++<=20){
        Thread.sleep(1000);
        Message msg = MessageBuilder.withBody((" <- " + i + " -> hello "+new Date()).getBytes("UTF-8")).setHeader("x-delay", 3000).build();
        rabbitTemplate.convertAndSend(RabbitConfig.DelayExchange,RabbitConfig.delayKey,msg);
        }
    }

方式二:使用死信列队实现延迟队列
先定义死信交换机、队列,在定义正常的消息队列,因为正常的队列要绑定死信交换机
延迟列队图解:
在这里插入图片描述

@Configuration
public class RabbitConfig {
    //死信队列名称
    public static final String DLX_Exchange = "DLX_Exchange";
    public static final String DLX_Queue = "DLX_Queue";
    public static final String DlX_Key = "red";
    //普通队列名称
    public static final String DLX2Exchange = "DLX2Exchange";
    public static final String DLX2Queue = "DLX2Queue";
    public static final String DLX2Key = "yellow";

    //先定义死信交换机和死信队列
    @Bean
    public Queue DlxQueue(){
        return new Queue(DLX_Queue,true,false,false);
    }
    @Bean
    public DirectExchange DlxDirectExchange(){
        return new DirectExchange(DLX_Exchange);
    }
    @Bean
    public Binding DlxBinding(){
        return BindingBuilder.bind(DlxQueue()).to(DlxDirectExchange()).with(DlX_Key);
    }

    //普通队列
    @Bean
    public Queue queue(){
        Map<String,Object> args = new HashMap<>();
        args.put("x-message-ttl",1000*10); //设置TTL这样才能发送到死信交换机
        args.put("x-dead-letter-routing-key",DlX_Key);//因为死信交换机不止一个,所以要找到具体的死信交换机
        args.put("x-dead-letter-exchange",DLX_Exchange);//因为和死信交换机绑定的私信队列不止一个,所以要找的具体定死信队列,通过Routingn key的方式
        return new Queue(DLX2Queue,true,false,false,args); //别忘了args
    }
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(DLX2Exchange);
    }
    @Bean
    public Binding binding(){
        return BindingBuilder.bind(queue()).to(directExchange()).with(DLX2Key);
    }

消费数据

@Component
public class ListenerConsumer {
    @RabbitListener(queues = RabbitConfig.DLX_Queue)
    public void handel(String msg){
        System.out.println("消费时间 "+new Date()+" msg " + msg);
    }
}

发送数据

@SpringBootTest
class MqDlxAchieveDelayApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    void contextLoads() throws InterruptedException {
        for (int i = 1;i <= 20; ++i ){
            Thread.sleep(1000);
            String msg = i + "hello " + new Date();
            rabbitTemplate.convertAndSend(RabbitConfig.DLX2Exchange,RabbitConfig.DLX2Key,msg);
        }
    }
}

7.12.8 生产者确认机制

配置类

##发送交换机的回调
spring.rabbitmq.publisher-confirm-type=correlated
##发送到队列的回调 消息如果没有成功发送回到队列,会触发回调方法
spring.rabbitmq.publisher-returns=true

配置交换机、列队、路由键、RabbitTemplate
对RabbitTemplate配置回调函数
1.实现RabbitTemplate.ConfirmCallback交换机的回调 重写confirm,不管消息成功是否发送到交换机,都会回调confirm方法,三个参数,b即ACK,boolean,s可以看到失败的原因
2.实现RabbitTemplate.ReturnsCallback队列的回调 重写returnedMessage,只有失败发送的队列,才会调用该方法,参数可以查看路由键、交换机等信息

@Configuration
@Slf4j
public class RabbitConfig implements RabbitTemplate.ReturnsCallback,RabbitTemplate.ConfirmCallback {
    public static final String ExchangeName = "ACK_Exchange";
    public static final String QueueName = "ACK_Queue";
    public static final String KeyName = "ACK_Key";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Bean
    public Queue queue(){
        return new Queue(QueueName);
    }
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange(ExchangeName);
    }
    @Bean
    public Binding binding(){
        return BindingBuilder.bind(queue()).to(directExchange()).with(KeyName);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {
        //发送成功或失败都会调用该方法 可以根据b来判断是否发送成功
        if (b){
            log.info("消息成功发送到交换机!,消息体:{}",correlationData);
        }else {
            log.info("我嘞个去:消息->{}"+correlationData+"原因"+ s);
        }
    }
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
        log.info("看到了我,说明消息已经失败发送到队列了!");
        log.info("失败的消息-->{}",returnedMessage.getMessage());
        log.info("消息的交换机-->{}",returnedMessage.getExchange());
        log.info("消息的路由键-->{}",returnedMessage.getRoutingKey());
        log.info("应答码-->{}",returnedMessage.getReplyCode());
        log.info("描述-->{}",returnedMessage.getReplyText());

    }

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }
}

7.12.8 一些发送失败的自带重试配置

spring.rabbitmq.template.retry.enabled=true
spring.rabbitmq.template.retry.initial-interval=1000ms
spring.rabbitmq.template.retry.max-attempts=10
spring.rabbitmq.template.retry.max-interval=10000ms
spring.rabbitmq.template.retry.multiplier=2

对应说明:
开启重试机制。
重试起始间隔时间。
最大重试次数。
最大重试间隔时间。
间隔时间乘数。(这里配置间隔时间乘数为 2,则第一次间隔时间 1 秒,第二次重试间隔时间 2 秒,第三次 4 秒,以此类推)

7.12.9 消息确认

  1. 一种是自动确认(默认):成功消费之后,自动发送,消息就会从队列中移除,7.12.5-8使用的都是这种
  2. 另一种是手动的:通过管道channel发送 结合try-catch很好的实现一下确认机制
  • basicAck:这个是手动确认消息已经成功消费,该方法有两个参数:第一个参数deliveryTag表示消息的 id;第二个参数 multiple 如果为 false,表示仅确认当前消息消费成功,如果为 true,则表示当前消息之前,所有未被当前消费者确认的消息都消费成功。
  • basicNack:这个是告诉 RabbitMQ 当前消息未被成功消费,该方法有三个参数:第一个参数deliveryTag表示消息的 id;第二个参数 multiple 如果为 false,表示仅拒绝当前消息的消费,如果为 true,则表示拒绝当前消息之前,所有未被当前消费者确认的消息;第三个参数 requeue 含义和前面所说的一样,被拒绝的消息是否重新入队。
  • basicReject:根据指定的deliveryTag,对该消息表示拒绝,该方法有两个参数:第一个参数deliveryTag表示消息的 id,第二个参数requeue,取值为true:Broker将消息重新放回队列,接下来会重新投递给消费端;取值为false:Broker将消息标记为已消费,不会放回队列

basicNack()和basicReject()有啥区别?
basicNack()有批量操作
basicReject()没有批量操作

对应实例 自动

@Component
public class Consumer {
    @RabbitListener(queues = RabbitConfig.QueueName)
    public void handler(String msg) throws InterruptedException {
        System.out.println(msg);
        System.out.println("已处理一条消息");
        Thread.sleep(2000);
    }
}

手动 和7.12.3就是差不多的了
在这里插入图片描述

@Component
@Slf4j
public class Consumer {

    @RabbitListener(queues = RabbitConfig.QueueName)
    public void handler(String dataString, Message message, Channel channel) throws IOException {
        //获取消息id
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            log.info("消息内容:{}",dataString);
            //给MQ返回ACK信息 带上消息id 表示消费成功 MQ根据id进行删除
            System.out.println(10/0);
            channel.basicAck(deliveryTag,false);
        }catch (Exception e){
            //获取信息,看当前消息是否曾经被投递过
            Boolean redelivered = message.getMessageProperties().getRedelivered();
            if (!redelivered){
                log.info("消息{}重新投递一次",dataString);
                channel.basicNack(deliveryTag,false,true);
            }else {
                log.info("给你机会不中用{} 滚吧",dataString);
                channel.basicReject(deliveryTag,false);
            }
        }
    }
}           

由于该类抛出了IOException还要捕获它

7.12.10 幂等性问题

大家设想下面一个场景:

消费者在消费完一条消息后,向 RabbitMQ 发送一个 ack 确认,此时由于网络断开或者其他原因导致 RabbitMQ 并没有收到这个ack,那么此时 RabbitMQ并不会将该条消息删除,当重新建立起连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。同时,由于类似的原因,消息在发送的时候,同一条消息也可能会发送两次(参见四种策略确保RabbitMQ 消息发送可靠性!你用哪种?)。种种原因导致我们在消费消息时,一定要处理好幂等性问题。

思路:

采用 Redis,在消费者消费消息之前,现将消息的 id 放到 Redis 中,存储方式如下:
id-0(正在执行业务)
id-1(执行业务成功)
如果 ack 失败,在 RabbitMQ 将消息交给其他的消费者时,先执行 setnx,如果 key 已经存在(说明之前有人消费过该消息),获取他的值,如果是 0,当前消费者就什么都不做,如果是 1,直接 ack。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值