RabbitMQ理论+实战

本文详细介绍了RabbitMQ的中间件应用场景,如数据传输、流量削峰、消息分发等,以及常用的AMQP、MQTT、Kafka等协议。此外,还探讨了RabbitMQ的高可用机制,包括主从模式、多主集群等,并通过实例展示了如何在Linux环境中安装和配置RabbitMQ。最后,文章讨论了分布式事务的几种解决方案,如两阶段提交、补偿事务和MQ事务消息,并分析了各自的优缺点。
摘要由CSDN通过智能技术生成

1.引出

1.1 中间件应用场景

1.跨系统数据传输
2.高并发的流量削峰
3.数据的分发与异步处理
4.大数据分析与传递
5.分布式事务

1.2 中间件常用协议

01、什么是协议

所谓协议是指:
1:计算机底层操作系统和应用程序通讯时共同遵守的一组约定,只有遵循共同的约定和规范,系统和底层操作系统之间才能相互交流。
2:和一般的网络应用程序的不同它主要负责数据的接受和传递,所以性能比较的高。
3:协议对数据格式和计算机之间交换数据都必须严格遵守规范。

02、网络协议的三要素

1.语法。语法是用户数据与控制信息的结构与格式,以及数据出现的顺序。
2.语义。语义是解释控制信息每个部分的意义。它规定了需要发出何种控制信息,以及完成的动作与做出什么样的响应。
3.时序。时序是对事件发生顺序的详细说明。

比如我MQ发送一个信息,是以什么数据格式发送到队列中,然后每个部分的含义是什么,发送完毕以后的执行的动作,以及消费者消费消息的动作,消费完毕的响应结果和反馈是什么,然后按照对应的执行顺序进行处理。如果你还是不理解:大家每天都在接触的http请求协议:

1:语法:http规定了请求报文和响应报文的格式。
2:语义:客户端主动发起请求称之为请求。(这是一种定义,同时你发起的是post/get请求)
3:时序:一个请求对应一个响应。(一定先有请求在有响应,这个是时序)

而消息中间件采用的并不是http协议,而常见的消息中间件协议有:OpenWire、AMQP、MQTT、Kafka,OpenMessage协议。

面试题:为什么消息中间件不直接使用http协议呢?
1: 因为http请求报文头和响应报文头是比较复杂的,包含了cookie,数据的加密解密,状态码,响应码等附加的功能,但是对于一个消息而言,我们并不需要这么复杂,也没有这个必要性,它其实就是负责数据传递,存储,分发就行,一定要追求的是高性能。尽量简洁,快速。
2:大部分情况下http大部分都是短链接,在实际的交互过程中,一个请求到响应很有可能会中断,中断以后就不会就行持久化,就会造成请求的丢失。这样就不利于消息中间件的业务场景,因为消息中间件可能是一个长期的获取消息的过程,出现问题和故障要对数据或消息就行持久化等,目的是为了保证消息和数据的高可靠和稳健的运行。

03:AMQP协议

AMQP:(全称:Advanced Message Queuing Protocol) 是高级消息队列协议。由摩根大通集团联合其他公司共同设计。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有RabbitMQ等。
特性:
1:分布式事务支持。
2:消息的持久化支持。
3:高性能和高可靠的消息处理优势。

AMQP协议的支持者:
在这里插入图片描述
AMQP生产者流转过程
channer(信道) 效率高
加粗样式
AMQP消费者流转过程
在这里插入图片描述

04:MQTT协议

MQTT协议:(Message Queueing Telemetry Transport)消息队列是IBM开放的一个即时通讯协议,物联网系统架构中的重要组成部分。
特点:
1:轻量
2:结构简单
3:传输快,不支持事务
4:没有持久化设计。
应用场景:
1:适用于计算能力有限
2:低带宽
3:网络不稳定的场景。
支持者:

05、OpenMessage协议

是近几年由阿里、雅虎和滴滴出行、Stremalio等公司共同参与创立的分布式消息中间件、流处理等领域的应用开发标准。
特点:
1:结构简单
2:解析速度快
3:支持事务和持久化设计。
在这里插入图片描述

06、Kafka协议

kafka协议是基于TCP/IP的二进制协议。消息内部是通过长度来分割,由一些基本数据类型组成。
特点是:
1:结构简单
2:解析速度快
3:无事务支持
4:有持久化设计

				ActiveMQ	RabbitMQ	Kafka	RocketMQ
文件存储			 支持		  支持		 支持		支持
数据库		  	 支持		   /		  /			/
04、消息分发策略的机制和对比

在这里插入图片描述

1.3 消息队列高可用和高可靠

01、什么是高可用机制

所谓高可用:是指产品在规定的条件和规定的时刻或时间内处于可执行规定功能状态的能力。
当业务量增加时,请求也过大,一台消息中间件服务器的会触及硬件(CPU,内存,磁盘)的极限,一台消息服务器你已经无法满足业务的需求,所以消息中间件必须支持集群部署。来达到高可用的目的。

02、集群模式1 - Master-slave主从共享数据的部署方式

