RabbitMQ
一. 简介
MQ(MessageQueue)消息队列,一个队列(是一个逻辑上的存在)中存放了很多的消息,在同一个队列中这些消息是有序的。它的作用有以下几点:
- 削峰填谷;
- 系统间解耦;
- 控制订单的超时;
常用的MQ框架:ActiveMQ、ZeroMQ、QMQ(去哪儿网)、RabbitMQ、RocketMQ(阿里巴巴)、Kafka、Pulsar.
二. RabbitMQ的安装
docker run -p 15672:15672 -p 5672:5672 -d rabbitmq:3.9-management
说明:15672是rabbitmq提供的web服务器的接口;5672是rabbitmq的端口,在浏览器中输入:
http://ip:15672,使用 guest/guest 访问即可
三. RabbitMQ的运行原理
- RabbitMQ的一个服务,我们习惯性的将其称作为一个 Broker(所有的MQ都是这么来取名一个MQ的服务器的)
- RabbitMQ最核心就是Queue, 消息最终都是放到 Queue中的,每个队列都要有名字;
- 消息要达到队列必须要经过交换机,队列必须要通过binding_key绑定到交换机;
- 发送消息的时候,每个消息要指定 routing_key,交换机就根据routing_key将消息分发到对应的队列中。
- 消息的消费者,直接对应到队列。
MQ的出现就是为了解决上下游的处理速度不均衡问题的。
四. RabbitMQ的5种工作模型
4.1 简单模式
简单模型,一个消息的生产者对应着一个消息的消费者,所采用的交换机使用RabbitMQ提供的默认交换机。
public class Producer {
public static void main(String[] args) throws Exception{
Connection connection = ConnectionUtils.getConnection();
// 获取一个通道
Channel channel = connection.createChannel();
/**
* basicPublish 是发布消息:
* 第一个参数是:交换机的名字,如果是空字符串,那么使用的是 RabbitMQ自动一个默认交换机
* 第二个参数是: routing_key;
* 第三个参数是:消息的属性:后面讲
* 第四个参数是:具体的消息
*/
channel.basicPublish("", "first-queue", null, "Hello RabbitMQ".getBytes(StandardCharsets.UTF_8));
System.out.println("消息发送完毕");
}
}
public class Consumer {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
/**
* 第一个参数:队列的名称
* 第二个参数:队列是否持久化, true就是队列持久化,就是写入到磁盘中;false表示队列只存在于内容中,rabbitmq挂了,队列没了;都设置true
* 第三个参数:队列是否为排他队列,就是队列只作用与当前连接断掉了,队列就没了; 都设置为 false
* 第四个参数:表示是否自动删除, 如果队列中的消息都被消费过(不管是否确认),那么连接断开之后,那么队列就会删除;都设置为false
* 第五个参数:队里属性;
*
* 如果声明了一个队列,并且没有指定绑定到哪个交换机,那么这个队列会绑定到默认交换机,并且它的 binding_key 就是队列的名称
*/
channel.queueDeclare("first-queue", true, false, false, null);// 声明一个队列
/**
* 第一个参数是队列的名词,表示当前想消费哪个队列中的消息;
* 第二个参数如果为true, 就表示自动确认,那么队列收到消费者的确认之后,就会将消息删除掉; 往后我们都是自动确认;
* 第三个参数就是处理消息的。
*/
channel.basicConsume("first-queue", 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, Charset.defaultCharset()));
/**
System.out.println(envelope.getDeliveryTag());
// // 自己手动确认,第一个参数表示消息在MQ中的编号;第二个表示是否确认多个
channel.basicAck(envelope.getDeliveryTag(), false);
*/
}
});
}
}
4.2 工作模式
工作模型就是相对与简单模型而言,就是多了一个消费者而已。
4.3 发布订阅模型(fanout模型)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s3SAJLrd-1680076110918)(images/python-three.png)]
发布订阅模型,无关路由键,发送到对应交换机的消息会到达所有绑定到该交换机的队列上。
4.4 直连模型(routing 模型)
RabbitMQ的直连模型是最标准的一种工作模型。
4.5 topic模式
fanout是到达所有的队列;direct只能到达某个队列;如果想让消息到达部分队列,那么就使用 topic模型。topic模型中有两个特殊符号:
- #,表示0到多个;
- *,表示有且只有一个;
五. RabbitMQ与spring整合
第一步,引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
第二步,引入该依赖之后,会在IOC容器中存在一个 RabbitTemplate 这样一个 bean,用来发送消息的;
5.1 简单模型与工作模型
// 简单和工作模型,不需要指定交换机
@RabbitListener(queuesToDeclare = @Queue(name = "work-queue"))
public void consume(String msg) throws InterruptedException {
System.out.println("One: " + msg + "; " + new Date());
}
5.2 fanout模型
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "fanout-exchange", type = ExchangeTypes.FANOUT),
value = @Queue("error-queue")
))
public void consume(String msg) {
System.out.println("Error: " + msg);
}
}
5.3 direct模型
RabbitMQ的交换机默认类型就是 direct 模型
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "direct-exchange", type = ExchangeTypes.DIRECT), // 交换的类型默认是 direct模式,可以不用写
value = @Queue("error-queue"),
key = {"error", "fatal"}
))
public void consume(String msg) {
System.out.println("Error: " + msg);
// insert user
}
5.4 topic模型
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "topic-exchange", type = ExchangeTypes.TOPIC),
value = @Queue("company-queue"),
key = "company.#"
))
public void consume(String msg) {
System.out.println("Company: " + msg);
}
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "topic-exchange", type = ExchangeTypes.TOPIC),
value = @Queue("java-queue"),
key = "company.java.*"
))
public void consume(String msg) {
System.out.println("Java: " + msg);
}
5.5 springboot整合MQ的消息确认方式
springboot在整合MQ的时候,使用的是手动确认的方式。当消息的消费方执行完方法之后,如果没有抛出异常,发送一个 ack 的确认,MQ收到确认之后,会删除对应的消息;如果方法执行抛出了异常,那么就不会发送ack确认,会重新拉去MQ中消息。消息的确认是一条条确认的。在消息消费的方法中,不能捕获异常。
5.6 消息的抓取以及重试机制
在消费消息的时候,默认每次抓取 250 条数据,这个要依据实际情况来配置每次抓取的数据量;
因为消息在消费失败时候,会反复的去抓取数据,这会极大的影响MQ的性能,所以需要配置重试的机制;
spring:
rabbitmq:
host: 192.168.50.53
port: 5672
username: admin
password: admin
listener:
simple:
# 表示每次从 Queue中抓取几条数据
prefetch: 5
# retry是重试的意思
retry:
# 开启重试机制,如果一旦开启,在达到重试的次数之后,如果消息还是没有被消费成功,那么消息就会丢失;但是可以
# 将其放入到死信队列。
enabled: true
# 初始化时间间隔
initial-interval: 3000
# 最大时间间隔
max-interval: 60000
# 最大尝试的次数
max-attempts: 4
# 重试时间的乘法因此, 第一次间隔3s, 第二次间隔9秒,第三次间隔27s
multiplier: 3
六. 消息的重复消费
消息的消费方和MQ服务宕机的情况下,某个已经消费过的消息,没有及时的发送 ack确认,这个消息还存在于队列中,当服务恢复之后再次消费该消息,导致消息的重复消费。解决的方式是两点:
- 每个消息必须要有一个唯一值;
- 消息的消费方做消息的幂等设计;
@Component
@Transactional
public class Reconsume {
@Resource
private MqTestMapper mapper;
@Resource
private MsgInfoMapper msgInfoMapper;
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(name = "other-exchange"), // 交换的类型默认是 direct模式,可以不用写
value = @Queue("user-queue"),
key = "user"
))
public void consume(Message message) throws InterruptedException {
String correlationId = message.getMessageProperties().getCorrelationId();
// String messageKey = RABBITMQ_MESSAGE_KEY_PREFIX + correlationId;
QueryWrapper qw = new QueryWrapper();
qw.eq("msg_id", correlationId);
Object msgInfo = msgInfoMapper.selectOne(qw);
if(null == msgInfo) {
String msg = new String(message.getBody(), Charset.defaultCharset());
MqTest mqTest = JSONObject.parseObject(msg, MqTest.class);
mapper.insert(mqTest); // 业务操作,可能有很多代码
//为了解决消息的重复消费问题的
msgInfoMapper.insert(MsgInfo.builder().msgId(correlationId).build());
}
}
}
七. 死信队列
私信队列它依然是一个队列,只是它的作用决定我们将其称作一个死信队列;死信队列我们不会直接往这个队列中投递消息。
public class DeadLetterProducer {
private static final String DEAD_LETTER_QUEUE = "dead_letter_queue";
private static final String DEAD_LETTER_ROUTING_KEY = "dead_letter_routing_key";
private static final String DEAD_LETTER_EXCHANGE = "dead_letter_exchange";
private static final String COMMON_EXCHANGE = "common_exchange";
private static final String COMMON_QUEUE = "common_queue";
private static final String COMMON_ROUTING_KEY = "common_routing_key";
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
// 创建一个死信队列
channel.queueDeclare(DEAD_LETTER_QUEUE, true, false, false, null);
// 创建一个死信交换机
channel.exchangeDeclare(DEAD_LETTER_EXCHANGE, BuiltinExchangeType.DIRECT);
// 将死信队列绑定到死信交换机上
channel.queueBind(DEAD_LETTER_QUEUE, DEAD_LETTER_EXCHANGE, DEAD_LETTER_ROUTING_KEY);
// 创建一个常规的交换机
channel.exchangeDeclare(COMMON_EXCHANGE, BuiltinExchangeType.DIRECT);
Map<String, Object> params = ImmutableMap.of(
"x-dead-letter-exchange", DEAD_LETTER_EXCHANGE,
"x-dead-letter-routing-key", DEAD_LETTER_ROUTING_KEY
);
// 创建一个常规队列
channel.queueDeclare(COMMON_QUEUE, true, false, false, params);
// 将常规队列绑定到常规交换机上
channel.queueBind(COMMON_QUEUE, COMMON_EXCHANGE, COMMON_ROUTING_KEY);
AMQP.BasicProperties bp = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // 2表示持久化
.expiration("15000") // 表示消息在15s之内没有被消费,就自动进入到死信队列
.build();
channel.basicPublish(COMMON_EXCHANGE, COMMON_ROUTING_KEY, bp,"消息".getBytes(StandardCharsets.UTF_8));
}
}
7.1 死信队列的应用场景
- 创建订单的时候,将订单投递到队列中中,设置一个超时时间,但是不要消费该队列中的消息;
- 给队列设置死信队列;
- 当订单超时后,消息会自动达到死信队列,在死信队列中用一个消费者;
- 死信队列的消费者,在获取到订单之后,判断订单是否支付,如果支付了啥都不做;如果没之后,取消订单;
八. RabbitMQ的消息的可靠性投递
- 在投递消息之前,将消息在本地存储一份;
- 开头投递消息;
- 如果MQ通过消息确认机制告知我们,如果成功到达队列就将消息删除;如果没有成功到达队列,就啥也不干;
- 引入消息补偿系统,其实就是通过定时任务,扫描消息表,进行消息的重投;
九. 接口的幂等设计
其他
- 系统永远以管理身份运行:https://baijiahao.baidu.com/s?id=1725635393301181756&wfr=spider&for=pc
bitMQ的消息的可靠性投递