- 简单消息队列
- 模型
官方图例中,P代表消息生产者,中间代表消息队列,C代表消息的消费者。
- 获取RabbitMQ链接
/**
* 获取MQ连接
*/
public static Connection getConnection() throws IOException, TimeoutException {
//定义一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("*.*.*.*");
// 协议:端口号 AMQP:5672
factory.setPort(5672);
//设置数据库
factory.setVirtualHost("/vhost_haoyuehong");
//用户名
factory.setUsername("haoyuehong");
//密码
factory.setPassword("123456");
factory.setConnectionTimeout(30*1000);//默认60秒
return factory.newConnection();
}
- 发送一个消息
public class Send {
private static final String QUEUE_NAME = "test_simple_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//获取一个连接
Connection connection = ConnectionUtils.getConnection();
//从连接中获取吧通道
Channel channel = connection.createChannel();
//创建队列声明(注册)
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//要发送的消息
String msg = "hello rabiitmq";
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
System.out.println("-->send:"+msg);
channel.close();
connection.close();
}
}
运行以上程序,在RabbitMQ web控制面板QUEUE选项卡中可以看到如下:
说明消息发送成功。
- 接受(消费)消息
/**
* 消息的消费者
*/
public class Recv {
private static final String QUEUE_NAME = "test_simple_queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//获取一个连接
Connection connection = ConnectionUtils.getConnection();
//从连接中获取通道
Channel channel = connection.createChannel();
//定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
while (true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String s = new String(delivery.getBody());
System.out.println("<----recv:"+s);
}
}
}
消息数量变为0,说明已被消费。
以上方法已过世,修改如下:
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//获取一个连接
Connection connection = ConnectionUtils.getConnection();
//从连接中获取通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//事件机制 有消息时触发
DefaultConsumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("<------new API recv:" + msg);
}
};
//监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
- 总结:简单消息队列耦合性高,生产者与消费者一一对应,不能让多个消费者同时消费,不能满足分布式场景,且当消费者的对列名变更时,生产者的对列名也要相应变更。
- Work Queues工作队列
- 模型
- 轮训分发(Round-Robin dispatch)
我们假设有一个生产者和两个消费者(生产者和消费者的代码和Simple queue一样,只是多了一个消费者),生产者每0.5秒发送一条消息(共发送50条),消费者1每2秒处理一条消息,消费者2每1秒处理一条消息,测试结果发现:消费者1处理的数据数量和消费者2消费的消息数量相同,且消费者1处理奇数条(下角标为奇数)的数据,消费者2处理偶数条(下角标为偶数)的消息,这种方式叫做轮训分发(Round-Robin)
2.公平分发(fair dispatch)
消费者每次处理完消息后手动反馈给消息队列,消息队列分发下一条消息给消费者。
代码如下:
- 生产者:
public class Send {
private static final String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//每个消费者在消息确认之前,消息队列不发送消息到消费者,一次只处理一个消息
int prefetchCount = 1;
channel.basicQos(prefetchCount);
//发送50条消息
for(int i=0;i<50;i++){
String msg = "hello work queue :"+i;
channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
Thread.sleep(i*20);
}
channel.close();
connection.close();
}
}
- 消费者1:
public class Recv1 {
private static final String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection connection = ConnectionUtils.getConnection();
//获取通道
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println("Recv1 <------收到消息:"+s);
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
//监听队列
boolean autoAck = false; //自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
}
}
- 消费者2:
public class Recv2 {
private static final String QUEUE_NAME = "test_work_queue";
public static void main(String[] args) throws IOException, TimeoutException {
//获取连接
Connection connection = ConnectionUtils.getConnection();
//获取通道
final Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println("Recv1 <------收到消息:"+s);
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
//监听队列
boolean autoAck = false; //自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
}
}
- 轮训分发与公平分发的区别:
- 每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只处理一个消息。限制发送给同一个消费者不得超过1条消息;
int prefetchCount = 1;
channel.basicQos(prefetchCount);
- 消费者增加了channel.basicQos(1)控制同一消息消费次数
channel.basicQos(1);
- 将轮询分发时自动应答修改为手动应答
channel.basicAck(envelope.getDeliveryTag(),false);
………………
boolean autoAck = false; //自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
- 消息应答
boolean autoAck = false; //自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
autoAck为自动应答,是一个布尔值,
- boolean autoAck = true; 自动确认模式
一旦RabbitMQ将消息分发给消费者,就会将消息从内存中删除,这种情况下,如果消息处理过程中发生异常,消息将会丢失,
2. boolean autoAck = false; 手动确认模式
如果有一个消费者挂掉,消息队列将会将该消息交付给其他消费者,消费者发送消息应答后RabbitMQ将内存中的消息删除
消息应答默认为false,假设一种极端情况,RabbitMQ发生故障宕机了,因为消息存在内存中,RabbitMQ挂掉后,消息也会丢失,这就需要将数据持久化
- 消息持久化
boolean durable = false; //持久化
channel.queueDeclare(QUEUE_NAME, durable,false,false,null);
注意:声明好的队列不能修改durable,因为已经将队列声明好,RabbitMQ不准许重新定义一个已存在的对列
- 订阅模式(Publish/Subscribe)
- 模型
X---交换机
-
- 一个生产者多个消费者
- 每个消费者都有自己的对列
- 生产者没有直接把消息发送到对列,而是发送到交换机(转发器 exchange)
- 每个队列都要绑定到交换机上
- 生产者发送的消息经过交换机到达对列,就能实现一个消息被多个消费者消费
生产者:
public class Send {
private static final String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout"); //1 交换机名字 2 交换机类型
//发送消息
String msg = "hello ps";
channel.basicPublish(EXCHANGE_NAME,"",null,msg.getBytes());
System.out.println("send msg ----->"+msg);
channel.close();
connection.close();
}
}
运行以上程序,查看控制台:
此时,消息已经丢失,因为交换机没有存储能力,RabbitMQ里面只有对列有存储能力,由于此时没有对列绑定,所以消息丢失了
消费者1:
public class Recv1 {
private static final String QUEUE_NAME = "test_queue_fanout_email";
private static final String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
final Channel channel = connection.createChannel();
//对列声明
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定对列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println("Recv1 <------收到消息:"+s);
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
//监听队列
boolean autoAck = false; //自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
}
}
消费者2:
public class Recv1 {
private static final String QUEUE_NAME = "test_queue_fanout_email";
private static final String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
final Channel channel = connection.createChannel();
//对列声明
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定对列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println("Recv2 <------收到消息:"+s);
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("[2] done");
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
//监听队列
boolean autoAck = false; //自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
}
}
- Exchange(交换机/转发器)
- 接受生产者消息
- 向对列推送消息
- Exchange类型
交换器的类型,常见的有fanout、direct、topic、headers这四种
- Fanout Exchange : 不处理任何的路由键,它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中,如上代码便是Fanout Exchange。
1.1 模式
2. Direct Exchange : 直连的方式 把消息路由到那些 BindingKey RoutingKey 完全匹配的队列中
2.1 模式
生产者:
public class Send {
private static final String EXCHANGE_NAME = "test_exchange_rounting";
private static final String ROUNTINGKEY = "key";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT.getType());
String msg = "hello rounting";
channel.basicPublish(EXCHANGE_NAME,ROUNTINGKEY,null,msg.getBytes());//ROUNTINGKEY声明了路由键 相同的路由键能收到消息
System.out.println("send msg ----->"+msg);
channel.close();
connection.close();
}
}
消费者:
public class Recv1 {
private static final String QUEUE_NAME = "test_queue_rounting_email";
private static final String EXCHANGE_NAME = "test_exchange_rounting";
private static final String ROUNTINGKEY = "key";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,ROUNTINGKEY); //与生产者声明的路由键相同
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String s = new String(body, "UTF-8");
System.out.println("Recv1 <------收到消息:"+s);
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("[1] done");
channel.basicAck(envelope.getDeliveryTag(),false);
}
}
};
//监听队列
boolean autoAck = false; //自动应答
channel.basicConsume(QUEUE_NAME,autoAck,consumer);
}
}
- Topic Exchange : 交换器在匹配规则上进行了扩展,它有一定的约束条件:
- RoutingKey 为一个点号" . “分隔的字符串(被点号”.“分隔开的每段独立的字符串称为一个单词) 如:com.toher.user 和 org.toher.product
- BindingKey和RoutingKey同样也是点号”."分隔的字符串;
- BindingKey 中可以存在两种特殊字符串 * 和 #,用于做模糊匹配,其中 * 代表一个单词,# 可以用于匹配多规格单词(可以是零个)
- 模式