在这里插入图片描述

解说:生产者将消费发送到Master节点,所有的都连接这个消息队列共享这块数据区域,Master节点负责写入,一旦Master挂掉,slave节点继续服务。从而形成高可用。

03、集群模式2 - Master- slave主从同步部署方式

img

解释:这种模式写入消息同样在Master主节点上,但是主节点会同步数据到slave节点形成副本,和zookeeper或者redis主从机制很类同。这样可以达到负载均衡的效果,如果消费者有多个这样就可以去不同的节点就行消费,以为消息的拷贝和同步会暂用很大的带宽和网络资源。在后续的rabbtmq中会有使用。

04、集群模式3 - 多主集群同步部署模式

img

解释:和上面的区别不是特别的大,但是它的写入可以往任意节点去写入。

05、集群模式4 - 多主集群转发部署模式

img

解释:如果你插入的数据是broker-1中,元数据信息会存储数据的相关描述和记录存放的位置(队列)。
它会对描述信息也就是元数据信息就行同步,如果消费者在broker-2中进行消费,发现自己几点没有对应的消息,可以从对应的元数据信息中去查询,然后返回对应的消息信息,场景:比如买火车票或者黄牛买演唱会门票,比如第一个黄牛有顾客说要买的演唱会门票,但是没有但是他会去联系其他的黄牛询问,如果有就返回。

06、集群模式5 Master-slave与Breoker-cluster组合的方案

img

解释:实现多主多从的热备机制来完成消息的高可用以及数据的热备机制,在生产规模达到一定的阶段的时候,这种使用的频率比较高。

这么集群模式,具体在后续的课程中会进行一个分析和讲解。他们的最终目的都是为保证:消息服务器不会挂掉,出现了故障依然可以抱着消息服务继续使用。

反正终归三句话:
1:要么消息共享,
2:要么消息同步
3:要么元数据共享

2. 安装步骤

2.1 将rabbitmq安装包上传到linux系统中

erlang-22.0.7-1.el7.x86_64.rpm
rabbitmq-server-3.7.18-1.el7.noarch.rpm

2.2 安装Erlang依赖包

rpm -ivh erlang-22.0.7-1.el7.x86_64.rpm
elr -v 看Erlang是否安装成功

2.3安装RabbitMQ安装包(需要联网)

要先安装socat,MQ依赖它
yml install -y socat
# 解压mq
 rpm -ivh  rabbitmq-server-3.7.18-1.el7.noarch.rpm
 systemctl start rabbitmq-server 
 # 开机自启动
 systemctl enable rabbit-server
 联网方式:yum install -y rabbitmq-server-3.7.18-1.el7.noarch.rpm
    注意:默认安装完成后配置文件模板在:/usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example目录中,需要    
            将配置文件复制到/etc/rabbitmq/目录中,并修改名称为rabbitmq.config

2.4复制配置文件

cp /usr/share/doc/rabbitmq-server-3.7.18/rabbitmq.config.example /etc/rabbitmq/rabbitmq.config

2.5查看配置文件位置

ls /etc/rabbitmq/rabbitmq.config

2.6.修改配置文件

vim /etc/rabbitmq/rabbitmq.config

将上图中配置文件中红色部分去掉%%,以及最后的,逗号 修改为下图:

2.7.执行如下命令,启动rabbitmq中的插件管理

rabbitmq-plugins enable rabbitmq_management
出现如下说明:
    Enabling plugins on node rabbit@localhost:
rabbitmq_management
The following plugins have been configured:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
Applying plugin configuration to rabbit@localhost...
The following plugins have been enabled:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch
set 3 plugins.
Offline change; changes will take effect at broker restart.

2.8.启动RabbitMQ的服务

systemctl start rabbitmq-server
systemctl restart rabbitmq-server
systemctl stop rabbitmq-server

2.9.查看服务状态

systemctl status rabbitmq-server

rabbitmq-server.service - RabbitMQ broker
     Loaded: loaded (/usr/lib/systemd/system/rabbitmq-server.service; disabled; vendor preset: disabled)
     Active: active (running) since 三 2019-09-25 22:26:35 CST; 7s ago
   Main PID: 2904 (beam.smp)
     Status: "Initialized"
     CGroup: /system.slice/rabbitmq-server.service
             ├─2904 /usr/lib64/erlang/erts-10.4.4/bin/beam.smp -W w -A 64 -MBas ageffcbf -MHas ageffcbf -
             MBlmbcs...
             ├─3220 erl_child_setup 32768
             ├─3243 inet_gethost 4
             └─3244 inet_gethost 4

2.10.关闭防火墙服务

systemctl disable firewalld
   Removed symlink /etc/systemd/system/multi-user.target.wants/firewalld.service.
    Removed symlink /etc/systemd/system/dbus-org.fedoraproject.FirewallD1.service.
    systemctl stop firewalld   

2.11.访问web管理界面

http://10.15.0.8:15672/

2.12.登录管理界面

username:  guest
password:  guest

3. RabiitMQ 配置

3.1RabbitMQ 管理命令行

