一、引言
什么RabbitMQ?
RabbitMQ是基于amqp协议,实现的一种MQ理念的服务。类似的服务 RocketMQ、ActiveMQ、Kafka等
为什么在分布式项目中需要一款消息中间件?
消息中间件能够实现一些Feign(同步调用)无法实现的效果:
1、服务的异步调用
2、消息的广播(事件总线)
3、消息的延迟处理
4、分布式事务
5、请求削峰(处理高并发)
二、RabbitMQ的Docker安装
1)拉取镜像
docker pull rabbitmq:3.8.5-management
2)准备docker-compose模板
....
rabbitmq:
image: rabbitmq:3.8.5-management
container_name: rabbitmq
ports:
- 5672:5672
- 15672:15672
restart: always
3)启动rabbitmq容器
docker-compose up -d rabbitmq
4)访问rabbitmq的管理页面
5)登录rabbitmq的管理页面 (账号:guest 密码:guest)
三、RabbitMQ常用模型
1)模型一
P -> Provider(提供者)
红色方块 -> 队列(存储消息)
C -> Consumer(消费者)
2)模型二
一个提供者对应多个消费者,消息会轮训发送给两个消费者
起到一个消费端负载均衡的目的,减轻消费端的消费压力
3)模型三
发布/订阅模式 - 消息广播
多个消费者会同时收到提供者发布的消息
X -> Exchange(交换机,消息的复制转发,不能存储消息)交换机类型有4种fanout,direct,topic,header 模型3用的就是fanout,不含路由键的无条件广播
4)模型四 (交换机用的是direct,含路由键的,选择)
路由键 -> 交换机和队列绑定若干路由键,发布的消息可以指定路由键发送
5)模型五
通配符的路由键
6)模型六
Rabbitmq的同步调用模型
四、JavaAPI调用RabbitMQ
添加依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>
1)模型一的实现
服务的提供者:
public static void main(String[] args) throws IOException, TimeoutException {
//1、连接RabbitMQ
Connection connection = ConnectionUtil.getConnection();
//2、通过连接获得管道对象(后面所有的操作都是通过管道对象操作)
Channel channel = connection.createChannel();
//3、创建队列
channel.queueDeclare("test_queue1", false, false, false, null);
//4、给队列中发布消息
String msg = "Hello RabbitMQ!!!!";
channel.basicPublish("", "test_queue1", null, msg.getBytes("utf-8"));
//关闭连接
connection.close();
}
服务的消费者:
public static void main(String[] args) throws IOException {
//1、连接RabbitMQ
Connection connection = ConnectionUtil.getConnection();
//2、获得连接的channel对象
Channel channel = connection.createChannel();
//3、监听队列
channel.basicConsume("test_queue1", 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, "utf-8"));
}
});
//关闭连接???
// connection.close();
}
思考:
1、队列应该在消费者端创建还是提供者端创建?- 消费者通常创建队列,提供者创建交换机
因为提供者创建交互机没有消费者顶多把消息丢失,而不会报错,消费者创建队列没有人给他发消息他也不会报错。
2、消费端是同步消费消息还是异步消费消息?- 同步消费,必须消费完一条消息,才能继续消费下一条消息,在实际开发过程中,为了提高消费者的消费速率,往往会引入线程池的方式,进行多线程消费。
3.匿名内部类使用外部类的局部变量需要加上final
//创建一个线程池 - 线程数量为5 private static ExecutorService executorService = Executors.newFixedThreadPool(5); .... channel.basicConsume("test_queue1", true, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { executorService.submit(new Runnable() { public void run() { try { System.out.println("接收到消息:" + new String(body, "utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } });
五、常用方法的参数
/**
* Declare a queue
* @see com.rabbitmq.client.AMQP.Queue.Declare
* @see com.rabbitmq.client.AMQP.Queue.DeclareOk
* @param queue the name of the queue
* @param durable true if we are declaring a durable queue (the queue will survive a server restart)
* @param exclusive true if we are declaring an exclusive queue (restricted to this connection)
* @param autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use)
* @param arguments other properties (construction arguments) for the queue
* @return a declaration-confirm method to indicate the queue was successfully declared
* @throws java.io.IOException if an error is encountered
*/
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;
参数一:queue 队列的名称
参数二:durable 是否持久化,默认为false,非持久化
参数三:exclusive 是否为排他队列,排他队列只有创建这个队列的连接可以操作,其他连接不能操作该队列
参数四:autoDelete 是否自动删除,默认为false,如果为true,那么当所有监听这个队列的消费端断开监听后,队列会自动删除
参数五:arguments 用来设置一个额外的参数
/**
* Declare an exchange, via an interface that allows the complete set of
* arguments.
* @see com.rabbitmq.client.AMQP.Exchange.Declare
* @see com.rabbitmq.client.AMQP.Exchange.DeclareOk
* @param exchange the name of the exchange
* @param type the exchange type
* @param durable true if we are declaring a durable exchange (the exchange will survive a server restart)
* @param autoDelete true if the server should delete the exchange when it is no longer in use
* @param internal true if the exchange is internal, i.e. can't be directly
* published to by a client.
* @param arguments other properties (construction arguments) for the exchange
* @return a declaration-confirm method to indicate the exchange was successfully declared
* @throws java.io.IOException if an error is encountered
*/
Exchange.DeclareOk exchangeDeclare(String exchange,
String type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments) throws IOException;
参数一:exchange 交换机的名称
参数二:type 交换机的类型(fanout|direct|topic|header)
参数三:durable 持久化,默认false
参数四:autoDelete 是否自动删除,默认为false,如果未true,所以绑定到交换机的队列,解绑后,交换机就会自动删除
参数五:internal 是否为内置交换机,默认为false,如果为true表示,当前交换机只能绑定其他交换机,提供者不能直接发消息给该交换机
参数六:arguments 用来设置一个额外的参数
六、TTL - 过期时间
6.1 消息的过期时间(重要)
6.1.1 通过队列的方式设置消息的过期时间
Map<String, Object> map = new HashMap<>();
//通过队列,设置5秒的消息过期时间
map.put("x-message-ttl", 5000);
channel.queueDeclare("demo_queue", true, false, false, map);
6.1.2 通过消息本事设置消息的过期时间
String msg = "Hello RabbitMQ!!!!";
//给当前消息本身设置5秒的过期时间
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties().builder();
builder.expiration("5000");
AMQP.BasicProperties basicProperties = builder.build();
channel.basicPublish("demo_exchange", "", basicProperties, msg.getBytes("utf-8"));
注意:
1、RabbitMQ的队列,只会移除队头的过期元素
2、通过队列的方式设置消息的过期时间,过期的元素一定是在队头
3、通过消息本身设置过期时间,过期的消息就不一定在队头
6.2 队列本身的过期时间(了解)
Map<String, Object> map = new HashMap<>();
//设置队列本身的过期时间
map.put("x-expires", 5000);
channel.queueDeclare("demo_queue", true, false, false, map);
七、死信队列
7.1 什么是死信队列?
死信队列本身其实是一个普通队列,但是专门用来存放死信消息,这种队列就称之为死信队列
7.2 什么是死信消息?
死信消息的产生方式:
1、消息过期后,就会变成死信消息
2、队列满了之后,继续添加元素,就会产生死信消息
3、消息被消费者拒绝,并且requeue设置为false时,消息变成死信消息
7.3 设置死信交换机、死信队列
//创建死信交换机、死信队列、绑定
//---------------------------------------------
channel.exchangeDeclare("dead_exchange", "fanout");
channel.queueDeclare("dead_queue", true, false,false, null);
channel.queueBind("dead_queue", "dead_exchange", "");
//---------------------------------------------
//3、创建交换机
channel.exchangeDeclare("demo_exchange", "fanout"); //fanout|direct|topic|header
//4、创建队列
Map<String, Object> map = new HashMap<>();
//设置普通队列绑定死信交换机
map.put("x-dead-letter-exchange", "dead_exchange");
//设置5秒的过期时间,方便参数死信消息
map.put("x-message-ttl", 5000);
channel.queueDeclare("demo_queue", true, false, false, map);
//绑定
channel.queueBind("demo_queue", "demo_exchange", "");
八、延迟队列
8.1 什么是延迟队列?
提供者发送的消息,要通过一段时间的延迟,才会被消息者消费。
注意:RabbitMQ本身没有提供延迟队列,但是开发者可以通过TTL + 死信队列的方式,实现延迟的效果
8.2 延迟队列的实现
8.3 延迟队列的实际运用场景(但是延迟队列有一个缺点因为RabbitMQ的队列,只会移除队头的过期元素,死信队列也是一样,所以在面对不确定过期时间就不好了,比如队头是8个小时过期,后面是半个小时,得先等8个小时的消费完了才能消费半个小时的)
下单后,半小时之内,未支付的订单需要自动关闭
8.4 延迟队列的实际开发结构
1、延迟队列没办法实现随意时间的延迟
2、延迟队列不能取代定时任务
延迟队列非常适合处理,过xxxx时间需要做xxx事情的场景,不适合处理每天中午12点做xxx事情
九、消息的持久化
RabbitMQ中,队列的持久化,不意味着消息持久化,默认消息都是非持久化的。
9.1 设置持久化消息
方式一:
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties().builder();
//给当前消息本身设置5秒的过期时间
//builder.expiration("5000");
//设置当前消息未持久化消息 - 写入RabbitMQ的硬盘中的
builder.deliveryMode(2);
AMQP.BasicProperties basicProperties = builder.build();
channel.basicPublish("demo_exchange", "", basicProperties, msg.getBytes("utf-8"));
方式二:
channel.basicPublish("demo_exchange", "",
MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("utf-8"));
注意:因为持久化消息,意味着消息要写入RabbitMQ的硬盘,所以在相同数量消息的情况下,持久化也就意味着需要消耗更多的发布时间和RabbitMQ服务器的性能,会降低RabbitMQ服务的吞吐量。实际开发过程中,尽量只对需要持久化的消息设置持久化。
思考:交换机持久化 + 队列持久化 + 消息持久化 能否保证消息一定到达消费端? - 不能
十、发送端的确认机制
10.1 RabbitMQ的事务机制
//将当前的管道模式设置成事务模式
channel.txSelect();
try {
String msg = "Hello RabbitMQ!!!!";
channel.basicPublish("demo_exchange", "", MessageProperties.PERSISTENT_TEXT_PLAIN,
msg.getBytes("utf-8"));
//提交事务
channel.txCommit();
} catch (IOException e) {
e.printStackTrace();
//回滚事务
channel.txRollback();
//消息的重试机制(3次) + 消息的补偿机制
}
10.2 RabbitMQ的Publish confirmer机制
10.2.1 同步模式
//publish confirm - 同步模式
//设置channel为confirm模式
channel.confirmSelect();
//发送消息
String msg = "Hello RabbitMQ!!!!";
channel.basicPublish("demo_exchange", "", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("utf-8"));
boolean flag = true;
try {
if (!channel.waitForConfirms()) {
//消息发布失败
flag = false;
}
} catch (InterruptedException e) {
e.printStackTrace();
//消息发送失败
flag = false;
}
if (!flag){
//进行消息重试 + 消息补偿机制
}
10.2.2 异步模式
//publish confirm - 异步模式
//设置channel为confirm模式
channel.confirmSelect();
//设置confirm的异步模式的监听回调
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//确认达到rabbitmq
//deliveryTag - 消息id
//multiple - 批量 true批量确认 false单条确认
System.out.println("达到rabbitmq的消息:" + deliveryTag + " 批量?" + multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//未确认消息达到rabbitmq
//deliveryTag - 消息id
//multiple - 批量 true批量未到达 false单条未到达
//进行消息的重试和补偿
}
});
//发送消息
for (int i = 0; i < 10000; i++) {
String msg = "Hello RabbitMQ!!!!" + i;
channel.basicPublish("demo_exchange", "", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes("utf-8"));
}
十一、消费端确认、拒绝、限制机制
手动确认
channel.basicConsume("dead_queue", false, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("接收消息的时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println("消费者接收到消息:" + new String(body, "utf-8"));
//处理一个复杂的业务
//消息的手动确认
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
手动拒绝
//拒绝机制
//参数三:requeue,如果为true,表示消息继续放回rabbitmq队列中,如果未false,表示不放回去,消息变成死信消息
channel.basicNack(envelope.getDeliveryTag(), false, true);
消息的数量限制
//限制消息的数量
channel.basicQos(100);
channel.basicConsume("dead_queue", false, new DefaultConsumer(channel){
....
}
十二、RabbitMQ的消息补偿机制
RabbitMQ是没有提供补偿机制的,需要开发者手动维护
十三、消费端的幂等问题
各种确认机制、补偿机制、重试机制是完全有可能造成消息重复发送的,所以实际开发过程中,一定要保证消费端的幂等性。
解决方法:可以是悲观锁和乐观锁最常用的还是分布式锁
十四、RabbitMQ和SpringBoot整合
消费者和提供者都需要添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
消息的提供者:
1)配置application.yml
spring:
rabbitmq:
host: 192.168.195.188
port: 5672
username: guest
password: guest
virtual-host: /
2)创建RabbitMQ的交换机
@Configuration
public class RabbitMQConfiguration {
@Bean
public DirectExchange getExchange(){
return new DirectExchange("hotal_exchange", true, false);
}
}
3)发布消息到交换机
@Autowired
private RabbitTemplate rabbitTemplate;
//广播“酒店添加”的事件
rabbitTemplate.convertAndSend("hotal_exchange", "hotal_insert", entity);
注意:Spring管理的RabbitMQ对象,默认是懒加载,如果不发送消息,交换机就不会创建
消息的接收者:
1)配置RabbitMQ的连接配置
2)创建队列、交换机并且进行绑定
@Configuration
public class RabbitMQConfiguration {
@Bean
public DirectExchange getExchange(){
return new DirectExchange("hotal_exchange", true, false);
}
@Bean
public Queue getQueue(){
return new Queue("city_queue", true, false, false);
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding getBinding(Queue getQueue, DirectExchange getExchange){
// return new Binding("city_queue", Binding.DestinationType.QUEUE, "hotal_exchange", "hotal_insert", null);
return BindingBuilder.bind(getQueue).to(getExchange).with("hotal_insert");
}
}
3)监听队列