RabbitMQ的使用
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件),它一种程序对程序的通信方法,严格的遵循AMQP协议,高级消息队列协议,帮助我们在进程之间传递异步消息。
一:RabbitMQ安装(linux下)
1、在opt文件夹下创建docker_rabbitmq文件夹
2、创建docker-compose.yml 文件,添加下面的内容
version: "3.1"
services:
rabbitmq:
image: daocloud.io/library/rabbitmq:management
restart: always
container_name: rabbitmq
ports:
- 5672:5672
- 15672:15672
volumes:
- ./data:/var/lib/rabbitmq
3、在docker_rabbitmq文件夹下使用命令docker-compose up -d启动RabbitMQ
详细步骤如下:
[root@localhost ~]# cd /opt
[root@localhost opt]# mkdir docker_rabbitmq
[root@localhost opt]# cd docker_rabbitmq/
[root@localhost docker_rabbitmq]# vi docker-compose.yml
[root@localhost docker_rabbitmq]# docker-compose up -d
Creating rabbitmq ... done
[root@localhost docker_rabbitmq]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e319dc75bf44 daocloud.io/library/rabbitmq:management "docker-entrypoint.s…" 30 seconds ago Up 28 seconds 4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 15671/tcp, 15691-15692/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp rabbitmq
4、登录管理页面,用户名和密码默认都是guest↓
5、登录成功,看到管理界面的样子,更加说明mq已经安装成功,能供别人访问了↓
**二:SpringBoot整合RabbitMQ
1**、创建一个springboot工程,导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、在application.yml配置文件配置mq五大参数即加上虚拟主机
spring:
rabbitmq:
host: 10.20.159.25
port: 5672
username: test
password: test
virtual-host: /test
3、声明exchange、queue,并且绑定它们
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
//1. 创建exchange - topic
@Bean
public TopicExchange getTopicExchange(){
return new TopicExchange("boot-topic-exchange",true,false);
}
//2. 创建queue
@Bean
public Queue getQueue(){
return new Queue("boot-queue",true,false,false,null);
}
//3. 绑定在一起
@Bean
public Binding getBinding(TopicExchange topicExchange, Queue queue){
return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*");
}
}
4、测试类发布消息到RabbitMQ
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!");
}
}
5、创建消费者监听消息
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(Object message){
System.out.println("接收到消息:" + message);
}
}
手动Ack
执行一个任务可能需要花费几秒钟,你可能会担心如果一个消费者在执行任务过程中挂掉了。一旦RabbitMQ将消息分发给了消费者,就会从内存中删除。在这种情况下,如果正在执行任务的消费者宕机,会丢失正在处理的消息和分发给这个消费者但尚未处理的消息。
但是,我们不想丢失任何任务,如果有一个消费者挂掉了,那么我们应该将分发给它的任务交付给另一个消费者去处理。
为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。
如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完成,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。
没有任何消息超时限制;只有当消费者挂掉时,RabbitMQ才会重新投递。即使处理一条消息会花费很长的时间。
消息应答是默认打开的。我们通过显示的设置autoAsk=true可关闭这种机制。现即自动应答开,一旦我们完成任务,消费者会自动发送应答。通知RabbitMQ消息已被处理,可以从内存删除。如果消费者因宕机或链接失败等原因没有发送ACK(不同于ActiveMQ,在RabbitMQ里,消息没有过期的概念),则RabbitMQ会将消息重新发送给其他监听在队列的下一个消费者。
6、为了消息在消费过程中,服务器出现宕机,导致消息丢失,配置手动ack
# 记不住就抄,或者打个ack选择简单哪个即可↓
spring:
rabbitmq:
host: 10.20.159.25
port: 5672
username: test
password: test
virtual-host: /test
#手动ack的配置
listener:
simple:
acknowledge-mode: manual
7、代码手动ack,修改消费者的监听方法
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class Consumer {
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
System.out.println("接收到消息:" + msg);
//int i = 1 / 0;
//手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
为啥要改为手动ack?因为如果是自动ack,消费者即使出了异常,没有正常完成任务,mq也收到自动应答表示完成了,就删除mq中的消息,但是如果改为手动ack配置,当消费者出现异常就中断了,没有走后面手动ack的代码,就没有正确应答,mq不会把消息删除,如果消费者没有出现异常,即调用手动ack代码,给mq应答正常,删除消费消息
三、消息的可靠性
RabbitMQ的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,后面定时再次发送当前消息。事务的操作,效率太低,加了事务操作后,比平时的操作效率至少要慢100倍。
RabbitMQ除了事务,还提供了Confirm的确认机制(生产者到交换机)和return返回机制(交换机到队列),这个效率比事务高很多
1、在application配置文件中加入,确认和返回机制配置
spring:
rabbitmq:
host: 10.20.159.25
port: 5672
username: test
password: test
virtual-host: /test
#手动ack
listener:
simple:
acknowledge-mode: manual
#确认和返回机制
publisher-confirm-type: simple
publisher-returns: true
2、开启Confirm和Return的Java代码配置↓
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct // init-method,创建本类对象后设置回调来触发下面确认方法和返回方法的调用
public void initMethod(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
System.out.println("消息已经送达到Exchange");
}else{
System.out.println("消息没有送达到Exchange");
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("消息没有送达到Queue");
}
}
3、生产者需要开启监听,来观察效果;消费者不需要改动
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() throws IOException {
//rabbitTemplate.convertAndSend("boot-topic-exchange",
// "slow.red.dog","红色大狼狗!!");
//生产者增加System.in.read()来不停止好不断观察效果,消费者无需改动↓
//System.in.read();
//生产者故意设置不对的路由键找不到对应的队列来触发返回机制回调↓
rabbitTemplate.convertAndSend("boot-topic-exchange",
"slow.white.dog","红色大狼狗!!");
//生产者增加System.in.read()来不停止好不断观察效果,消费者无需改动↓
System.in.read();
}
}
四、避免消息重复消费
重复消费消息的原因是,消费者没有给RabbitMQ一个ack↓
为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,先将消息的id放到Redis中,比如
id-0(正在执行业务)
id-1(执行业务成功)
如果ack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。
极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、编写配置文件
spring:
rabbitmq:
host: 10.20.159.25
port: 5672
username: test
password: test
virtual-host: /test
#手动ack
listener:
simple:
acknowledge-mode: manual
#confirm和return机制
publisher-confirm-type: simple
publisher-returns: true
redis:
host: 10.20.159.25
port: 6379
3、修改生产者,发送消息时,指定messageId↓
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
class SpringbootRabbitmqApplicationTests {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() throws IOException {
CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("boot-topic-exchange",
"slow.red.dog","红色大狼狗!!",messageId);
//生产者增加System.in.read()来不停止好不断观察效果,消费者无需改动↓
System.in.read();
}
}
4、消费者,在消费消息时,根据具体业务逻辑去操作redis↓
@Autowired
private StringRedisTemplate redisTemplate;
@RabbitListener(queues = "boot-queue")
public void getMessage(String msg, Channel channel, Message message) throws IOException {
//0. 获取MessageId
String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation");
//1. 设置key到Redis
if(redisTemplate.opsForValue().setIfAbsent(messageId,"0",10, TimeUnit.SECONDS)) {
//2. 消费消息
System.out.println("接收到消息:" + msg);
//3. 设置key的value为1
redisTemplate.opsForValue().set(messageId,"1",10,TimeUnit.SECONDS);
//4. 手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}else {
//5. 获取Redis中的value即可 如果是1,手动ack
if("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}