1.服务启动相关

systemctl start|restart|stop|status rabbitmq-server

2.管理命令行 用来在不使用web管理界面情况下命令操作RabbitMQ

rabbitmqctl  help  可以查看更多命令

3.插件管理命令行

rabbitmq-plugins enable|list|disable

4.

新增用户
rabbitmqctl add_user admin admin

设置用户分配权限
rabbitmqctl set_user_tags admin administrator

在这里插入图片描述

3.2. Docker安装

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

在这里插入图片描述

3.2 web管理界面介绍

3.2.1 overview概览

在这里插入图片描述

connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况

channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。

Exchanges:交换机,用来实现消息的路由

Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。

3.2.2 Admin用户和虚拟主机管理

1.添加用户

上面的Tags选项,其实是指定用户的角色,可选的有以下几个:

超级管理员(administrator)

可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。

监控者(monitoring)

可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)

策略制定者(policymaker)

可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。

普通管理者(management)

仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。

其他

无法登陆管理控制台,通常就是普通的生产者和消费者。
2. 创建虚拟主机

虚拟主机

为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。

RabbitMQ核心组成部分

在这里插入图片描述
server:又称broker,接受客户端连接,实现AMQP实体服务。

connection:连接和具体broker网络连接。

channel:网络信道,几乎所有操作都在channel中进行,channel是消息读写的通道。客户端可以建立多个channel,每个channel表示一个会话任务。

message:消息,服务器和应用程序之间传递的数据,由properties和body组成。properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性;body是消息实体内容。

Virtual host:虚拟主机,用于逻辑隔离,最上层消息的路由。一个Virtual host可以若干个Exchange和Queue,同一个Virtual host不能有同名的Exchange或Queue。

Exchange:交换机,接受消息,根据路由键转发消息到绑定的队列上。

banding:Exchange和Queue之间的虚拟连接,binding中可以包括routing key

routing key:一个路由规则,虚拟机根据他来确定如何路由 一条消息。

Queue:消息队列,用来存放消息的队列。

RabbitMQ整体架构

在这里插入图片描述

RabbitMQ的运行流程

在这里插入图片描述

4.0公共方法创建交换机和队列

	// 交换机
	String exchangeName = "direct_message_exchage";
	// 交换机类型 direct/topic/fanout/headers
	String exchangeType = "direct";
	//持久化
	channel.exChangeDeclare(exchangeName,exchangeType,true,null);  
	//声明队列
	channel.queueDeclare(“queue5”,true,false,false,null);
	//绑定队列和交换机的关系
		channel.queueBind“queue5”,exchangeName,"order");
	// param2:往什么路由发送消息
	channel.basicPublish(exchangeName,"order",null,message.getBytes());

4.1 简单模式

在这里插入图片描述

  1. 有默认交换机
  2. 发送消息通过交换机发送
  3. 消费者会自动监听和订阅消息
    在上图的模型中,有以下概念:

P:生产者,也就是要发送消息的程序
C:消费者:消息的接受者,会一直等待消息到来。
queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
4. 开发生产者

  //创建连接工厂  rabbit基于amqp
  ConnectionFactory connectionFactory = new ConnectionFactory();
  connectionFactory.setHost("10.15.0.9");
  connectionFactory.setPort(5672);
  connectionFactory.setUsername("ems");
  connectionFactory.setPassword("123");
  connectionFactory.setVirtualHost("/");
  Connection connection = connectionFactory.newConnection("生产者“);
  //创建通道
  Channel channel = connection.createChannel();
  
  //参数1:队列名(虽然不指定交换机,但是会存在一个默认交换机 )
  //参数2: 是否持久化,durable,非持久化消息会存盘 但是服务器重启会消失
  //参数3:是否独占队列 
  //参数4:是否自动删除,最后一个消息读取完毕队列是否自动删除
  //参数5:其他属性
  channel.queueDeclare("hello",true,false,false,null);
  //发送消息给队列
  //参数1:交换机
  //参数2:队列、路由key 
  //参数3:消息的状态控制 
  //参数4:消息主体
  channel.basicPublish("","hello", null,"hello rabbitmq".getBytes());
  channel.close();
  connection.close();

队列里的消息
在这里插入图片描述

  1. 开发消费者
  //创建连接工厂
  ConnectionFactory connectionFactory = new ConnectionFactory();
  connectionFactory.setHost("10.15.0.9");
  connectionFactory.setPort(5672);
  connectionFactory.setUsername("ems");
  connectionFactory.setPassword("123");
  connectionFactory.setVirtualHost("/ems");
  Connection connection = connectionFactory.newConnection();
  Channel channel = connection.createChannel();
  channel.queueDeclare("hello", true, false, false, null);
  channel.basicConsume("hello",true,new DefaultConsumer(channel){
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
      System.out.println("收到消息时"+new String(body));
    }
  });
  1. 参数的说明
    channel.queueDeclare(“hello”,true,false,false,null);
    ‘参数1’:用来声明通道对应的队列
    ‘参数2’:用来指定是否持久化队列
    ‘参数3’:用来指定是否独占队列
    ‘参数4’:用来指定是否自动删除队列
    ‘参数5’:对队列的额外配置

