消息应答与消息持久化
消息应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
boolean autoAck = true;(自动确认模式)
一旦 Rabbit MQ 将消息分发给消费者,就会从内存中删除,这种情况下,如果杀死正在执行的消费者,就会丢失正在处理的消息
boolean autoAck = false;(手动模式)
如果有一个消费者挂掉,就会交付给其它消费者,Rabbit MQ 支持消息应答,消费者发送一个消息应答,告诉 Rabbit MQ 这个消息我已经处理完成,你可以删了,然后 Rabbit MQ 就删除内存中的消息
消息应答默认是打开的,false
Message acknowledgment
大家想想如果我们的 Rabbit MQ 挂了,我们的消息仍然会丢失
消息的持久化
boolean durable = false;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
我们将程序中的 boolean durable = false; 改成 true; 是不可以的,尽管代码是正确的,它也不会运行成功,因为我们已经定义了一个叫 work_queue 的消息队列,这个 queue 是未持久化的,Rabbit MQ 不允许重新定义(不同参数)一个已存在的队列
订阅模式 publish/subscribe
模型
解读:
1.一个生产者,多个消费者
2.每一个消费者都有自己的队列
3.生产者没有直接把消息发送到队列 而是发到了交换机 转发器 exchange
4.每个队列都要绑定到交换机上
5.生产者发送的消息,经过交换机,到达队列,就能实现一个消息被多个消费者消费
注册 -> 邮件 -> 短信
生产者
public class Send {
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 声明exchange
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 消息内容
String message = "hello ps!";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("[x] Send '" + message + "'");
channel.close();
connection.close();
}
}
消息哪去了???丢失了!!!因为交换机没有存储的能力,在 Rabbit MQ 中只有队列只有存储能力,因为这时候还没有队列绑定到交换机,所以数据丢失了
消费者 1
public class Receive1 {
private final static String QUEUE_NAME = "queue_work1";
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 保证一次只分发一个
channel.basicQos(1);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 获取到到达的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[1] done");
// 手动回执
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 监听队列,自动应答改为 false
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
消费者 2
public class Receive2 {
private final static String QUEUE_NAME = "queue_work2";
private final static String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception {
// 获取一个连接
Connection connection = ConnectionUtils.getConnection();
// 从连接中获取一个通道
Channel channel = connection.createChannel();
// 创建队列声明
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 绑定队列到交换机
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 保证一次只分发一个
channel.basicQos(1);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
/**
* 获取到到达的消息
* @param consumerTag
* @param envelope
* @param properties
* @param body
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("[2] done");
// 手动回执
channel.basicAck(envelope.getDeliveryTag(), false);
}
}
};
// 监听队列,自动应答改为 false
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
}
}
Exchange(交换机、转发器)
一方面是接收生产者的消息,另一方面是向队列推送消息
匿名转发 “”
Fanout(不处理路由键)
Direct(处理路由键)
路由模式
模型
生产者
public class Send {
private static final String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
String msg = "hello direct!";
String routingKey = "error";
channel.basicPublish(EXCHANGE_NAME, routingKey, null, msg.getBytes());
System.out.println("send " + msg);
channel.close();
connection.close();
}
}
消费者 1
public class Receive1 {
private static final String EXCHANGE_NAME = "exchange_direct";
private static final String QUEUE_NAME = "queue_direct_1";
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);
channel.basicQos(1);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} 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 Receive2 {
private static final String EXCHANGE_NAME = "exchange_direct";
private static final String QUEUE_NAME = "queue_direct_2";
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);
channel.basicQos(1);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "error");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "info");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "warning");
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(2000);
} 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);
}
}
Topic exchange
将路由键和某模式匹配
# 匹配一个或多个
* 匹配一个
Goods.#
模型
商品:发布 修改 删除 查询
生产者
public class Send {
private final static String EXCHANGE_NAME = "exchange_topic";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String msgString = "商品...";
channel.basicPublish(EXCHANGE_NAME, "goods.add", null, msgString.getBytes());
System.out.println("---send " + msgString);
channel.close();
connection.close();
}
}
消费者 1
public class Receive1 {
private static final String EXCHANGE_NAME = "exchange_topic";
private static final String QUEUE_NAME = "queue_topic_1";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.add");
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[1] recv msg:" + msg);
try {
Thread.sleep(2000);
} 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 Receive2 {
private static final String EXCHANGE_NAME = "exchange_topic";
private static final String QUEUE_NAME = "queue_topic_2";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "utf-8");
System.out.println("[2] recv msg:" + msg);
try {
Thread.sleep(2000);
} 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);
}
}
Rabbit MQ 的消息确认机制(事务 + confirm)
在 Rabbit MQ 中,我们可以持久化数据解决 Rabbit MQ 服务器异常的数据丢失问题
问题:生产者将消息发送出去之后,消息到底有没有到达 Rabbit MQ 服务器,默认的情况是不知道的
两种方式:
1.AMQP 实现了事务机制
2.Confirm 模式
事务机制
txSelect txCommit txRollBack
txSelect:用户将当前 channel 设置成 transaction 模式
txCommit:用于提交事务
txRollBack:回滚事务
生产者
public class TxSend {
private static final String QUEUE_NAME = "queue_tx";
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 msgString = "hello tx message";
try {
channel.txSelect();
channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
System.out.println("send " + msgString);
channel.txCommit();
} catch (Exception e) {
channel.txRollback();
System.out.println("send message txRollBack");
} finally {
channel.close();
connection.close();
}
}
}
消费者
public class TxReceive {
private static final String QUEUE_NAME = "queue_tx";
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);
channel.basicConsume(QUEUE_NAME, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("[recv] tx msg:" + new String(body, "utf-8"));
}
});
}
}
此种模式还是很耗时的,采用这种方式降低了 Rabbit MQ 的消息吞吐量
Confirm 模式
生产者端 confirm 模式的实现原理
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息,都会被指派为一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker 回传给生产者的确认消息中,deliver-tag 域中包含了确认消息的序列号,此外,broker 也可以设置 basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理
Confirm 模式最大的好处在于它是异步的
Nack
开启 confirm 模式
channel.confirmSelect()
编程模式:
1.普通:发一条 waitForConfirms()
2.批量的:发一批 waitForConfirms()
3.异步 confirm 模式:提供一个回调方法
Confirm 单条
public class Send1 {
private static final String QUEUE_NAME = "queue_confirm1";
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);
channel.confirmSelect();
String msgString = "hello confirm message!";
channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
if (!channel.waitForConfirms()) {
System.out.println("message send failed");
} else {
System.out.println("message send ok");
}
channel.close();
connection.close();
}
}
批量
public class Send2 {
private static final String QUEUE_NAME = "queue_confirm2";
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);
channel.confirmSelect();
String msgString = "hello confirm message!";
for (int i = 0; i < 10; i++) {
channel.basicPublish("", QUEUE_NAME, null, msgString.getBytes());
}
if (!channel.waitForConfirms()) {
System.out.println("message send failed");
} else {
System.out.println("message send ok");
}
channel.close();
connection.close();
}
}
异步模式
Channel 对象提供的 confirmListener() 回调方法只包含 deliveryTag(当前 Channel 发出的消息序号),我们需要自己为每一个 Channel 维护一个 unconfirm 的消息序号集合,每 publish 一条数据,集合中元素加 1,没回调一次 handlerAck() 方法,unconfirm 集合删掉相应的一条(multiple=false)或多条(multiple=true)记录,从程序运行效率上看,这个 unconfirm 集合最好采用有序集合 SortedSet 存储结构
public class Send3 {
private static final String QUEUE_NAME = "queue_confirm3";
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);
channel.confirmSelect();
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long l, boolean b) throws IOException {
if (b) {
System.out.println("---handleAck---multiple");
confirmSet.headSet(l + 1).clear();
} else {
System.out.println("---handleAck---multiple false");
confirmSet.remove(l);
}
}
@Override
public void handleNack(long l, boolean b) throws IOException {
if (b) {
System.out.println("---handleNack---multiple");
confirmSet.headSet(l + 1).clear();
} else {
System.out.println("---handleNack---multiple false");
confirmSet.remove(l);
}
}
});
String msgStr = "ssssss";
while (true) {
long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("", QUEUE_NAME, null, msgStr.getBytes());
confirmSet.add(seqNo);
}
}
}