目录
一、MQ相关概念
MQ(message queue),本质是一个队列FIFO先进先出;
应用场景:12306购票、淘宝秒杀的时候、嘀嘀打车等;
1.1 使用MQ的作用:
1、流量削峰;
原来需要再很短的时间内去请求数据库,使用队列之后把请求的时间拉长;
2、解耦:
减少A系统和其他系统之间的直接联系,其他系统需要消息,直接去队列中按照相应规则取数据,而不用取更改A系统中的代码;减少和A系统之间的关联;
3、异步处理
同步:当我在处理的时候其他进程不能进来;
异步:当我在处理的时候,其他进程可以进入,提高了响应的速度;
1.2 MQ选择:
- 大量数据的互联网服务的数据收集业务,如果有日志采集功能,首选Kafka;
- 0消息丢失,天生就是为金融互联网领域而生,适用于高并发场景;
- 数据量没有那么大使用RabbitMQ,并且RabbitMQ管理起来非常方便,可靠性高;
RabbitMQ:
1、支持多语言、多平台;
2、采用erlang语言编写,天生具有高并发的优点;
3、微秒级的时效性;
Channel:作为轻量级的Connection极大减少了操作系统建立TCP connection的开销;
二、Hello World实战
2.1 Demo
生产者
package whut.zyf.test01;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
//生产者
public class Producer {
private static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
//1、首先要创建一个连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.44.101");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
//2、得到一个channel 为了减少tcp连接的开销
Channel channel = connection.createChannel();
//3、声明一个队列
/**
* 第一个参数:队列的名称
* 第二个参数:队列是否持久化
* 第三个参数:是否只让一个消费者独自消费(排它)
* 第四个参数:消费者断开连接之后,是否自动删除该队列
* 第五个参数:代表的是其他参数(死信队列)
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//4、发送消息
/**
* 第一个参数:交换机的名称 “ ”空字符串代表默认的交换机
* 第二个参数:路由的key 也就是发送给哪个队列
* 默认采用队列名称作为路由key,发送的消息就会被路由到对应的队列中
* 第三个参数:其他配置参数 (比如设置消息的过期时间)
* 第四个参数:发送消息内容
*/
String message = "hello word";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完成");
channel.close();
connection.close();
}
}
第二种写法,可以不用使用close,因为Connection和channel都继承了AutoCloseable
package whut.zyf.test01; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; //生产者 public class Producer02 { private static final String QUEUE_NAME="hello"; public static void main(String[] args) throws Exception { //1、首先要创建一个连接 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.44.101"); factory.setUsername("admin"); factory.setPassword("123"); //Connection和channel都继承了AutoCloseable接口,说明这种接口就可以不用显示的调用close方法 try(Connection connection = factory.newConnection();Channel channel = connection.createChannel();) { channel.queueDeclare(QUEUE_NAME,false,false,false,null); String message = "hello word"; channel.basicPublish("",QUEUE_NAME,null,message.getBytes()); System.out.println("消息发送完成"); } } }
消费者
package whut.zyf.test01;
import com.rabbitmq.client.*;
//消费者
public class Consumer {
private static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
//1、消费者要想获取队列中的连接,那么首先也要先创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.44.101");
factory.setUsername("admin");
factory.setPassword("123");
Connection connection = factory.newConnection();
//2、创建channel;
Channel channel = connection.createChannel();
System.out.println("消费者启动等到消费");
//消费者需要一直消费消息,所以不需要断开连接
/**
* 队列推送消息给消费者的时候,如何消费消息
* consumerTag:消费者的标记
* delivery:传递给消费者的传递内容;
*/
DeliverCallback deliverCallback = (consumerTag,delivery) -> {
//消费者的信息,也就是是谁消费的
System.out.println(consumerTag);
String receivedMessage = new String(delivery.getBody());
System.out.println("消费者接收到的消息:" + receivedMessage);
System.out.println("发送的第几个消息:"+delivery.getEnvelope().getDeliveryTag());
};
/**
* 消费者取消消费的回调接口
*/
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
/**
* 消费者消费队列里面的消息
* 第一个参数:队列名称
* 第二个参数:是否采用自动应答(也就是相当于收快递的时候,是自动签收还是手动签收)
* 第三个参数:队列推送消息给消费者的时候,如何消费消息;
* 第四个参数:消费者取消消费消息的时候,回调接口;
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
2.2 消息应答
防止消费者在消费消息的时候,消息丢失
手动应答:可以防止消息的丢失
DeliverCallback deliverCallback = (var1, var2) -> {
String receivedMessage = new String(var2.getBody());
System.out.println("C2消费者接收到的消息" + receivedMessage);
// channel.basicAck();
// channel.basicNack();
// channel.basicReject();
};
如果将自动应答设置为false,可以再deliverCallback中,使用channel的三个方法,传入相应的参数,采取不同的应答模式,和消息拒绝策略;
2.3 消息重新入队
当C1在处理消息的时候,还没有处理完,断开了连接,这个时候消息应该如何处理?
该消息会重新入队,然后发送给当前活跃的存活的消费者;
2.4 RabbitMQ持久化
持久化是防止,生产者在生产消息的时候,消息不丢失
队列持久化
在创建队列的时候进行指定:
boolean durable = true;
channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
保证队列不丢失, 再对队列使用持久化之后,不可以再将其变为非持久化,除非删掉重新创建;
消息持久化
在发送消息的时候进行指定:MessageProperties.PERSTENT_TEXT_PLAIN;
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
如果传了一个很大的数据,比如2G,如果在持久化的过程中RabbitMQ宕机了怎么办?
可以使用更强力的持久化策略;
2.5 发布确认(重点)
发布确认可以解决数据在持久化过程中遇到RabbitMQ宕机的情况;
单个发布确认
缺点:吞吐量低;
//单个消息发布确认
public static void publishMessageIndividual() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认,目的就是为了发送消息更加安全
channel.confirmSelect();
long begin = System.currentTimeMillis();
for (int i = 0;i < MESSAGE_COUNT;i++){
String message = "消息";
channel.basicPublish("",queueName,null,message.getBytes());
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("broker已经收到消息");
}
}
long end = System.currentTimeMillis();
System.out.println(end - begin);
}
批量发布确认
批量操作缺点:因为不知道是这一批中的哪一个消息没有确认,所以需要对这一批全部进行重新发送,这样就会导致重复消费;
//批量消息发布确认
public static void publishMessageBatch() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认,目的就是为了发送消息更加安全
channel.confirmSelect();
//要做批量处理,首先得知道批次大小是多少
//定义一个多少个消息作为批量的值;
int batchSize = 50;
//定义一个当前已经发送多少个未确认的消息
int outstandingMessageCount=0;
long begin = System.currentTimeMillis();
for (int i = 0;i < MESSAGE_COUNT;i++){
String message = "消息" + i;
channel.basicPublish("",queueName,null,message.getBytes());
outstandingMessageCount++;
if (batchSize == outstandingMessageCount){
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("broker已经收到消息");
outstandingMessageCount=0;
}
}
}
//对于无法整除的情况,比如120个消息,还剩余的20个就通过下面的代码进行处理
if (outstandingMessageCount > 0){
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("broker已经收到消息");
}
}
long end = System.currentTimeMillis();
System.out.println(end - begin);
}
异步发布确认
逻辑图
//异步发布消息确认
public static void publishMessageAsync() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
//开启发布确认,目的就是为了发送消息更加安全
channel.confirmSelect();
//线程安全的一个map:
// ConcurrentSkipListMap:可以将序号和消息进行关联:
// 只需给定序号,就会把《=当前序号的值作为一个map,提取出来;
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
//异步发布消息确认,需要一个监听器
/**
* 1、sequenceNumber 当前消息序号;
* 2、multiple 处理一个还是多个消息;
* true:多个消息
* false:单个消息
*/
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
//判断是批处理韩式单个处理
if (multiple){
//这个时候吧小于sequenceNumber的消息都从hashmap中删除
//获取序号小于等于sequenceNumber的数据
ConcurrentNavigableMap<Long, String> headMap = outstandingConfirms.headMap(sequenceNumber);
//删除
headMap.clear();
}else {
//只签收当前sequenceNumber的消息
outstandingConfirms.remove(sequenceNumber);
}
};
//如果说没有收到消息
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
if (multiple){
String message = outstandingConfirms.get(sequenceNumber);
System.out.println(sequenceNumber + "需要重新发送");
}else {
System.out.println(sequenceNumber + "需要重新发送");
}
};
//监听器:收到消息或者没有收到消息回调
channel.addConfirmListener(ackCallback,nackCallback);
long begin = System.currentTimeMillis();
for (int i = 0;i < MESSAGE_COUNT;i++){
String message = "消息";
//将消息添加到hashmap中
//channel.getNextPublishSeqNo():获取序号从1开始
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
channel.basicPublish("",queueName,null,message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("异步处理时间 =" + (end - begin));
}
四、交换机
临时队列:一旦消费者断开连接,队列就会被自动删除;
绑定(binding):让交换机和队列之间简历连接
默认交换机
4.1 Fanout:扇出(广播)
实战
生产者:
package whut.zyf.test04;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import whut.zyf.utils.RabbitMqUtils;
import java.util.Scanner;
public class Task04 {
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机
/**
* 第一个是交换机的名称;
* 第二个是交换机的类型;
*/
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
System.out.println("等待输入消息:..........................");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.nextLine();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
System.out.println("消息发送完成:" + message);
}
}
}
消费者_01
package whut.zyf.test04;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConsumerShutdownSignalCallback;
import com.rabbitmq.client.DeliverCallback;
import whut.zyf.utils.RabbitMqUtils;
public class ReceiveLog01 {
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("消费者等待消费消息");
//创建临时队列
/**
* 临时队列:名字随机,消费者只要和队列断开连接,就会自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//吧队列和交换机进行绑定
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("ReceiveLogs01消费者,把接收到的消息打印在控制台...................");
DeliverCallback deliverCallback = (consumerTag,delivery) -> {
String receivedMessage = new String(delivery.getBody());
System.out.println("ReceiveLogs01消费者接收到的消息:" + receivedMessage);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
消费者_02
package whut.zyf.test04;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import org.apache.commons.io.FileUtils;
import whut.zyf.utils.RabbitMqUtils;
import java.io.File;
public class ReceiveLog02 {
private static String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("消费者等待消费消息");
//创建临时队列
/**
* 临时队列:名字随机,消费者只要和队列断开连接,就会自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//吧队列和交换机进行绑定
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("ReceiveLogs02消费者,把接收到的消息存储在磁盘...................");
DeliverCallback deliverCallback = (consumerTag,delivery) -> {
String receivedMessage = new String(delivery.getBody());
File file = new File("F:\\JavaProjects\\RabbitMQ\\log.txt");
FileUtils.writeStringToFile(file,receivedMessage,"UTF-8",true);
System.out.println("消息已经写入磁盘");
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
4.2 Direct交换机
Direct只关注他绑定的某一类型或某一些消息,比如我只想让错误日志进入某个队列中进行处理;
实战
实战:
生产者
package whut.zyf.test05;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import whut.zyf.utils.RabbitMqUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
public class EmitLogDirect {
private static String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机
/**
* 第一个是交换机的名称;
* 第二个是交换机的类型;
*/
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//创建多个bindingKey
Map<String,String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("info","普通info消息");
bindingKeyMap.put("warning","警告warning信息");
bindingKeyMap.put("error","错误error消息");
bindingKeyMap.put("debug","调式debug消息");
for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
String routingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes());
System.out.println("消息发送完成" + message);
}
}
}
消费者_01
package whut.zyf.test05;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import whut.zyf.utils.RabbitMqUtils;
public class ReceiveLog01 {
private static String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("消费者等待消费消息");
String queueName = "console";
channel.queueDeclare(queueName,false,false,false,null);
//把队列和交换机进行绑定
channel.queueBind(queueName,EXCHANGE_NAME,"info");
channel.queueBind(queueName,EXCHANGE_NAME,"warning");
System.out.println("ReceiveLogs01消费者,把接收到的消息打印在控制台...................");
DeliverCallback deliverCallback = (consumerTag,delivery) -> {
String receivedMessage = new String(delivery.getBody());
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println("ReceiveLogs01消费者接收到的消息:" + receivedMessage);
System.out.println("C1接收路由" + routingKey+ "........消息.........");
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
消费者_02
package whut.zyf.test05;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import org.apache.commons.io.FileUtils;
import whut.zyf.utils.RabbitMqUtils;
import java.io.File;
public class ReceiveLog02 {
private static String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("消费者等待消费消息");
//创建临时队列
/**
* 临时队列:名字随机,消费者只要和队列断开连接,就会自动删除
*/
String queueName = "disk";
channel.queueDeclare(queueName,false,false,false,null);
//吧队列和交换机进行绑定
channel.queueBind(queueName,EXCHANGE_NAME,"error");
System.out.println("ReceiveLogs02消费者,把接收到的消息存储在磁盘...................");
DeliverCallback deliverCallback = (consumerTag,delivery) -> {
String receivedMessage = new String(delivery.getBody());
File file = new File("F:\\JavaProjects\\RabbitMQ\\direct_log.txt");
FileUtils.writeStringToFile(file,receivedMessage,"UTF-8",true);
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println("C2接收路由" + routingKey+ ".....消息............");
System.out.println("消息已经写入磁盘");
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消费者取消消费消息");
};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
}
}
5、Topic
和前面原理一样,支持通配符方式进行绑定,更加细致;
* :代表匹配一个单词;
# :0-多个单词
三、死信队列
由于某种原因导致队列中的消息无法被消费者消费——死信
装有死信消息的队列称为死信队列;
死信的来源:
- 消息TTL过期
- 队列达到最大长度
- 消息被拒绝
实战逻辑图
3.1 消息TTL过期
生产者
package whut.zyf.test07;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import whut.zyf.utils.RabbitMqUtils;
public class Producer {
private static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
System.out.println("生产者等待发送消息");
String message = "info";
//给消息一个TTL时间,时间到期之后还没有被消费就进入死信;
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("5000").build();
channel.basicPublish(NORMAL_EXCHANGE_NAME,"zhangsan",properties,message.getBytes());
System.out.println("消息发送完成");
}
}
消费者
package whut.zyf.test07;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import whut.zyf.utils.RabbitMqUtils;
import java.util.HashMap;
import java.util.Map;
public class Consumer01 {
private static final String NORMAL_QUEUE_NAME = "normal_queue";
private static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
private static final String DEAD_QUEUE_NAME = "dead_queue";
private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//死信队列
channel.queueDeclare(DEAD_QUEUE_NAME,false,false,false,null);
//死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//死信队列和死信交换机之间的绑定关系
channel.queueBind(DEAD_QUEUE_NAME,DEAD_EXCHANGE_NAME,"lisi");
//正常队列和死信交换机的绑定关系
//将deadLetterParams传到正常队列中,就完成了正常队列和死信交换机之间的绑定
Map<String,Object> deadLetterParams = new HashMap<>();
deadLetterParams.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
deadLetterParams.put("x-dead-letter-routing-key","lisi");
//声明一个正常队列
channel.queueDeclare(NORMAL_QUEUE_NAME,false,false,false,deadLetterParams);
//正常交换机
channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//正常队列和交换机绑定
channel.queueBind(NORMAL_QUEUE_NAME,NORMAL_EXCHANGE_NAME,"zhangsan");
DeliverCallback deliverCallback = (consumerTag,delivery) -> {
String message = new String(delivery.getBody());
System.out.println("消费者接收到的消息:" + message);
};
channel.basicConsume(NORMAL_QUEUE_NAME,true,deliverCallback,(consumerTag) -> {
System.out.println(consumerTag+"消费者取消消费消息");
});
}
}
3.2 队列长度限制
//将deadLetterParams传到正常队列中,就完成了正常队列和死信交换机之间的绑定
Map<String,Object> deadLetterParams = new HashMap<>();
deadLetterParams.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME);
deadLetterParams.put("x-dead-letter-routing-key","lisi");
//给正常队列添加长度属性
deadLetterParams.put("x-max-length",6); //正常队列的最大容纳长度
3.3 消息拒绝
采用手动应答,之后拒绝重新入队,消息就会进入死信队列
四、延迟队列
场景:
1、订单在十分钟之内未支付则自动取消;
2、新创建的店铺,如果在十天内没有上传过商品,则自动发送消息提醒;
3、用户注册成功后,如果三天内没有登陆则进行短信提醒;
4、用户发起退款,如果三天内没有得到处理则通知相关运营人员;
5、预定会议后,需要在预定的时间内通知各个与会人员参与会议;
队列TTL和消息TTL
消息是否过期需要到消费者那里去进行判断;
队列过期之后,不需要消费者进行判断,直接进入死信队列;