4.2 第二种模型(work quene)

  1. 有默认交换机
  2. 发送消息通过交换机发送
  3. 消费者会自动监听和订阅消息
    Work queues,也被称为(Task queues),任务模型。当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型:让多个消费者绑定到一个队列,共同消费队列中的消息。队列中的消息一旦消费,就会消失,因此任务是不会被重复执行的。
  • 分为公平模式(按照处理能力进行分发)和轮询模式(平分)

角色:

P:生产者:任务的发布者
C1:消费者-1,领取任务并且完成任务,假设完成速度较慢
C2:消费者-2:领取任务并完成任务,假设完成速度快

  1. 开发生产者
// param2 轮询要变成autoAck:true 不然会造成死循环  公平是false
channel.queueDeclare("hello", true, false, false, null);
for (int i = 0; i < 10; i++) {
  channel.basicPublish("", "hello", null, (i+"====>:我是消息").getBytes());
}

2.开发消费者-1

channel.queueDeclare("hello",true,false,false,null);
channel.basicConsume("hello",true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});

3.开发消费者-2

//公平模式 param2 一定是false
channel.queueDeclare("hello",true,false,false,null);
channel.basicConsume("hello",true,new DefaultConsumer(channel){
  // ++ 同一时刻服务器之会推送一条消息给消费者
  // QOS代表每次能取出的数量
  channel.basicQS(1);
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    try {
      Thread.sleep(1000);   //处理消息比较慢 一秒处理一个消息
      // +++
      finalChannel.basicAck(delivery.getEnvelope().getDelivetyTag(),false)
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("消费者2: "+new String(body));  
  }
});

4.测试结果

总结:默认情况下,RabbitMQ将按顺序将每个消息发送给下一个使用者。平均而言,每个消费者都会收到相同数量的消息。这种分发消息的方式称为循环。

5.消息自动确认机制
Doing a task can take a few seconds. You may wonder what happens if one of the consumers starts a long task and dies with it only partly done. With our current code, once RabbitMQ delivers a message to the consumer it immediately marks it for deletion. In this case, if you kill a worker we will lose the message it was just processing. We’ll also lose all the messages that were dispatched to this particular worker but were not yet handled.

But we don’t want to lose any tasks. If a worker dies, we’d like the task to be delivered to another worker.

channel.basicQos(1);//一次只接受一条未确认的消息
//参数2:关闭自动确认消息
channel.basicConsume("hello",false,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
    channel.basicAck(envelope.getDeliveryTag(),false);//手动确认消息
  }
});

NACK

4.3 第三种模型(fanout) 发布订阅

fanout 扇出 也称为广播(有人订阅都能收到)

在广播模式下,消息发送流程是这样的:

可以有多个消费者
每个消费者有自己的queue(队列)
每个队列都要绑定到Exchange(交换机)
生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
交换机把消息发送给绑定过的所有队列
队列的消费者都能拿到消息。实现一条消息被多个消费者消费

  1. 开发生产者
//声明交换机
channel.exchangeDeclare("logs","fanout");//广播 一条消息多个消费者同时消费
//发布消息
channel.basicPublish("logs","",null,"hello".getBytes());
  1. 开发消费者-1
//绑定交换机
channel.exchangeDeclare("logs","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定exchange
channel.queueBind(queue,"logs","");
//处理消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});
  1. 开发消费者-2
//绑定交换机
channel.exchangeDeclare("logs","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定exchange
channel.queueBind(queue,"logs","");
//处理消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者2: "+new String(body));
  }
});

4.开发消费者-3

//绑定交换机
channel.exchangeDeclare("logs","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将临时队列绑定exchange
channel.queueBind(queue,"logs","");
//处理消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者3: "+new String(body));
  }
});
  1. 测试结果
    在这里插入图片描述

4.4 第四种模型(Routing)

在这里插入图片描述

4.4.1 Routing 之订阅模型-Direct(直连)

在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。

在Direct模型下:

队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
流程:
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

  1. 开发生产者
//声明交换机  参数1:交换机名称 参数2:交换机类型 基于指令的Routing key转发
channel.exchangeDeclare("logs_direct","direct");
// 向email的队列发送消息
String key = "email";
//发布消息
channel.basicPublish("logs_direct",key,null,("指定的route key"+key+"的消息").getBytes());

2.开发消费者-1

 //声明交换机
channel.exchangeDeclare("logs_direct","direct");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列和交换机
channel.queueBind(queue,"logs_direct","error");
channel.queueBind(queue,"logs_direct","info");
channel.queueBind(queue,"logs_direct","warn");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});

3.开发消费者-2

//声明交换机
channel.exchangeDeclare("logs_direct","direct");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列和交换机
channel.queueBind(queue,"logs_direct","error");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者2: "+new String(body));
  }
});

4.5 第五种模型-Topic

在这里插入图片描述

Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!这种模型Routingkey 一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
统配符
* (star) can substitute for exactly one word. 匹配不多不少恰好1个词

