参考:
导入RabbitMQ的依赖:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.4.1</version>
</dependency>
一、简单队列模式
图示:
P:代表生产者
红色方块:代表队列
C:代表消费者
创建RabbitMQ连接工具类:
public class ConnectionUtil {
public static void main(String[] args) throws Exception {
System.out.println(getConnection());
}
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");
return factory.newConnection();
}
}
1.发送消息
public class Send {
private final static String QUEUE_NAME = "q_test_01";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//消息内容
String message = "Hello World";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("[x] send " + message + "");
//关闭通道
channel.close();
//关闭连接
connection.close();
}
}
2.接收消息
public class Recv {
private final static String QUEUE_NAME = "q_test_01";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列
channel.basicConsume(QUEUE_NAME, true, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
}
}
}
二、Work模式
图示:
1个生产者,1个队列,2个消费者
1.发送消息
public class Send {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] args) throws Exception {
//获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (int i = 0; i < 100; i++) {
//消息内容
String message = "" + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(10);
}
channel.close();
connection.close();
}
}
2.接受消息
消费者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] args) throws Exception {
//获取到连接
Connection connection = ConnectionUtil.getConnection();
//获取到mq通道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//同一时刻服务器只会发送一条消息给消费者
channel.basicQos(1);
//定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(QUEUE_NAME, true, consumer);
//获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println("[y] Received" + message);
//休眠
Thread.sleep(10);
//返回确认状态,注释掉表示使用自动确认模式
// channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_work";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 同一时刻服务器只会发一条消息给消费者
//队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
//休眠1秒
Thread.sleep(10000);
//下面这行注释掉表示使用自动确认模式,只有手动ack确认后,队列才会推送下一条消息到消费者
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
备注:
1.消费者1开启了ack自动确认,
2.消费者2关闭了ack自动确认,使用的是手动ack确认,只有队列收到了上N(N为设置的Qos值)条消息的ack确认后才会发送下N条消息。
3.怎样才能做到按照每个消费者的能力分配消息呢?联合使用 Qos 和 Acknowledge 就可以做到。
basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。
4.消息的确认模式
消费者从队列中获取消息,服务端如何知道消息已经被消费呢?
4.1 模式1:自动确认
只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费。
4.2 模式2:手动确认
消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。
开启手动ack确认:
5.两种分发方式
5.1 轮询分发 :
使用任务队列的优点之一就是可以轻易的并行工作。如果我们积压了好多工作,我们可以通过增加工作者(消费者)来解决这一问题,使得系统的伸缩性更加容易。在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配)。平均每个消费者获得相同数量的消息。这种方式分发消息机制称为Round-Robin(轮询)。
使用轮询分发:两个消费者的Qos值设为相同的,关闭手动应答,改为自动应答。
5.2 公平分发 :
虽然上面的分配法方式也还行,但是有个问题就是:比如:现在有2个消费者,所有的奇数的消息都是繁忙的,而偶数则是轻松的。按照轮询的方式,奇数的任务交给了第一个消费者,所以一直在忙个不停。偶数的任务交给另一个消费者,则立即完成任务,然后闲得不行。而RabbitMQ则是不了解这些的。这是因为当消息进入队列,RabbitMQ就会分派消息。它不看消费者为应答的数目,只是盲目的将消息发给轮询指定的消费者。
使用公平分发:关闭自动应答,改为手动应答。
测试数据:
消费者1:休眠10ms(能力强)
消费者2:休眠100ms(能力弱)
消费者1和消费者2都开启了手动ack确认,消费者1消费了81条消息,消费者2消费了19条数据,消息顺序没规律。(公平分发)
消费者1和消费者2都开启了自动ack确认,消费者1消费了50条消息,消费者2消费了50条数据,奇偶轮询。(轮询分发)
三、订阅模式
特点:
1.1个生产者,多个消费者
2.每个消费者都有自己的一个队列
3.生产者没有将消息直接发送到队列,而是发送到了交换机
4.每个队列都要绑定到交换机
5.生产者发送的消息,经过交换机,到达队列,实现一个消息被多个消费者获取
发送消息:
public class Send {
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] args) throws Exception {
//获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明exchange,exchange一旦创建就不能改变,而服务器端创建了exchange,客户端也创建一遍就会报错
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//消息内容
String message = "Hello World";
//注意:消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("sent " + message);
channel.close();
connection.close();
}
}
接受消息:
消费者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_work1";
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] args) throws Exception {
//获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
//同一时刻服务器只会发送一条消息给消费者
channel.basicQos(1);
//定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
//获取消息
while (true) {
//消费者接受队列推送过来的消息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_work2";
private final static String EXCHANGE_NAME = "test_exchange_fanout";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv2] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
备注:
1.一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费
2.在创建exchange时,一旦创建就不能改变,而服务端创建了exchange,客户端也创建一遍就会报错。
3.消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。
在管理工具中查看队列和交换机的绑定关系:
交换机类型为:fanout(广播式交换机)
1.这种模式不需要指定Routing key路由键,一个交换机可以绑定多个队列queue,一个queue可同时与多个exchange交换机进行绑定;
2.如果消息发送到交换机上,但是这个交换机上面没有绑定的队列,那么这些消息将会被丢弃;
四、路由模式
交换机类型为:direct(直连交换机)
说明:
1.任何发送到Direct Exchange的消息都会被转发到指定RouteKey中指定的队列Queue;
2.生产者生产消息的时候需要执行Routing Key路由键;
3.队列绑定交换机的时候需要指定Binding Key,只有路由键与绑定键相同的话,才能将消息发送到绑定这个队列的消费者;
4.如果vhost中不存在RouteKey中指定的队列名,则该消息会被丢弃。
发送消息:
public class Send {
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] args) throws Exception {
//获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明exchange,交换机类型为:direct
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
//消息内容
String message = "新增商品";
// 消息的key为:delete
channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
System.out.println("[x] sent " + message);
channel.close();
connection.close();
}
}
接受消息:
消费者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_direct_1";
private final static String EXCHANGE_NAME= "test_exchange_direct";
public static void main(String[] args) throws Exception {
//获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"delete");
//同一时刻服务器只会发送一条消息给消费者
channel.basicQos(1);
//定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
//获取消息
while (true) {
//消费者接受队列推送过来的消息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_direct_2";
private final static String EXCHANGE_NAME = "test_exchange_direct";
public static void main(String[] args) throws Exception {
//获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
//同一时刻服务器只会发送一条消息给消费者
channel.basicQos(1);
//定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
//监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
//获取消息
while (true) {
//消费者接受队列推送过来的消息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
五、主题模式(通配符模式)
交换机类型为:topic
1.任何发送到Topic Exchange的消息都会被转发到所有满足Route Key与Binding Key模糊匹配的队列Queue上;
2.生产者发送消息的时候需要指定Route Key,同时绑定Exchange与Queue的时候也需要指定Binding Key;
3.#” 表示0个或多个关键字,“*”表示匹配一个关键字;
4.如果Exchange没有发现能够与RouteKey模糊匹配的队列Queue,则会抛弃此消息;
5.如果Binding中的Routing key *,#都没有,则路由键跟绑定键相等的时候才转发消息,类似Direct Exchange;
6.如果Binding中的Routing key为#或者#.#,则全部转发,类似Fanout Exchange;
发送消息:
public class Send {
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
// 消息内容
String message = "Hello World!!";
channel.basicPublish(EXCHANGE_NAME, "routekey.1", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
channel.close();
connection.close();
}
}
接受消息:
消费者1:
public class Recv1 {
private final static String QUEUE_NAME = "test_queue_topic_work_1";
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "routekey.*");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv_x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
消费者2:
public class Recv2 {
private final static String QUEUE_NAME = "test_queue_topic_work_2";
private final static String EXCHANGE_NAME = "test_exchange_topic";
public static void main(String[] argv) throws Exception {
// 获取到连接以及mq通道
Connection connection = ConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成
channel.basicConsume(QUEUE_NAME, false, consumer);
// 获取消息
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [Recv2_x] Received '" + message + "'");
Thread.sleep(10);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
备注:
同一条消息可以发送到多个队列上,一个队列上只有一个消费者实例会消费消息。
六:RPC模式
RPC的处理流程:
1.当客户端启动时,创建一个匿名的回调队列。
2.客户端为RPC请求设置2个属性:replyTo,设置回调队列名字;correlationId,标记request。
3.请求被发送到rpc_queue队列中。
4.RPC服务器端监听rpc_queue队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
5.客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了。
RPC服务端;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
//具体处理方法
private static int fib(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] argv) throws Exception {
//从工厂中获取连接
Connection connection = ConnectionUtil.getConnection();
//从连接中获取信道
final Channel channel = connection.createChannel();
//name:队列名字
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
System.out.println(" [x] Awaiting RPC requests");
while (true) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
BasicProperties props = delivery.getProperties();
// BasicProperties replyProps = new BasicProperties.Builder().correlationId(props.getCorrelationId()).build();
AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder()
.correlationId(props.getCorrelationId()).build();
String message = new String(delivery.getBody());
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
String repsonse = "" + fib(n);
channel.basicPublish("", props.getReplyTo(), replyProps, repsonse.getBytes());
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
}
}
RPC客户端:
public class RPCClient {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
private String replyQueueName;
public RPCClient() throws Exception {
//建立一个连接和一个通道,并为回调声明一个唯一的'回调'队列
connection = ConnectionUtil.getConnection();
channel = connection.createChannel();
//定义一个临时变量的接受队列名
replyQueueName = channel.queueDeclare().getQueue();
}
//发送RPC请求
public String call(String message) throws IOException, InterruptedException {
//生成一个唯一的字符串作为回调队列的编号
String corrId = UUID.randomUUID().toString();
//发送请求消息,消息使用了两个属性:replyto和correlationId
//服务端根据replyto返回结果,客户端根据correlationId判断响应是不是给自己的
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().correlationId(corrId).replyTo(replyQueueName)
.build();
//发布一个消息,requestQueueName路由规则
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
//由于我们的消费者交易处理是在单独的线程中进行的,因此我们需要在响应到达之前暂停主线程。
//这里我们创建的 容量为1的阻塞队列ArrayBlockingQueue,因为我们只需要等待一个响应。
final BlockingQueue<String> response = new ArrayBlockingQueue<String>(1);
// String basicConsume(String queue, boolean autoAck, Consumer callback)
channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
//检查它的correlationId是否是我们所要找的那个
if (properties.getCorrelationId().equals(corrId)) {
//如果是,则响应BlockingQueue
response.offer(new String(body, "UTF-8"));
}
}
});
return response.take();
}
public void close() throws IOException {
connection.close();
}
public static void main(String[] argv) {
RPCClient fibonacciRpc = null;
String response = null;
try {
fibonacciRpc = new RPCClient();
System.out.println(" [x] Requesting fib(30)");
response = fibonacciRpc.call("30");
System.out.println(" [.] Got '" + response + "'");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fibonacciRpc != null) {
try {
fibonacciRpc.close();
} catch (IOException _ignore) {
}
}
}
}
}