Message Queue
消息队列,又称消息中间件,是典型的生产者和消费者模型,生产者不断向队列中生产消息,消费者不断从队列中获取消息。因为消息的生产和消费都是异步进行的,所以只需要关心消息的发送和接收,没有业务逻辑的侵入,轻松实现系统间的解耦。
AMQP
Advanced Message Queue Protocol(高级消息队列协议),AMQP不是从API层面进行限定的,而是直接定义网络交换的数据格式。
创建虚拟机时,虚拟机的命名必须是以“/”开头,生产者可以将消息发送给交换机,也可以直接发送给队列。
1. 创建虚拟机和用户(同时给该用户赋权限)
2. 将虚拟机和用户设置关联
RabbitMQ 的几种工作模式
生产者发送消息后要关闭连接,避免浪费资源,消费者不能关闭连接,否则接收不到队列中的消息。
1. 点对点
最简单的工作模式,生产者不经过交换机直接将消息发送到队列,消费者从队列中获取消息。
工具类:
public class RabbitMQUtils {
// 创建mq连接工厂
public static ConnectionFactory connectionFactory = new ConnectionFactory();
static {
// 设置mq服务器地址
connectionFactory.setHost("110.27.10.63");
// 设置mq端口号
connectionFactory.setPort(5672);
// 设置需要连接的虚拟主机名
connectionFactory.setVirtualHost("/ems");
// 设置访问虚拟主机的账号和密码
connectionFactory.setUsername("emsuser");
connectionFactory.setPassword("123");
}
public static Connection getConnection() {
try {
return connectionFactory.newConnection();
} catch (Exception e){
e.printStackTrace();
}
return null;
}
public static void close(Connection connection, Channel channel){
try {
if(channel!=null)channel.close();
if(connection!=null)connection.close();
} catch (Exception e){
e.printStackTrace();
}
}
}
生产者:
public void sendMessage() throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
// 获取连接中的通道
Channel channel = connection.createChannel();
/*
* 通道绑定指定队列(可理解为提前对某些队列的一些设置,对指定队列的参数要与消费者一致)
* arg1:要绑定的队列名称,队列不存在则自动创建
* arg2:是否持久化队列(是队列本身,不包含队列中的数据),true-是
* arg3:当前连接是否独享队列,true-是
* arg4:当前队列在被消费完成后是否自动被删除(消费完成后只有消费者与队列断开连接才会彻底删除该队列),true-是
* arg5:额外附加参数
*/
channel.queueDeclare("hello1",true,false,true,null);
/*
* 发布消息(具体发布信息到哪个队列)
* arg1:交换机名
* arg2:路由key(没有交换机时为队列名)
* arg3:传递消息额外设置(如消息持久化参数:MessageProperties.PERSISTENT_TEXT_PLAIN)
* arg4:消息内容
*/
channel.basicPublish("","hello1", MessageProperties.PERSISTENT_TEXT_PLAIN,"hello rabbit's1收到".getBytes());
RabbitMQUtils.close(connection,channel);
}
消费者:
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello1",true,false,true,null);
/*
* 消费消息(对队列的参数要与生产者一致)
* arg1:要消耗消息的队列名
* arg2:开启消息自动确认机制(接收到消息就确认,不关心业务逻辑是否完成)
* arg3:消费时的回调接口
*/
channel.basicConsume("hello1", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("============"+new String(body));
}
});
}
2. work queue
点对点模式中当生产速度大于消费速度时,队列中会出现大量消息堆积,无法及时处理。多个消费者共同消费同一个队列时,消息消费速度会大大提高,消息一旦被消费就会从队列中消失,因此不会出现重复消费。
默认情况下队列的消息分配策略是平均分配,rabbitmq只关心消费者是否拿到消息,不关系消息的处理业务是否完成,所以会导致处理消息慢的消费者消息积压。(也就是默认的平均分配策略是不可取的。)
根据消费者的处理速度决定消息的分配:将消费者消息自动确认改成手动确认,实现能者多劳。
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
final Channel channel = connection.createChannel();
channel.queueDeclare("work",true,false,false,null);
// 每次只消费一个消息
channel.basicQos(1);
// arg2:消息自动确认
channel.basicConsume("work",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumer-1:"+new String(body));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 手动确认消息 arg1:当前消息,arg2:是否开启多个消息同时确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
3. fanout(广播或发布订阅)
生产者将消息发给交换机,由交换机将消息发送给与此交换机绑定过的所有队列。
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
/*
* 通道指定交换机
* arg1:交换机名称
* arg2:交换机类型
*/
channel.exchangeDeclare("logs","fanout");
channel.basicPublish("logs","",null,"fanout广播模式11sdsd22".getBytes());
RabbitMQUtils.close(connection,channel);
}
}
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 通道声明交换机
channel.exchangeDeclare("logs","fanout");
// 创建临时队列
String queue = channel.queueDeclare().getQueue();
/*
* 队列与交换机绑定
* 参数分别为:队列名、交换机名、rountkey
*/
channel.queueBind(queue,"logs","");
// 消费信息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumer1:"+new String(body));
}
});
}
}
4. direct(路由)
生产者发送消息时指定路由键,队列只从交换机中获取指定路由键的消息,而不是全部获取交换机中的消息。与fanout模式多指定了rountingKey而已!
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机,并指定为订阅类型
channel.exchangeDeclare("logs_direct","direct");
String routingName = "debug";
channel.basicPublish("logs_direct",routingName,null,routingName.getBytes());
RabbitMQUtils.close(connection,channel);
}
}
public class Comsumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("logs_direct","direct");
channel.basicQos(1);
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"logs_direct","info");
channel.basicConsume(queue,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consmer-1:"+new String(body));
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
final Channel channel = connection.createChannel();
channel.exchangeDeclare("logs_direct","direct");
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"logs_direct","debug");
channel.queueBind(queue,"logs_direct","info");
channel.queueBind(queue,"logs_direct","error");
channel.basicQos(1);
channel.basicConsume(queue,false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer-2:"+new String(body));
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
5. topic(主题)
与direct模式不同的是不再是固定的rountingkey,而是模糊的rountingkey。
*:替代一个单词
#:替代0到多个单词
public class Provider {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 声明交换机名称和类型
channel.exchangeDeclare("topics","topic");
String routingKey = "sd.name";
// 发送信息
channel.basicPublish("topics",routingKey,null,("路由key为:"+routingKey).getBytes());
RabbitMQUtils.close(connection,channel);
}
}
public class Consumer1 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topics","topic");
String queue = channel.queueDeclare().getQueue();
String routingKey = "name.*";
channel.queueBind(queue,"topics",routingKey);
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer-1 消费的路由为:"+new String(body));
}
});
}
}
public class Consumer2 {
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topics","topic");
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"topics","name.#");
channel.queueBind(queue,"topics","*.name.*");
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer-2 消费路由为:"+new String(body));
}
});
}
}
通过channel.queueDeclare().getQueue()获得的队列为临时队列,该队列不会持久化,要想获取持久化数据要做以下修改:
生产者发送消息时将消息持久化:
channel.basicPublish("topics",routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN,("路由key为:"+routingKey).getBytes());
消费者获取非临时队列并绑定到指定交换机exchange:
String queue = channel.queueDeclare("holo",true,false,false,null).getQueue(); channel.queueBind(queue,"topics","name.#");
生产者消息确认
事务
有一条消息发送失败,则全部消息回滚。事务的机制是阻塞性的,在发送一条消息后要等待rabbitmq回应后才能发送下一条; 会增加生产者与broker的交互次数,造成资源的浪费; 整体效率不高;因此实际应用中比较少用。
public static void main(String[] args) throws IOException {
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("tx.exchange.direct","direct");
channel.queueDeclare("tx.direct.queue",true,false,true,null);
channel.queueBind("tx.direct.queue","tx.exchange.direct","info");
/**
* 事务的机制是阻塞性的,在发送一条消息后要等待rabbitmq回应后才能发送下一条;
* 会增加生产者与broker的交互次数,造成资源的浪费;
* 整体效率不高;因此实际应用中比较少用。
*/
// 开启事务
channel.txSelect();
try {
channel.basicPublish("tx.exchange.direct","info",null,"事务模式1".getBytes());
int a = 10/0;
channel.basicPublish("tx.exchange.direct","info",null,"事务模式2".getBytes());
// 事务提交
channel.txCommit();
System.out.println("消息发送成功提交");
} catch (Exception e){
// 事务回滚
channel.txRollback();
System.out.println("消息发送失败回滚");
// 处理发送失败的消
} finally {
RabbitMQUtils.close(connection,channel);
}
}
消息确认
1. 单条确认
缺点:每条消息都要确认,效率不是很高;
优点:能明确知道是哪条消息发送失败,可以避免补发重复。
Connection connection = RabbitMQUtils.getConnection();
Channel channel = null;
try {
channel = connection.createChannel();
channel.exchangeDeclare("confirm.exchange","direct");
channel.queueDeclare("confirm.queue",true,false,true,null);
channel.queueBind("confirm.queue","confirm.exchange","info");
/*
* 开启消息确认机制
* confirm只能保证消息到达exchange,无法保证消息被exchange分发到queue。
*/
channel.confirmSelect();
channel.basicPublish("confirm.exchange","info",null,new Book("出埃及记",10).toString().getBytes());
/*
* 确认消息是否发送成功,
* 缺点:每条消息都要确认,效率不是很高
* 优点:能明确知道是哪条消息发送失败,可以避免补发重复
*/
boolean b = channel.waitForConfirms(1000);
if(b){
System.out.println("消息发送成功");
} else {
System.out.println("消息发送失败");
// 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
System.out.println("消息发送失败:阻塞");
// 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
} catch (TimeoutException e) {
System.out.println("消息发送失败:超时");
// 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
} finally {
RabbitMQUtils.close(connection,channel);
}
2. 批量确认
优点:消息批量确认,效率很高;
缺点:批量消息中有一条发送失败则认定失败,抛出异常。消息补发时会全部重新补发,会出现消息重复发送,发送效率变低
try {
channel = connection.createChannel();
channel.exchangeDeclare("confirm.exchange","direct");
channel.queueDeclare("confirm.queue",true,false,true,null);
channel.queueBind("confirm.queue","confirm.exchange","info");
// 开启消息确认
/*
* 批量确认消息是否发送成功,
* 优点:消息批量确认,效率很高
* 缺点:批量消息中有一条发送失败则认定失败,抛出异常。消息补发时会全部重新补发,会出现消息重复发送,发送效率变低
*/
channel.confirmSelect();
channel.basicPublish("confirm.exchange","info",null,new Book("出埃及记",10).toString().getBytes());
channel.basicPublish("confirm.exchange","info",null,new Book("出埃及记",10).toString().getBytes());
channel.waitForConfirmsOrDie(1000);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("消息发送失败");
// 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
} catch (TimeoutException e) {
e.printStackTrace();
System.out.println("消息发送失败超时");
// 递归补发,递归一定次数仍然失败后,将数据存入数据库或redis
} finally {
RabbitMQUtils.close(connection,channel);
}
3. 异步确认
Connection connection = RabbitMQUtils.getConnection();
Channel channel = null;
try {
channel = connection.createChannel();
channel.exchangeDeclare("confirm.exchange","direct");
channel.queueDeclare("confirm.queue",true,false,true,null);
channel.queueBind("confirm.queue","confirm.exchange","info");
/*
* 开启confirm 机制
* exchange不能持久化消息,queue能持久化,由于confirm机制只能确认消息是否发送到exchange,而不能确认消息是否从exchange发送到queue,
* 所以当消息到达exchange而为到queue时发生异常,同样会导致消息丢失,
* 所以还需开启return 机制
*/
channel.confirmSelect();
/*
* 异步confirm监听
*/
channel.addConfirmListener(new ConfirmListener() {
/*
* 消息被确认的回调
* arg1: 当前消息编号
* arg2: 当前消息是否同时确认了多条
*/
public void handleAck(long l, boolean b) throws IOException {
System.out.println("当前确认消息编号为:"+l+"。同时确认为:"+b);
}
/*
* 消息没被确认的回调
* arg1: 当前消息编号
* arg2: 当前消息是否同时确认了多条
*/
public void handleNack(long l, boolean b) throws IOException {
System.out.println("当前未被确认消息编号为:"+l+"。同时确认为:"+b);
}
});
/*
* return 监听
*/
channel.addReturnListener(new ReturnListener() {
// 当消息没有到达queue时才会执行
public void handleReturn(int i, String s, String s1, String s2, AMQP.BasicProperties basicProperties, byte[] bytes) throws IOException {
System.out.println(new String(bytes,"utf-8")+"没有被送达queue中.");
}
});
// 第三个参数为true时才会触发return机制(默认为false)
channel.basicPublish("","info",true,null,new Book("西游记",110).toString().getBytes());
// deliveryMode 交付模式为1表示持久化消息,反之为0;为每一个Message设置一个id,为消费者消息防重复消费做铺垫
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder().deliveryMode(1).messageId(UUID.randomUUID().toString()).build();
channel.basicPublish("confirm.exchange","info",true,basicProperties,new Book("出埃及记",10).toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
// 异步监听要求连接不能断
// RabbitMQUtils.close(connection,channel);
}
消费者消息防重复消费
问题:有多个消费者消费同一个队列时,一个消费者拿到消息消费过程中又被另一个消费者拿到消费,造成消息的重复消费。
思路:当消息被分给某一个消费者时,在redis中记录该消息的唯一标识,当其他消费者也拿到了此消息时先去redis中查看是否有此消息的标识,如果没有则进行消费;如果有且没消费完毕,则不做任何操作,如果有且消费完成,则确认此消息。为避免消费消息过程中出现死锁等异常情况 ,在redis中记录消息标识时需设置超时时间。
public static void main(String[] args) {
Connection connection = RabbitMQUtils.getConnection();
try {
final Channel channel = connection.createChannel();
channel.exchangeDeclare("confirm.exchange","direct");
channel.queueDeclare("confirm.queue",true,false,true,null);
channel.queueBind("confirm.queue","confirm.exchange","info");
final Jedis jedis = new Jedis("110.37.20.5",8888);
channel.basicConsume("confirm.queue",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("sdsds:"+properties.getMessageId());
// 1. 在redis中记录当前要处理的消息
String set = jedis.set(properties.getMessageId(), "0", new SetParams().nx().ex(7));
// 2. 记录成功:说明redis 中之前没有该消息,该消息未被消费处理,此时进行消息的消费
if(set != null && set.equalsIgnoreCase("OK")){
System.out.println("接收到消息,处理!");
channel.basicAck(envelope.getDeliveryTag(),false);
jedis.set(properties.getMessageId(),"1",new SetParams().xx().ex(10));
} else {
System.out.println("jilu");
// 3. 记录失败:说明该消息已被记录,正在被处理;获取key的值,0-正在处理,不做任何操作,1-处理完成没确认,进行确认操作。
String status = jedis.get(properties.getMessageId());
if("1".equals(status)){
channel.basicAck(envelope.getDeliveryTag(),false);
} else {
}
}
}
});
} catch (IOException e) {
e.printStackTrace();
// 消息处理异常:将消息重新发送到队列尾部或者其他操作。
}
}
rabbitmq依赖:
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>