(hash) can substitute for zero or more words. 匹配没有或多个词

如:
audit.# 匹配audit.irs.corporate或者 audit.irs 等
audit.* 只能匹配 audit.irs
1.开发生产者

//生命交换机和交换机类型 topic 使用动态路由(通配符方式)
channel.exchangeDeclare("topics","topic");
// 向队列为"user.save.*"发送消息
String routekey = "user.save.*";//动态路由key
//发布消息
channel.basicPublish("topics",routekey,null,("这是路由中的动态订阅模型,route key: ["+routekey+"]").getBytes());

2.开发消费者-1
Routing Key中使用*通配符方式

 //声明交换机
channel.exchangeDeclare("topics","topic");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列与交换机并设置获取交换机中动态路由
channel.queueBind(queue,"topics","user.*");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者1: "+new String(body));
  }
});

3.开发消费者-2

Routing Key中使用#通配符方式

//声明交换机
channel.exchangeDeclare("topics","topic");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//绑定队列与交换机并设置获取交换机中动态路由
channel.queueBind(queue,"topics","user.#");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
  @Override
  public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    System.out.println("消费者2: "+new String(body));
  }
});

当发送的队列不存在的时候会报异常,不会自动创建

RabbitMQ使用场景

解耦、异步、高内聚低耦合

5. SpringBoot中使用RabbitMQ

5.0 搭建初始环境

  1. 引入依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 配置配置文件
spring:
  application:
    name: springboot_rabbitmq
  rabbitmq:
    host: 10.15.0.9
    port: 5672
    username: ems
    password: 123
    virtual-host: /ems
# RabbitTemplate 用来简化操作 使用时候直接在项目中注入即可使用

5.1 第一种 fanout模型使用

在这里插入图片描述

开发生产者

@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testHello(){
	String routeKey = null;
  // param1:交换机
  // 2:路由Key/queue 队列名称
  // 3:消息内容
  rabbitTemplate.convertAndSend("fanout_order_exchange",routeKey,"hello world");
}

配置类–配置队列和交换机的关系

@Configuration
public class RabbitMqConfiguration{
    //1.声明注册fanout模式的交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanout_order_exchange",true,false);
    }
    //2.声明队列
    @Bean
    public Queue smsQueue(){
        return new Queue("sms.fanout.queue",true);
    }
    @Bean
    public Queue duanxinQueue(){
        return new Queue("duanxin.fanout.queue",true);
    }
    @Bean
    public Queue emailQueue(){
        return new Queue("email.fanout.queue",true);
    }
    //3.完成绑定关系
    @Bean
    public Binding smsBingding(){
        return BindingBuilder.bin(smsQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding duanxinBingding(){
        return BindingBuilder.bin(duanxinQueue()).to(fanoutExchange());
    }
    @Bean
    public Binding emailBingding(){
        return BindingBuilder.bin(emailQueue()).to(fanoutExchange());
    }
}

开发消费者
FanoutSmsConsumer.java

@Component
@RabbitListener(queue = {"sms.direct.queue"})
public class FanoutSmsConsumer{
    @RabbitHandler
    public void reviceMessage(String message){
        sout("sms接收到了的订单信息是:"+message);
    }
}

FanoutDuanxinConsumer.java

@Component
@RabbitListener(queue = {"duanxin.direct.queue"})
public class FanoutDuanxinConsumer{
    @RabbitHandler
    public void reviceMessage(String message){
        sout("duanxin接收到了的订单信息是:"+message);
    }
}

FanoutEmailConsumer.java

@Component
@RabbitListener(queue = {"duanxin.direct.queue"})
public class FanoutEmailConsumer{
    @RabbitHandler
    public void reviceMessage(String message){
        sout("email接收到了的订单信息是:"+message);
    }
}

5.2 第二种work模型使用(direct)

生产者

OrderService.java

public class OrderService{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    //模拟用户下单
    public void makeOrder(String userid,String productid,int num){
        //1.根据商品id查询库存是否足够
        //2.保存订单
        String orderId = UUID.randomUUID().toString();
        sout("订单生产成功:"+orderId);
        //3.通过MQ来完成消息的分发
        //参数1:交换机 参数2:路由key/queue队列名称 参数3:消息内容
        String exchangeName = "direct_order_exchange";
        String routingKey = "";
        rabbitTemplate.convertAndSend(exchangeName,"email",orderId);
        rabbitTemplate.convertAndSend(exchangeName,"duanxin",orderId);
    }
}

消费者
RabbitMqConfiguration.java

@Configuration
public class RabbitMqConfiguration{
    //1.声明注册fanout模式的交换机
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("direct_order_exchange",true,false);
    }
    //2.声明队列
    @Bean
    public Queue smsQueue(){
        return new Queue("sms.direct.queue",true);
    }
    @Bean
    public Queue duanxinQueue(){
        return new Queue("duanxin.direct.queue",true);
    }
    @Bean
    public Queue emailQueue(){
        return new Queue("email.direct.queue",true);
    }
    //3.完成绑定关系
    @Bean
    public Binding smsBingding(){
        return BindingBuilder.bin(smsQueue()).to(fanoutExchange()).with("sms");
    }
    @Bean
    public Binding duanxinBingding(){
        return BindingBuilder.bin(duanxinQueue()).to(fanoutExchange()).with("duanxin");
    }
    @Bean
    public Binding emailBingding(){
        return BindingBuilder.bin(emailQueue()).to(fanoutExchange()).with("email");
    }
}
public void testHello(){
	String routeKey = null;
  // param1:交换机
  // 2:路由Key/queue 队列名称
  // 3:消息内容
  rabbitTemplate.convertAndSend("direct_order_exchange","email","hello world");
  rabbitTemplate.convertAndSend("direct_order_exchange","短信","hello world");
}
  1. 队列和交换机的绑定关系在消费者或者生产者段都可以,但是在消费者段更好,因为队列要声明好才能运行,消费者和队列的关系更直接
  2. 说明:默认在Spring AMQP实现中Work这种方式就是公平调度,如果需要实现能者多劳需要额外配置

