消息队列总结
消息队列的使用场景
异步处理
在我们的系统中,业务越来越复杂就导致我们与别的微服务的交互越来越多,使用消息队列可以在主服务中做相应的处理之后别的例如电商里面订单的积分服务,通知服务等可以放到消息队列中后续等消费,这样能够降低响应时间
服务解耦
使用消息队列将相关消息放入消息队列,哪些服务需要消费就去订阅相关队列就行,生产消息的服务不用处理这些消息被哪些服务调用等问题,就达到了解耦的目的
削峰
例如秒杀活动,做活动时请求量很高,如果所有的请求一下落到服务中可能会导致我们的服务器或者数据库扛不住并发压力出现各种问题,所以引入消息中间件做缓冲,将请求放入消息队列中,后端服务去消费队列中的请求,超时的请求可以直接返回错误,这样就可以达到削峰的目的
引入消息队列可能出现的问题
引入消息队列会导致系统可用性降低,复杂度提高,还可能会出现一致性问题
消息队列的高可用
RabbitMQ是基于主从做高可用的,RabbitMQ有三种工作模式:单机模型、普通集群模式、镜像集群模式
单机模式
顾名思义,只在一台机器上部署RabbitMQ服务,生产上不会用
普通集群模式
普通集群模式是在多台服务器上启动多个RabbitMQ服务,我们所创建的queue的元数据会同步到所有的RabbitMQ服务中,但消息只会存储到创建queue的服务中,其他服务想要获取消息则需要根据queue的元数据找到对应的服务去拉取消息
这种方式如果存放消息的服务宕机了,那么就无法获取到消息了,即使开启了持久化机制,也要等服务恢复之后才能够继续获取消息。而且我们随机连接服务取消息会产生数据拉取的开销,只用存储数据的服务取消息会有单机性能瓶颈
镜像集群模式
镜像集群模式其实就是无论queue的元数据还是queue里面的消息,都会同步到指定的服务上,即每个服务上都会有queue的完整镜像,包含queue的全部数据信息,每次写消息到queue时都会同步到指定服务上
我们可以在RabbitMQ的后台管理页面增加一个镜像集群模式的策略,并且可以消息是同步到所有的节点还是指定数量的节点。我们新建queue时应用该策略就可以实现镜像集群模式了
- 优点:即使其中某个服务宕机了,其他的服务上还包含有queue的全部信息,还能够继续提供服务。
- 缺点:每个消息都需要同步到全部的服务上,会增加网络带宽压力;无法线性扩展queue
消息队列怎样保证幂等性
幂等性就是指一个消息消费了多次还能够保证对应数据是正确的
我们为每条消息指定一个唯一id,然后利用缓存或数据库来存储消费过的id,在消费之后判断一下该消息id是否已存在,已存在就代表已经消费过,就不再执行相应的处理了
如何保证消息的可靠性传输
以RabbitMQ举例,我们可以把这个问题分为三部分来解决:
生产者发消息到消息队列
使用事务会影响效率,所以一般使用confirm机制
RabbitMQ中我们使用 channel.confirmSelect
() 方法开启confirm机制,在开启confirm机制后,每次提交消息都会生成一个唯一id,如果消息投递成功RabbitMQ就会给客户端发送一个ACK确认,这个过程是异步的所以比着事务会大大提高效率
//创建Exchange
channel.exchangeDeclare("confirm.exchange", BuiltinExchangeType.DIRECT, true, false, new HashMap<>());
//创建Queue
channel.queueDeclare("confirm.queue", true, false, false, new HashMap<>());
//绑定路由
channel.queueBind("confirm.queue", "confirm.exchange", "confirm");
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
log.info("ack : deliveryTag = {},multiple = {}", deliveryTag, multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
log.error("nack : deliveryTag = {},multiple = {}", deliveryTag, multiple);
}
});
String msgTemplate = "测试消息[%d]";
for (int i = 0; i < 10; i++) {
channel.basicPublish("confirm.exchange", "confirm", new AMQP.BasicProperties(), String.format(msgTemplate, i).getBytes(StandardCharsets.UTF_8));
}
消息队列
为了防止消息在消息队列中丢失,我们需要同时开启队列和消息的持久化机制
- 开启队列持久化机制:在创建queue的时候设置持久化
- 开启消息持久化机制:发送消息的时候设置持久化
消息队列到消费者
在消费者处理消息的时候出现异常导致消息没有被正常处理,但是消息已经在队列中被移除了,这就会导致消息丢失
默认情况下,消费者从队列中获取消息后会自动ack,我们可以在消费者端开启手动ack模式,在业务正常处理结束之后发送ack,这样可以避免业务异常导致的消息丢失
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
try {
byte[] body = message.getBody();
String messageContent = new String(body, StandardCharsets.UTF_8);
if("error".equals(messageContent)){
throw new RuntimeException("业务异常");
}
log.info("收到的消息内容:{}",messageContent);
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}catch (Exception e){
log.info("消费消息失败!重回队列!");
channel.basicNack(message.getEnvelope().getDeliveryTag(),false,true);
}
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
log.info("取消订阅:{}",consumerTag);
}
};
channel.basicConsume("confirm.queue",false,deliverCallback,cancelCallback);
如何保证消息的顺序性
在RabbitMQ中可能出现顺序问题的情况是一个消息队列对应多个消费者,消费者从队列中取消息的顺序是对的,但是每个消费者的执行时间是不确定的,无法保证先拿到消息的消费者先完成操作,这样就有可能导致顺序问题。
解决方式是可以创建多个queue,使用多个消费者去一对一的监听queue,生产者负责将需要保证顺序的消息发到一个queue里就行。
例如我们在RabbitMQ中建立相同前缀的队列,后面跟着队列编号(例如order:00、order:01、order:02),然后使用相同数量的消费者监听对应的队列,在我们发送消息之前通过计算(例如哈希计算之后取模)来确定同一业务逻辑的消息放入同一个队列,然后消费者就可以顺序消费消息了
消息队列中的消息模型
消息队列有两种消息模型:队列模型和发布/订阅模型
队列模型
一个或多个生产者向队列中存放消息,一个或多个消费者去队列中取消息进行消费,每隔消息只能被一个消费者消费,这就叫做队列模型
发布/订阅模型
在发布订阅模型中,消息发送方称为发布者,消息消费方被称为订阅者,服务端存放消息的主体称为topic(主题),发布者将消息发送到主题中,所以订阅了该主题的消费者都可以消费这条消息。发布/订阅模型和队列模型的区别是一条消息是否能被多个消费者消费
小结
RabbitMQ采用队列模型,RocketMQ和Kafka采用的是发布/订阅模型
RabbitMQ虽然采用的是队列模型,但是也可以实现一个消息被多个消费者消费,它的方法是通过Exchange交换机将同一个消息发送到多个队列,不同的消费者监听不同的队列就可以实现了
RabbitMQ实现延迟队列
[【RabbitMQ】一文带你搞定RabbitMQ延迟队列]https://www.cnblogs.com/mfrank/p/11260355.html
RabbitMQ中是使用死信队列来实现的延迟队列
死信队列
死信(Dead Letter)是RabbitMQ中的一种机制,如果队列中的消息出现下面情况:
- 消息被否定确认,即消费者使用
channel.basicNack()
或channel.basicReject()
,并且requeue的属性设置为false - 当消息超出了消息或队列设置(同时设置取最小值)的TTL时间
- 当消息数量已经超出最大队列长度时会将队列最前的消息放入死信队列
那么该消息将会成为死信,如果该队列配置了死信队列信息,那么死信消息将会被放入死信队列,如果没有配置,则死信消息会被丢弃
死信队列其实就是一个普通队列,只是我们将它配置为某个队列的死信队列,同样的,死信交换机也是普通的交换机。为业务队列配置死信队列的示例代码如下:
// 声明业务队列A
@Bean("businessQueueA")
public Queue businessQueueA(){
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUEA_ROUTING_KEY);
return QueueBuilder.durable(BUSINESS_QUEUEA_NAME).withArguments(args).build();
}
我们可以利用死信队列机制来实现延时队列,即我们为业务队列设置TTL为半小时,并配置死信交换机和死信路由key,创建对应的死信队列与死信交换机绑定,然后创建一个消费者监听这个死信队列。在超时后消息会被放入死信队列,消费者接收到这个消息查询该订单状态如果是未审批,则向数据库插入相反持仓数据并取消这个交易然后发送邮件提醒交易员和投资经理该交易未审批请重新发起交易