一、延时队列
延时队列场景:
场景:比如未付款订单,超过一定时间,系统自动取消订单并释放占有物品。
常用解决方案:
spring的schedule定时任务轮询数据库。
缺点:
消耗系统内存、增加了数据库压力、存在较大的时间误差
解决:
rabbitmq的消息TTL和死信Excahnge的结合。
1、消息的TTL(Time To Live)
- TTL就是消息的存活时间。
- RabbitaMQ 可以对队列和消息分别设置TTL。
- 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
- 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列,这个消息的死亡时间有可能不一样(不同队列的设置)。这里讲单个消息的TTL,因为它是实现延时任务的关键。可以通过设置消息的expiration字段或者x-massage-ttl属性来设置时间,两者是一样的效果。
2、Dead Letter Exchange(DLX)
- 一个消息在满足如下条件,会进入死信路由,这里是路由而不是队列,一个路由可以对应多个队列。
1)、什么是死信
- 一个消息被Consumer拒收了,并且reject方法参数里的requeue是false。也就是说不会在进入到队列里面。被其他消费者消费。
- 消息的TTL到了,消息到期了。
- 队列的长度限制满了,排在前面的消息会被丢弃或者扔在死信路由上。
Dead Letter Exchange其实就是一种普通的Exchange,和创建其他的Exchange没有什么两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Excahnge中。
我们既可以控制消息在一段时间后变成死信,又可以控制变成死信消息被路由到某一个指定的交换机,结合两者,其实就可以实现一个延时队列。
3、延时队列实现
1、设置队列的过期时间实现延时队列
2、设置消息过期时间实现延时队列
4、代码示例
@Configuration
public class MyMQConfig {
/**
* 容器中的组件,都会自动创建(RabbitMQ没有的情况)
* @return
*/
@Bean
public Queue orderDelayQueue(){
Map<String,Object> arguments = new HashMap<>();
/**
* x-dead-letter-exchange: order-event-exchange 死信路由
* x-dead-letter-routing-key: order-release-queue 路由键 当死信队列到期之后,返回一个新消费队列,由消费者进行消费
* x-message-ttl: 60000 消息到期时间
*/
arguments.put("x-dead-letter-exchange","order-event-exchange");
arguments.put("x-dead-letter-routing-key","order.release.order");
arguments.put("x-message-ttl",60000);
return new Queue("order.delay.queue", true, false, false,arguments);
}
@Bean
public Queue orderReleaseQueue(){
return new Queue("order.release.queue",true,false,false);
}
@Bean
public Exchange orderEventExchange(){
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateBinding(){
return new Binding("order.delay.queue", Binding.DestinationType.QUEUE,"order-event-exchange","order.create.order",null);
}
@Bean
public Binding orderReleaseBinding(){
return new Binding("order.release.queue", Binding.DestinationType.QUEUE,"order-event-exchange","order.release.order",null);
}
}
生产者生产消息
@Test
void sendMessage(){
UserInfo userInfo = new UserInfo("aioadmin","123456");
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order", userInfo);
}
消费者接收消息
@Component
@RabbitListener(queues = {"order.release.queue"})
public class RabbitMqService {
@RabbitHandler
public void receiveMessage(Message message, UserInfo userInfo, Channel channel) {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
channel.basicAck(deliveryTag, false);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println(userInfo);
}
}
二、消息丢失、重复、积压解决方案
1、消息丢失
- 消息发送出去,由于网络问题没有抵达服务器。
- 做好容错方法,发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发方式。
- 做好日志记录,每个消息状态是否都被服务收到都应该记录
- 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息,重新发送。
- 消息抵达broker,broker要将消息写入磁盘(持久化)才算成功,此时broker尚未持久化完成,宕机。
- publisher也必须加入确认回调机制,确保成功的消息,修改数据库消息状态。
- 自动ACK的状态下,消费者收到消息,但没来得及消费然后宕机。
- 一定开启手动ACK,消费成功才移出,失败或没来得及处理就noAck并重新入列。
2、消息重复
- 消息消费成功,事务已提交ack时,机器宕机。导致ack没有成功,Broker消息重新由unack变为ready,并发送给其他消费者
- 消息消费失败,由于重试机制,自动又将消息发送出去。
- 成功消费,ack时宕机。消息由unack变为ready,broker又重新发送。
- 消费者的业务消费接口应该设计为幂等性的,比如扣库存有工作单的状态标志。
- 使用防重表,发送消息每一个都有业务的唯一标识,处理过就不用处理。
- rabbitMQ的每一条消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
3、消息积压
- 消费者宕机积压
- 消费者消费能力不足积压
- 发送者发送流量太大积压
- 上线更多的消费者,进行正常消费
- 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