5.3 Topic 订阅模型(动态路由模型)

生产者

public class OrderService{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    //模拟用户下单
    public void makeOrder(String userid,String productid,int num){
        //1.根据商品id查询库存是否足够
        //2.保存订单
        String orderId = UUID.randomUUID().toString();
        sout("订单生产成功:"+orderId);
        //3.通过MQ来完成消息的分发
        //参数1:交换机 参数2:路由key/queue队列名称 参数3:消息内容
        String exchangeName = "direct_order_exchange";
        String routingKey = "com.duanxin";
        rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId);
    }
}

消费者(采用注解)

FanoutSmsConsumer.java

@Component
@RabbitListener(bindings = @QueueBinding(
	value = @Queue(value = "sms.topic.queue",durable = "true",antoDelete = "false"),
    exchange = @Exchange(value = "topic_order_exchange",type = "ExchangeTypes.TOPIC")
    key = "#.sms.#"
))
public class TopicSmsConsumer{
    @RabbitHandler
    public void reviceMessage(String message){
        sout("sms接收到了的订单信息是:"+message);
    }
}

FanoutDuanxinConsumer.java

@Component
@RabbitListener(bindings = @QueueBinding(
	value = @Queue(value = "duanxin.topic.queue",durable = "true",antoDelete = "false"),
    exchange = @Exchange(value = "topic_order_exchange",type = "ExchangeTypes.TOPIC")
    key = "#.duanxin.#"
))

public classTopicDuanxinConsumer{
    @RabbitHandler
    public void reviceMessage(String message){
        sout("duanxin接收到了的订单信息是:"+message);
    }
}

FanoutEmailConsumer.java

@Component
@RabbitListener(bindings = @QueueBinding(
	value = @Queue(value = "email.topic.queue",durable = "true",antoDelete = "false"),
    exchange = @Exchange(value = "topic_order_exchange",type = "ExchangeTypes.TOPIC")
    key = "#.email.#"
))
public class TopicEmailConsumer{
    @RabbitHandler
    public void reviceMessage(String message){
        sout("email接收到了的订单信息是:"+message);
    }
}

6. RabbitMQ高级

6.1过期时间TTL

概述
过期时间 TTl表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置 TTL,目前有两种方法可以设置

  • 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间
  • 第二种方法是对消息进行单独设置,每条消息 TTL可以不同

如果上述两种方法同时使用,则消息的过期时间以两者 TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的 TTL值,就称为 dead message被投递到死信队列,消费者将无法再收到该消息

设置队列TTL

RabbitMqConfiguration.java

@Configuration
public class TTLRabbitMQConfiguration{
    //1.声明注册direct模式的交换机
    @Bean
    public DirectExchange ttldirectExchange(){
        return new DirectExchange("ttl_direct_exchange",true,false);}
    //2.队列的过期时间
    @Bean
    public Queue directttlQueue(){
        //设置队列过期时间
        //存储在这个队列的消息5s之后被移除
        Map<String,Object> args = new HashMap<>();
        args.put("x-message-ttl",5000);//这里一定是int类型
        return new Queue("ttl.direct.queue",true,false,false,args);}
    
    @Bean
    public Binding ttlBingding(){
        return BindingBuilder.bin(directttlQueue()).to(ttldirectExchange()).with("ttl");
    }
}

在这里插入图片描述

设置消息TTL

public class OrderService{
    @Autowired
    private RabbitTemplate rabbitTemplate;
    //模拟用户下单
    public void makeOrder(String userid,String productid,int num){
        //1.根据商品id查询库存是否足够
        //2.保存订单
        String orderId = UUID.randomUUID().toString();
        sout("订单生产成功:"+orderId);
        //3.通过MQ来完成消息的分发
        //参数1:交换机 参数2:路由key/queue队列名称 参数3:消息内容
        String exchangeName = "ttl_order_exchange";
        String routingKey = "ttlmessage";
        //给消息设置过期时间
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
            public Message postProcessMessage(Message message){
                //这里就是字符串
                message.getMessageProperties().setExpiration("5000");
                message.getMessageProperties().setContentEncoding("UTF-8");
                return message;
            }
        }
        rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId,messagePostProcessor);
    }
}

