读书笔记-rabbitmq实战指南
RabbitMQ简介
生产者
首先与rabbitmq服务器建立一个连接,然后在这个连接的基础上创建一个信道(Channel),
之后建立一个交换器(Exchange),和一个队列(Queue),并通过路由键进行绑定,然后发送一条信息,然后关闭连接
public class RabbitProducer {
private static final String EXCHANGE_NAME = "exchange_demo";
private static final String ROUTING_KEY = "routingkey_demo";
private static final String QUEUE_NAME = "queue_demo";
private static final String IP_ADDRESS = "192.168.20.4";
private static final int PORT = 5672;//RabbitMQ(AMQP) 服务端默认端口号为 5672
public static void main(String[] args) throws IOException,
TimeoutException, InterruptedException {
Connection connection = ConnectionUtils.getConnection(IP_ADDRESS, PORT, "my_vhost",
"admin", "admin");
Channel channel = connection.createChannel();
// 创建一个 type="direct" 、持久化的、非自动删除的交换器
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true, false, null);
// 创建一个持久化、非排他的、非自动删除的队列
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 将交换器与队列通过路由键绑定
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
// 发送一条持久化的消息: hello world !
String message = "Hello World !";
// 消息发布
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
//关闭资源
ConnectionUtils.close(channel, connection);
}
}
消费者
public class RabbitConsumer {
private static final String QUEUE_NAME = "queue_demo";
private static final String IP_ADDRESS = "192.168.20.4";
private static final int PORT = 5672;
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
Address[] addresses = new Address[]{
new Address(IP_ADDRESS, PORT)
};
ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("my_vhost");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection(addresses);
final Channel channel = connection.createChannel();
// 设置客户端最多未被ack的个数
channel.basicQos(64);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException {
System.out.println(" recv message: " + new String(body));
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(QUEUE_NAME, consumer);
// 等待回调函数执行完毕之后 关闭资源
TimeUnit.SECONDS.sleep(5);
ConnectionUtils.close(channel, connection);
}
}
消息包括两个部分
消息体(payload)和标签体,消息体一般是一个json格式的数据,标签体包含一个交换器名称和路由键
交换器
交换器类型:
fanout: 把所有发送到该交换器的消息路由到绑定的队列中
direct: 把消息发送到BindingKey和RoutingKey完全匹配的队列中
tpoic: 发送到BindingKey和RoutingKey模糊匹配的队列中
headers: 不依赖路由键匹配规则,,而是根据消息中的headers属性进行匹配
因为性能差,所以很少使用
Rabbitmq进阶
mandatory和immediate是channel.basicPublish的两个参数
-
mandatory参数:
设置为true时,交换器无法根据条件找到队列,则会将消息返回给生产者,如果为false则直接丢弃
生产者可以使用channel.addReturnListener添加监听器获取返回的失败信息 -
设置TTL
1.通过队列设置,队列中所有信息都有相同的ttl
2.对消息本身进行单独设置,每条消息的ttl可以不同
如果两种方法都使用,则以最短时间为准
如果超过ttl时间,消息变成死信 -
死信队列
当队列中存在死信时,rabbitmq就会自动的将这个消息重新发布到设置的dlx中,进而被路由到另一个队列
死信队列样例
public class DealQueue {
public static void main( String[] args ) {
try {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
// 声明死信交换器和正常交换器
channel.exchangeDeclare("exchange.dlx", "direct", true, false, null);
channel.exchangeDeclare("exchange.normal", "fanout", true, false, null);
// 正常队列的声明和绑定
Map<String, Object> arg = new HashMap<String, Object>();
arg.put("x-message-ttl", NUM10000);
arg.put("x-dead-letter-exchange", "exchange.dlx");
channel.queueDeclare("queue.normal", false, false, false, arg);
channel.queueBind("queue.normal", "exchange.normal", "");
// 死信队列的声明和绑定
channel.queueDeclare("queue.dlx", false, false, false, null);
channel.queueBind("queue.dlx", "exchange.dlx", "rk");
// 发送消息
String message = "delay message !";
channel.basicPublish("exchange.normal", "rk", false,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
ConnectionUtils.close(channel, connection);
} catch (IOException e) {
log.info("catch IOException");
} catch (TimeoutException e) {
log.info("catch TimeoutException");
}
}
}
-
DLX(x-dead-letter-routing-key)
在异常情况下,消息不能被消费者正确消费,而被置入死信队列中的情况
后序可以通过消费这个死信队列中的内容分析当时遇到的异常情况,进而改善和优化系统
DLX配合TTL还可以实现延迟队列的功能 -
延迟队列
就是正常队列设置超时时间,并且将死信队列和正常队列的路由键绑定,消费者只消费死信队列,当消息在正常队列中超时后就被移动到死信队列进行消费,达到延迟的目的
-
优先级队列
arg.put("x-max-priority", NUM10);
队列上会有一个优先级标识
-
RPC
-
持久化
交换器持久化,队列持久化,消息持久化
交换器持久化:声明队列将durable参数设置为true
队列持久化:声明队列将durable参数设置为true(只是将队列的元数据初始化)
消息持久化: 消息的投递模式设置为2
如果将队列和消息都设置为持久化,会严重影响性能
如果不需要可靠性高的消息可以不设置为持久化
对所有对象进行持久化也不能保证消息不丢失
如果设置了autoAck为true,当消费者刚接收到消息就宕机了,也算数据丢失
这种情况可以设置手动确认即可
持久化消息的时候,rabbitmq不会为每条信息存一次盘,而是存在缓冲区中,如果这个时候宕机,消息也会丢失
解决办法:引入镜像队列,当master宕机可以启用slave
实际生产中一般都会设置镜像队列
发送端: 引入事务机制或者发送方确认机制,保证消息已经正确发送和存储到rabbitmq中
- 生产者确认
1.事务机制
事务机制是发送一条后阻塞,等待响应,这样会严重影响吞吐量
public class Transaction {
/**
* 交换器
*/
private static final String EXCHANGE_NAME = "exchange_demo";
/**
* 路由键
*/
private static final String ROUTING_KEY = "routingkey_demo";
/**
* 队列
*/
private static final String QUEUE_NAME = "queue_tx";
public static void main( String[] args ) {
try {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true, false, null);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);
try {
// 开启事务:将当前信道设置为事务模式
channel.txSelect();
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, false,
MessageProperties.PERSISTENT_TEXT_PLAIN, "事务".getBytes());
// 模拟异常
int result = 1 / 0;
// 提交事务
channel.txCommit();
} catch (Exception e) {
log.info("catch Exception");
// 回滚事务
channel.txRollback();
// 消息重发
log.info("增加消息重发逻辑");
}
ConnectionUtils.close(channel, connection);
} catch (IOException e) {
log.info("catch IOException");
} catch (TimeoutException e) {
log.info("catch TimeoutException");
}
}
}
2.发送方确认机制
好处是,它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条信息
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(); // 开始confirm模式
for (int i = 0; i < NUM20; i++) {
// 发送消息
String message = "hello, confirm message " + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
}
if (channel.waitForConfirms()) {
log.info(" [x] Sent message ok ");
} else {
log.info(" [x] Sent message fail ");
}
channel.close();
connection.close();
}
-
消息分发
channel.basicQos(5)
如果消费者的未确认消息超过5条,则不再向该消费者发送消息 -
消息传输保障
一般中间件的消息传输保障分为三个等级:
rabbitmq支持最多一次和最少一次
要做到最少一次,消息觉不丢失
1.消息生产者开启事务机制或者publish confirm机制,确保消息可靠传输到rabbitmq中
2.消息生产者配合使用mandatory参数或者备份交换器保证消息能够从交换器路由到队列中
3.消息和队列要进行持久化处理
4.消费者在消费时要讲autoAck设置为false,然后通过手动确认方式去确认已经正确消费的消息
恰好一次:无法保证
最多一次:生产者随意发送,消费者随意接受即可,很难保证消息不会丢失
数据去重一般由业务去自行处理
如:引入全局id或者根据业务本身特性去重
有的业务本身具有幂等性
rabbitmq管理
虚拟主机:相当于一个独立的小型rabbitmq服务器
rabbitmqctl add_vhost aaa
添加虚拟主机
rabbitmqctl list_vhosts
列出所有虚拟主机