简介
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现。AMQP 的出现其实也是应了广大人民群众的需求,虽然在同步消息通讯的世界里有很多公开标准(如 COBAR的 IIOP ,或者是 SOAP 等),但是在异步消息处理中却不是这样,只有大企业有一些商业实现(如微软的 MSMQ ,IBM 的 Websphere MQ 等),因此,在 2006 年的 6 月,Cisco 、Redhat、iMatix 等联合制定了 AMQP 的公开标准。
RabbitMQ是由RabbitMQ Technologies Ltd开发并且提供商业支持的。该公司在2010年4月被SpringSource(VMWare的一个部门)收购。在2013年5月被并入Pivotal。其实VMWare,Pivotal和EMC本质上是一家的。不同的是VMWare是独立上市子公司,而Pivotal是整合了EMC的某些资源,现在并没有上市。
注意:RabbitMQ是采用erlang语言开发的,所以必须有erlang环境才可以运行
环境安装
1、下载并安装erlang
下载地址:http://www.erlang.org/download
2、配置erlang环境变量信息
新增环境变量ERLANG_HOME=erlang的安装地址,并将%ERLANG_HOME%\bin加入到path
3、下载并安装RabbitMQ
下载地址:http://www.rabbitmq.com/download.html
注意: RabbitMQ 它依赖于Erlang,需要先安装Erlang。
RabbitMQ管理平台
RabbitMQ 管理平台地址 http://127.0.0.1:15672
默认账号:guest/guest 用户可以自己创建新的账号
Virtual Hosts
像mysql有数据库的概念并且可以指定用户对库和表等操作的权限。那RabbitMQ呢?RabbitMQ也有类似的权限管理。在RabbitMQ中可以虚拟消息服务器VirtualHost,每个VirtualHost相当月一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通。
不同的团队,不同的项目有自己独立的virtualhost,好处就是进行相互隔离。适合于大型互联网公司进行使用区分不同的业务逻辑。
RabbitMQ消息确认机制
- AMQP 事务机制
- Confirm 模式
事务模式
- txSelect 将当前channel设置为transaction模式
- txCommit 提交当前事务
- txRollback 事务回滚
应答模式
- 自动应答
不在乎消费者对这个消息处理是否成功,都会告诉队列删除该消息。如果处理消息失败,会实现自动补偿(重试)。
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
- 手动应答
//生产者端代码不变,消费者端代码这部分就是用于开启手动应答模式的。
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
//注:第二个参数值为false代表关闭RabbitMQ的自动应答机制,改为手动应答。
//在处理完消息时,返回应答状态,true表示为自动应答模式。
channel.basicAck(envelope.getDeliveryTag(), false);
消费处理完业务逻辑后,手动返回ack(通知),告诉给队列服务器是否删除该消息。
RabbitMQ消息重试机制
-
消费者在消费消息的时候,如果消费者业务逻辑出现程序异常
此时应该使用消息重试机制。 - 消费者获取到消息后,调用第三方接口,但接口暂时无法访问
此时需要重试机制。
- 消费者获取到消息后,抛出数据转换异常
此时不需要重试机制,需要重新发布才能解决。可以采用日志记录+定时任务job健康检查+人工进行补偿
五种形式队列
- 点对点(简单)的队列
- 工作(公平性)队列模式
- 发布订阅模式
- 路由模式Routing
- 通配符模式Topics
简单队列
推送:消费者已经启动,与队列建立起长连接,一旦生产者向队列投递消息,队列会立马推送给消费者。
拉取:生产者先投递消息给队列,这时候消费者再启动的时候,就会向队列获取消息。
1、pom文件引入
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.5</version>
</dependency>
</dependencies>
2、封装Connection
public class MQConnectionUtils {
// 创建新的MQ连接
public static Connection newConnection() throws IOException, TimeoutException {
// 1.创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 2.设置连接地址
factory.setHost("127.0.0.1");
// 3.设置用户名称
factory.setUsername("admin");
// 4.设置用户密码
factory.setPassword("admin");
// 5.设置amqp协议端口号
factory.setPort(5672);
// 6.设置VirtualHost地址
factory.setVirtualHost("/admin_host");
Connection connection = factory.newConnection();
return connection;
}
}
3、生产者
// 简单队列生产者
public class Producer {
// 队列名称
private static final String QUEUE_NAME = "my_queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 1.创建一个新的连接
Connection connection = MQConnectionUtils.newConnection();
// 2.创建通道
Channel channel = connection.createChannel();
// 3.创建一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
for (int i = 1; i <= 50; i++) {
// 4.创建msg
String msg = "my_msg_" + i;
// 5.生产者发送消息者
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
4、消费者
public class Consumer1 {
// 队列名称
private static final String QUEUE_NAME = "my_queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 1.创建一个新的连接
Connection connection = MQConnectionUtils.newConnection();
// 2.创建通道
final Channel channel = connection.createChannel();
// 3.消费者关联队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
// 监听获取消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消费者获取生产者消息:" + msg);
}
};
//4.设置应答模式 如果为true情况下 表示为自动应答模式 false 表示为手动应答
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
消费者集群的情况下,默认采用均摊消费。
假如生产者生产10个消息,那么消费1和消费者2个字消费5个。
公平队列(能者多劳)
目前消息转发机制是平均分配,这样就会出现俩个消费者,奇数的任务很耗时,偶数的任何工作量很小,造成的原因就是近当消息到达队列进行转发消息。并不在乎有多少任务消费者并未传递一个应答给RabbitMQ。仅仅盲目转发所有的奇数给一个消费者,偶数给另一个消费者。
公平队列的原理
队列服务器向消费者发送消息的时候,消费者采用ACK手动应答模式,队列服务器必须要收到消费者发送ack结果,才会继续发送下一个消息。
生产者
public class Producer {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 1.获取连接
Connection newConnection = MQConnectionUtils.newConnection();
// 2.创建通道
Channel channel = newConnection.createChannel();
// 3.创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
for (int i = 1; i <= 50; i++) {
String msg = "test_yushengjun" + i;
System.out.println("生产者发送消息:" + msg);
// 4.发送消息
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}
channel.close();
newConnection.close();
}
}
消费者01
public class Customer1 {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("001");
// 1.获取连接
Connection newConnection = MQConnectionUtils.newConnection();
// 2.获取通道
final Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msgString);
try {
Thread.sleep(1000);
} catch (Exception e) {
} finally {
// 手动回执消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 3.监听队列
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
消费者02
public class Customer2 {
private static final String QUEUE_NAME = "test_queue";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("002");
// 1.获取连接
Connection newConnection = MQConnectionUtils.newConnection();
// 2.获取通道
final Channel channel = newConnection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.basicQos(1);// 保证一次只分发一次 限制发送给同一个消费者 不得超过一条消息
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String msgString = new String(body, "UTF-8");
System.out.println("消费者获取消息:" + msgString);
try {
Thread.sleep(500);
} catch (Exception e) {
} finally {
// 手动回执消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 3.监听队列
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
RabbitMQ交换机
传统简单队列工作原理:生产者生产消息直接投递给队列服务器,然后队列服务器以推送或者拉取的方式给到消费者进行消费。
RabbitMQ高级队列则不同,生产者先投递消息给交换机,交换机根据路由策略转发不同的队列服务器中存放,队列服务器在以推送或者拉取的方式给到消费者进行消费。
交换机的作用
根据具体的路由策略分发到不同的队列中(有点类似于Nginx)。
注意:交换机没有存储消息功能,如果消息发送到没有绑定消费队列的交换机,消息则丢失。
交换机类型
- Direct exchange(直连交换机)
根据消息携带的路由键(routing key)将消息投递给对应队列。
- Fanout exchange(扇型交换机)
将消息路由给绑定到它身上的所有队列。
- Topic exchange(主题交换机)
队列通过路由键绑定到交换机上,然后,交换机根据消息里的路由值,将消息路由给一个或多个绑定队列。
- Headers exchange(头交换机)
类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
发布订阅模式
一个生产者发送消息,多个消费者获取消息(同样的消息),包括一个生产者,一个交换机,多个队列,多个消费者。
大致思路:
- 一个生产者,多个消费者
- 每一个消费者都有自己的一个队列
- 生产者没有直接发消息到队列中,而是发送到交换机
- 每个消费者的队列都绑定到交换机上
- 消息通过交换机到达每个消费者的队列
场景以用户发短信发邮件为例,交换机类型采取Fanout Exchange(扇型交换机)。
生产者
1、pom文件引入
<dependencies>
<!-- springboot-web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
</dependencies>
2、配置文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_host
3、交换机绑定队列
@Component
public class FanoutConfig {
// 邮件队列
private String FANOUT_EMAIL_QUEUE = "fanout_eamil_queue";
// 短信队列
private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
// 交换机名称
private String EXCHANGE_NAME = "fanoutExchange";
// 1.定义队列邮件
@Bean
public Queue fanOutEamilQueue() {
return new Queue(FANOUT_EMAIL_QUEUE);
}
@Bean
public Queue fanOutSmsQueue() {
return new Queue(FANOUT_SMS_QUEUE);
}
// 2.定义交换机
@Bean
FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_NAME);
}
// 3.队列与交换机绑定邮件队列
@Bean
Binding bindingExchangeEamil(Queue fanOutEamilQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutEamilQueue).to(fanoutExchange);
}
// 4.队列与交换机绑定短信队列
@Bean
Binding bindingExchangeSms(Queue fanOutSmsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(fanOutSmsQueue).to(fanoutExchange);
}
}
4、生产者投递消息
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
String msg = "my_fanout_msg:" + new Date();
System.out.println(msg + ":" + msg);
amqpTemplate.convertAndSend(queueName, msg);
}
}
5、控制层代码
@RestController
public class ProducerController {
@Autowired
private FanoutProducer fanoutProducer;
@RequestMapping("/sendFanout")
public String sendFanout(String queueName) {
fanoutProducer.send(queueName);
return "success";
}
}
消费者
1、pom文件引入
<dependencies>
<!-- springboot-web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
</dependencies>
2、配置文件
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /admin_host
3、邮件消费者
@Component
@RabbitListener(queues = "fanout_eamil_queue")
public class FanoutEamilConsumer {
@RabbitHandler
public void process(String msg) throws Exception {
System.out.println("邮件消费者获取生产者消息msg:" + msg);
}
}
4、短信消费者
@Component
@RabbitListener(queues = "fanout_sms_queue")
public class FanoutSmsConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("短信消费者获取生产者消息msg:" + msg);
}
}
路由模式
生产者发送消息到交换机并指定一个路由key,消费者队列绑定到交换机时要制定路由key(key匹配就能接受消息,key不匹配就不能接受消息)。
我们可以把路由key设置为insert ,那么消费者队列key指定包含insert才可以接收消息,消费者队列key定义为update或者delete就不能接收消息。很好的控制了更新,插入和删除的操作。
交换机采用direct模式。
生产者
与发布订阅唯一的区别就是交换机绑定队列和启动项的环节。
@Component
public class DirectConfig {
// 邮件队列
private String DIRECT_EMAIL_QUEUE = "direct_eamil_queue";
// 短信队列
private String DIRECT_SMS_QUEUE = "direct_sms_queue";
// 队列名称
private String EXCHANGE_NAME = "directExchange";
// 1.定义队列邮件
@Bean
public Queue directEamilQueue() {
return new Queue(DIRECT_EMAIL_QUEUE);
}
@Bean
public Queue directSmsQueue() {
return new Queue(DIRECT_SMS_QUEUE);
}
// 2.定义交换机
@Bean
DirectExchange directExchange() {
return new DirectExchange(EXCHANGE_NAME);
}
// 3.队列与交换机绑定邮件队列
@Bean
Binding bindingExchangeEamil(Queue directEamilQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directEamilQueue).to(directExchange).with("myKey");
}
// 4.队列与交换机绑定短信队列
@Bean
Binding bindingExchangeSms(Queue directSmsQueue, DirectExchange directExchange) {
return BindingBuilder.bind(directSmsQueue).to(directExchange).with("myKey");
}
}
启动项
@Component
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
public void send(String queueName) {
String msg = "my_direct_msg:" + new Date();
String routingKey = "myKey";
System.out.println(msg + ":" + msg);
amqpTemplate.convertAndSend(queueName,routingKey,msg);
}
}
通配符模式
此模式实在路由key模式的基础上,使用了通配符来管理消费者接收消息。生产者P发送消息到交换机X,type=topic,交换机根据绑定队列的routing key的值进行通配符匹配。
- 符号#:匹配一个或者多个词lazy.# 可以匹配lazy.irs或者lazy.irs.cor
- 符号*:只能匹配一个词lazy.* 可以匹配lazy.irs或者lazy.cor
跟路由模式很类似,就不过多介绍了。
死信队列
在定义业务队列的时候,可以考虑指定一个死信交换机,并绑定一个死信队列,当消息变成死信时,该消息就会被发送到该死信队列上,这样就方便我们查看消息失败的原因了。
RabbitMQ解决消息幂等性问题
产生原因
网络延迟传输中,消费出现异常或者是消费延迟消费,会造成MQ进行重试补偿,在重试过程中,可能会造成重复消费。
解决办法
①使用全局MessageID判断消费方是否使用同一个,解决幂等性。
②或者使用业务逻辑保证唯一(比如订单号码)。
RabbitMQ解决分布式事务问题
情景如下:
以目前流行点外卖的案例,用户下单后,调用订单服务,让后订单服务调用派单系统通知送外卖人员送单,这时候订单系统与派单系统采用MQ异步通讯。
RabbitMQ解决分布式事务原理: 采用最终一致性原理。
需要保证以下三要素:
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)。
2、MQ消费者消息能够正确消费消息,采用手动ACK模式,使用补偿机制(注意重试幂等性问题)。
3、如何保证第一个事务先执行,采用补单机制,在创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。
需要注意如下几种情况:
1.如果生产者投递消息到MQ服务成功,但是消费者消费失败了,生产者不需要回滚事务。
解决方法:消费者采用ACK应答方式,采用MQ补偿重试机制。补偿的过程中,注意幂等性问题。
2.如何确认生产者一定要将数据投递到MQ服务器中?
采用confirm机制(确认应答机制)
3.如果生产者发送消息到MQ服务器端失败
解决办法:使用生产者重试机制,进行发消息
4.如何保证一个事务先执行,生产者投递消息到MQ服务器端成功,消费者消费消息成功,但是订单事务回滚了。
解决办法:通过补单机制