RabbitMqConfiguration.java

@Configuration
public class TTLRabbitMQConfiguration{
    //1.声明注册direct模式的交换机
    @Bean
    public DirectExchange ttldirectExchange(){
        return new DirectExchange("ttl_direct_exchange",true,false);}
    //2.队列的过期时间
    @Bean
    public Queue directttlQueue(){
       //设置过期时间
        Map<String,Object> args = new HashMap<>();
        args.put("x-message-ttl",5000);//这里一定是int类型
        return new Queue("ttl.direct.queue",true,false,false,args);}
    @Bean
    public Queue directttlMessageQueue(){
        return new Queue("ttlMessage.direct.queue",true,false,false,args);}
    
    @Bean
    public Binding ttlBingding(){
        return BindingBuilder.bin(directttlMessageQueue()).to(ttldirectExchange()).with("ttlmessage");
    }
}

如果队列和消息都设置了过期时间,过期的时间按照小的时间来

6.2 RabbitMQ高级 - 死信队列

概述
DLX,全称 Dead-Letter-Exchange,可以称之为死信交换机,也有人称之为死信邮箱。当消息再一个队列中变成死信之后,它能被重新发送到另一个交换机中,这个交换机就是 DLX,绑定 DLX的队列就称之为死信队列。消息变成死信,可能是由于以下原因:

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度
    DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性,当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的 DLX上去,进而被路由到另一个队列,即死信队列。

要想使用死信队列,只需要在定义队列的时候设置队列参数x-dead-letter-exchange指定交换机即可

代码

DeadRabbitMqConfiguration.java

@Configuration
public class DeadRabbitMqConfiguration{
    //1.声明注册direct模式的交换机
    @Bean
    public DirectExchange deadDirect(){
        return new DirectExchange("dead_direct_exchange",true,false);}
    //2.队列的过期时间
    @Bean
    public Queue deadQueue(){
        return new Queue("dead.direct.queue",true);}
    @Bean
    public Binding deadbinds(){
        return BindingBuilder.bind(deadDirect()).to(deadQueue()).with("dead");
    }
}

RabbitMqConfiguration.java

@Configuration
public class TTLRabbitMQConfiguration{
    //1.声明注册direct模式的交换机
    @Bean
    public DirectExchange ttldirectExchange(){
        return new DirectExchange("ttl_direct_exchange",true,false);}
    //2.队列的过期时间
    @Bean
    public Queue directttlQueue(){
        //设置过期时间
        Map<String,Object> args = new HashMap<>();
        //args.put("x-max-length",5); //只能接受五个,超过就转到新建的死信队列
        args.put("x-message-ttl",5000);//这里一定是int类型
        args.put("x-dead-letter-exchange","dead_direct_exchange");
        //配置路由Key
        args.put("x-dead-letter-routing-key","dead");//fanout不需要配置
        return new Queue("ttl.direct.queue",true,false,false,args);}
    @Bean
    public Queue directttlMessageQueue(){
        return new Queue("ttlMessage.direct.queue",true,false,false,args);}
    
    @Bean
    public Binding ttlBingding(){
        return BindingBuilder.bin(directttlMessageQueue()).to(ttldirectExchange()).with("ttlmessage");
    }
}

6.3 内存磁盘的监控

RabbitMQ内存警告
在这里插入图片描述
RabbitMQ的内存控制
参考帮助文档:http://www.rabbbitmq.com/configure.html

当出现警告的时候,可以通过配置去修改和调整
命令的方式

//相对值 0.4-0.7之间
rabbitmqctl set_vm_memory_high_watermark <fraction>
rabbitmqctl set_vm_memory_high_watermark absolute 50MB

fraction/value 为内存阈值。默认情况是:0.4/2GB,代表的含义是:当 RabbitMQ的内存超过40%时,就会产生警告并且会阻塞所有生产者的连接。通过此命令修改阈值在 Broker重启以后将会失效,通过修改配置文件设置的阈值则不会随着重启而消失,但修改了配置文件一样要重启 Broker才会生效

配置文件方式 rabbitmq.conf
在这里插入图片描述

RabbitMQ的内存换页
在这里插入图片描述

RabbitMQ的磁盘预警
在这里插入图片描述

7.RabbitMQ高级 - 集群

7.1 RabbitMQ集群

在这里插入图片描述

集群搭建
配置的前提是你的 rabbitmq可以运行起来,比如ps aix|grep rebbitmq你能看到相关进程,又比如运行rabbitmqct status你可以看到类似如下信息而不报错:
在这里插入图片描述

单机多实例搭建

在这里插入图片描述

启动第二个节点
在这里插入图片描述

验证启动

ps aux|grep rabbitmq

rabbit-1操作作为主节点
在这里插入图片描述

rabbit-2操作作为从节点
在这里插入图片描述

验证集群状态
在这里插入图片描述

Web监控

