目录
rabbitMQ提供一下几种工作模式:
- 简单队列
- 工作队列
- 发布订阅
- 路由
- 话题
- RPC
- 发布者确认
1.工具类抽取
为了方便代码能直接使用,先将代码中用到的公告部分抽取取来。这个工具栏的作用很简单,就是回去一个Connection,代码如下:
public class ConnectionUtil {
public static Connection getConnection() throws Exception {
// 定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置服务地址
factory.setHost("localhost");
// 端口
factory.setPort(5672);
// 设置账号信息,用户名、密码、vhost
factory.setVirtualHost("testhost");
factory.setUsername("admin");
factory.setPassword("admin");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
2.简单队列
简单队列,就如名字一样,简单到这个队列,只有一个生产者,只对应一个消费者了。
2.1消息生产者
public boolean sendMessage(String queueName, String message) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明(创建)队列
channel.queueDeclare(queueName, false, false, false, null);
// 消息内容
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
// 关闭通道和连接
channel.close();
connection.close();
return false;
}
声明队列的时候,有四个参数,应为后面经常出现,现在提前说明一下这四个参数的含义:
channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments);
- queue:我们使用的队列名称
- durable:是否将消息持久化;假如我们在创建queue的时候没有指定其持久化,后期不可以在代码中修改其为持久化队列,只能删除重建。rabbitmq不允许重新定义(不同参数)一个已存在的队列
- exclusive:是否声明为独占队列
- autoDelete:消息投递到消费者后,server上是否自动删除消息
- arguments:队列的配置信息
2.2消息消费者
消费者消费信息的时候,对服务器有两种应答方式:自动应答,手动应答
自动应答:当队列服务器将消息投递给消费者后,不管消费者是否成功处理,都自行将队列删除。
手动应答:当队列服务器将消息投递给消费者后,需要消费者确认是否在服务器上删除该消息。
2.2.1客户端自动应答
/**
* 客户端自动应答消费消息
*/
public void reciveByAutoMode(String queueName) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, false, false, false, null);
// 定义消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
}
};
// 监听队列
channel.basicConsume(queueName, true, consumer);
}
代码中监听队列的第二个参数设置为true,表示自动应答。
basicConsume(String queue, boolean autoAck, Consumer callback)
2.2.2客户端手动应答
/**
* 客户端手动应答
*/
public void reciveByHandleMode(String queueName) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, false, false, false, null);
// 定义消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序确认,而不是rabbit的自动确认(手动回执)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,改成false则表示手动应答
channel.basicConsume(queueName, false, consumer);
}
手动应答,首先需要将监听队列的第二个参数设置为false,
basicConsume(String queue, boolean autoAck, Consumer callback)
同时,在consumer中,需要手动发送一个确认信息。
channel.basicAck(envelope.getDeliveryTag(), false);
3.工作队列
消息生产者可以不断的向服务器发送消息,但是消费者不一定能够及时或者快速处理掉消息,这就会导致消息的积压。
工作队列的出现,在某种程度上降低了消息积压发生的可能性。
工作队列,通过多个消费者消费同一个队列的数据,加快了消息的处理速度。
同时,队列提供了两种消息消费类型:公平消费,轮训消费
公平消费:服务器根据消费者的能力来投递消息,在消费者没有完成对消息的处理时,不会再次发送一条消息至消费者。
轮训消费:服务器将消息轮训投递给消费者,如果消费者有一个处理能力较差,会导致最后的压力都集中在能力差的机器上。
生产中,我们毫无疑问的提倡公平分发消费。
3.1消息生产者
public boolean sendMessage2WorkQueue(String queueName, List<String> messages) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明(创建)队列
channel.queueDeclare(queueName, false, false, false, null);
for (String message : messages) {
//发送消息
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
// 此行代码做公平分发用
// 每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只处理一个消息
// 限制发送给同一个消费者不得超过一条消息
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 关闭通道和连接
channel.close();
return true;
}
3.2消息消费者
/**
* 客户端手动应答
* 当采用公平分发的时候,消费端要关闭自动应答,改为手动应答
*
* @param queueName
* @throws Exception
*/
public void reciveByHandleMode(String queueName) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
final Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, false, false, false, null);
// 定义消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序确认,而不是rabbit的自动确认(手动回执)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,改成false则表示手动应答
channel.basicConsume(queueName, false, consumer);
}
4.发布订阅
从这开始接触一个新的概念:交换机
交换机相当于一个路由,对生产者发来的消息,根据配置进行路由,其本身并不存储消息。在rabbitmq中,只有队列能存储消息。
路由的方式有三种:fanout(对应发布订阅模式),direct(对应路由模式),topic(对应主题模式)
- fanout:发布订阅模式,顾名思义,像扇子一样(扇出),对绑定到该交换机的所有队列,都能收到消息。此时routingKey不生效。
- direct:路由模式,对绑定到该交换机的队列,通过routingKey匹配,只有完全匹配,交换机才会将消息路由到该队列。
- topic:主题模式,将路由和某个模式进行匹配(规则匹配, #:匹配一个或者多个; *:匹配一个)。匹配上的队列,交换机才会路由消息至该队列
1.一个生产者,多个消费者 2. 每一个消费者都有自己的队列 </br> 3. 生产者没有直接把消息发送到队列,而是发到了交换机(转发器 exchange) 4. 每个队列都要绑定到交换机上 5. 生产者发送的消息,经过交换机,到达队列,就能实现一个消息被多个消费者消费
4.1消息生产者
public void sendMessage(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机(交换机没有存储的能力,在rabbitmq中只有队列才有存储能力)
// fanout:不处理路由键,会将消息转发给所有绑定该交换机的队列,因此将routingKey设置为""
// direct:处理路由键,将消息转发给指定了routingKey的队列上
channel.exchangeDeclare(exchange, "fanout");
// 发送消息
channel.basicPublish(exchange, "", null, message.getBytes());
// 关闭通道
channel.close();
connection.close();
}
4.2消息消费者
public void reciveByHandleMode(String exchange, String queueName, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, false, false, false, null);
// 绑定到交换机
channel.queueBind(queueName, exchange, "");
// 可绑定多个
// channel.queueBind(queueName, exchange, routingKey);
// channel.queueBind(queueName, exchange, routingKey);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 定义消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序确认,而不是rabbit的自动确认(手动回执)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,改成false则表示手动应答
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, consumer);
}
5.路由
5.1消息生产者
public void sendMessage(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机(交换机没有存储的能力,在rabbitmq中只有队列才有存储能力)
// fanout:不处理路由键,会将消息转发给所有绑定该交换机的队列,因此将routingKey设置为""
// direct:处理路由键,将消息转发给指定了routingKey的队列上
channel.exchangeDeclare(exchange, "direct");
// 发送消息
channel.basicPublish(exchange, routingKey, null, message.getBytes());
// 关闭通道
channel.close();
connection.close();
}
5.2消息消费者
/**
* 将队列绑定到交换机
*
* @param exchange
* @param queueName
* @throws Exception
*/
public void reciveByHandleMode(String exchange, String queueName, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, false, false, false, null);
// 绑定到交换机
channel.queueBind(queueName, exchange, routingKey);
// 可绑定多个
// channel.queueBind(queueName, exchange, routingKey);
// channel.queueBind(queueName, exchange, routingKey);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 定义消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序确认,而不是rabbit的自动确认(手动回执)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,改成false则表示手动应答
channel.basicConsume(queueName, false, consumer);
}
6.话题
6.1消息生产者
public void sendMessage(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机(交换机没有存储的能力,在rabbitmq中只有队列才有存储能力)
// fanout:不处理路由键,会将消息转发给所有绑定该交换机的队列,因此将routingKey设置为""
// direct:处理路由键,将消息转发给指定了routingKey的队列上
//topic:将路由和某个模式进行匹配(规则匹配)。 #:匹配一个或者多个; *:匹配一个
channel.exchangeDeclare(exchange, "topic");
// 发送消息routingKey,such as :goods.add
channel.basicPublish(exchange, routingKey, null, message.getBytes());
// 关闭通道
channel.close();
}
6.2消息消费者
/**
* 将队列绑定到交换机
*
* @param exchange
* @param queueName
* @throws Exception
*/
public void reciveByHandleMode(String exchange, String queueName, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(queueName, false, false, false, null);
// 绑定到交换机,发送消息routingKey,such as :goods.#
channel.queueBind(queueName, exchange, routingKey);
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 定义消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
String message = new String(body, "UTF-8");
System.out.println("receiveMessage:" + message);
// 程序确认,而不是rabbit的自动确认(手动回执)
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,改成false则表示手动应答
boolean autoAck = false;
channel.basicConsume(queueName, autoAck, consumer);
}
7.RPC
8.发布者确认
生产者将消息发送至broker以后,生产者并不知道消息是否真的正确达到了目标队列,rabbitMQ当然不会这么傻,它提供了两种方式,来告知生产者,消息是否正确投递出去了。他们分别是:事务机制,确认机制。
8.1事务机制
投递前开启事务,投递结束后提交事务,若发送异常,则回滚事务。
public void createTransactionProducer(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机(交换机没有存储的能力,在rabbitmq中只有队列才有存储能力)
// fanout:不处理路由键
// direct:处理路由键
channel.exchangeDeclare(exchange, "topic");
try {
// 发送消息routingKey,such as :goods.add
channel.txSelect();
channel.basicPublish(exchange, routingKey, null, message.getBytes());
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
} finally {
// 关闭通道
channel.close();
}
}
8.2confirm机制
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上发布的消息都会被指派一个唯一的ID(默认从1开始), 一旦消息被投递到所匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确的到达目标队列了。 如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号。 此外,broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
8.2.1同步确认机制
/**
* @param exchange
* @param message
* @param routingKey
* @throws Exception
*/
public void createConfirmProducerSync(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机(交换机没有存储的能力,在rabbitmq中只有队列才有存储能力)
// fanout:不处理路由键
// direct:处理路由键
channel.exchangeDeclare(exchange, "topic");
try {
// 发送消息routingKey,such as :goods.add
channel.confirmSelect();
channel.basicPublish(exchange, routingKey, null, message.getBytes());
if (!channel.waitForConfirms()) {
System.out.println("send message fail");
} else {
System.out.println("send message success");
}
} catch (Exception e) {
} finally {
// 关闭通道
channel.close();
}
}
8.2.2异步确认机制
public void createConfirmProducerAsync(String exchange, String message, String routingKey) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明交换机(交换机没有存储的能力,在rabbitmq中只有队列才有存储能力)
// fanout:不处理路由键
// direct:处理路由键
channel.exchangeDeclare(exchange, "topic");
// 发送消息routingKey,such as :goods.add
channel.confirmSelect();
final SortedSet<Long> confirmSets = Collections.synchronizedSortedSet(new TreeSet<Long>());
channel.addConfirmListener(new ConfirmListener() {
// 没有问题的handleAck
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSets.headSet(deliveryTag + 1).clear();
} else {
confirmSets.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSets.headSet(deliveryTag + 1).clear();
} else {
confirmSets.remove(deliveryTag);
}
}
});
channel.basicPublish(exchange, routingKey, null, message.getBytes());
// 关闭通道
channel.close();
}