rabbitmq-plugins enable rabbitmq_management

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

小结
在这里插入图片描述

8.分布式事务

简述
分布式事务指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。

8.1分布式事务的方式

一、两阶段提交(2PC)

两阶段提交(2PC)需要数据库产商的支持,java组件有atomikosdeng

两阶段提交,通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。

准备阶段
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
在这里插入图片描述

存在问题
存在的问题
1、同步阻塞所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
2、单点问题协调者在2PC中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障, 所有参
与者会直等待味态, 无法完成其它操作。
3、数据不-致在阶段二,如果协调者只发送了部分Commit消息,此时网络发生异常,那么只有部分参与者接收
到Commit消息,也就是说只有部分参与者提交了事务,使得系统数据不- - 致。
4、太过保守任意-个节点失败就会导致整个事务失败,没有完善的容错机制。

二、补偿事务(TCC) 严选,阿里,蚂蚁金服。

TCC其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认- - - Confirm阶段是不会出错的。即:只要Try成功,Confirm- 定成功。
  • Cancel阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

举个例子,假入Bob要向Smith转账,思路大概是:我们有一个本地方法,里面依次调用
1: 首先在Try阶段,要先调用远程接口把Smith和Bob的钱给冻结起来。
2:在Confirm阶段,执行远程调用的转账的操作,转账成功进行解冻。
3:如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法(Cancel)。

优点:跟2PC比起来,实现以及流程相对简单了-些,但数据的一致性比2PC也要差一 些
缺点:缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一 些业务流程可能用TCC不太好定义及处理 。

三、本地消息表(异步确保)比如:支付宝、微信支付主动查询支付状态,对账单的形式

本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。

  • 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息, 本地事务能保证这个消息一定会被写入本地消息表中。
  • 之后将本地消息表中的消息转发到Kafka等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
  • 在分布式事务操作的另-方从消息队列中读取一个消息,并执行消息中的操作。
    在这里插入图片描述
    优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
    缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。

四、MQ事务消息

有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。以阿里的 RocketMQ 中间件为例,其思路大致为:

  • 第一阶段Prepared消息,会拿到消息的地址。第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
  • 也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

在这里插入图片描述
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。

通过本文我们总结并对比了几种分布式分解方案的优缺点,分布式事务本身是一个技术难题,是没有一种完美的方案应对所有场景的,具体还是要根据业务场景去抉择吧。笔者上家公司是试用阿里RocketMQ去实现的分布式事务,现在也有除了很多分布式事务的协调器,比如LCN等,大家可以多去尝试。

系统与系统之间的分布式事务问题

在这里插入图片描述

8.2系统间调用过程中事务回滚问题

@Service
public class OrderService {
    @Autowired
    private OrderDataBaseService orderDataBaseService;
    // 创建订单
    @Transactional(rollbackFor = Exception.class) // 订单创建整个方法添加事务
    public void createOrder(Order orderInfo) throws Exception {
        // 1: 订单信息--插入丁订单系统,订单数据库事务
        orderDataBaseService.saveOrder(orderInfo);
        // 2:通過Http接口发送订单信息到运单系统
        String result = dispatchHttpApi(orderInfo.getOrderId());
        if(!"success".equals(result)) {
            throw new Exception("订单创建失败,原因是运单接口调用失败!");
        }
    }
    /**
     *  模拟http请求接口发送,运单系统,将订单号传过去 springcloud
     * @return
     */
    private String dispatchHttpApi(String orderId) {
        SimpleClientHttpRequestFactory factory  = new SimpleClientHttpRequestFactory();
        // 链接超时 > 3秒
        factory.setConnectTimeout(3000);
        // 处理超时 > 2秒
        factory.setReadTimeout(2000);
        // 发送http请求
        String url = "http://localhost:9000/dispatch/order?orderId="+orderId;
        RestTemplate restTemplate = new RestTemplate(factory);//异常
        String result = restTemplate.getForObject(url, String.class);
        return result;
    }
}

基于MQ的分布式事务整体设计思路

在这里插入图片描述

8.3基于MQ的分布式事务消息的可靠生产问题

在这里插入图片描述

如果这个时候MQ服务器出现了异常和故障,那么消息是无法获取到回执信息。怎么解决呢?

8.3.1基于MQ的分布式事务消息的可靠生产问题-定时重发

在这里插入图片描述
增加确认机制
在这里插入图片描述
在这里插入图片描述
要在appication中配置确认类型

publisher-confirm-type: correlated

8.4基于MQ的分布式事务消息的可靠消费

在这里插入图片描述

8.5基于MQ的分布式事务消息的消息重发

在这里插入图片描述

  • 设置重试次数,可能会导致消息丢失,配置了重试次数就不要try/catch
    在这里插入图片描述

  • try/catch + 手动ACK
    在这里插入图片描述
    在这里插入图片描述

  • 2的条件上 加上死信队列

8.6基于MQ的分布式事务消息的死信队列消息转移 + 人工处理

在这里插入图片描述
如果死信队列报错就进行人工处理
在这里插入图片描述

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值