1. RabbitMQ
TTL+死信队列
TTL是RabbitMQ中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。
进入死信队列具备的条件:
- 消息被否定确认
- 消息的存活时间超过设置的最大TTL时间
- 消息队列的长度已经超过最大长度
实现思想:创建一个带过期属性的队列,不创建消费者,借助消息过期时间,当一条消息过期后成为死信,这条消息会投递给死信交换机,死信交换机则将消息发给死信队列,死信队列为普通队列,可以被消费者监听和消费。
- 创建队列时指定x-message-ttl,此时整个队列具有统一过期时间
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 6000);
//设置延迟队列绑定的死信交换机
//args.put("x-dead-letter-exchange", "delay-exchange");
//设置延迟队列绑定的死信路由键
//args.put("x-dead-letter-routing-key", "delay-route");
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
① 队列中这个属性的设置要在第一次声明队列的时候设置才有效,如果队列一开始已存在且没有这个属性,则要删掉队列再重新声明才可以。
② 队列的 ttl 只能被设置为某个固定的值,一旦设置后则不能更改,否则会抛出异常。
缺点:队列中的消息过期时间一致,由于不同的业务场景对过期时间的要求可能不同,会创建比较多的队列
- 发送消息为每个消息设置expiration,此时消息之间过期时间不同
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.expiration("6000");
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(exchangeName, routingKey, mandatory, properties, "msg body".getBytes());
缺点:消息过期不一定马上丢弃,因为rabbitmq只会对队头的消息进行扫描,只有当队列头部的消息消费后,才能对后续进行消费,如果当期队列有严重的消息积压情况,已过期的消息可能存活较长时间
如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用
延迟插件
rabbitmq-delayed-message-exchange插件官方下载地址:github.com/rabbitmq/ra…, 下载*.ez的文件
找到RabbitMQ的安装路径,将下载的插件放到plugins目录中。比如:
启用插件 使用rabbitmq-plugins enable rabbitmq_delayed_message_exchange命令启用插件
@Configuration
public class RabbitMqConfig {
/**
* 延迟交换器
*/
public final static String DELAY_EXCHANGE_NAME = "exchange_delay";
/**
* 订单队列
*/
public final static String ORDER_QUEUE_NAME = "order_delay";
/**
* 订单队列路由key
*/
public static final String ROUTING_KEY_DELAY = "routing.order.delay";
@Bean("delayOrderQueue")
public Queue delayOrderQueue() {
return new Queue(ORDER_QUEUE_NAME, true, false, false);
}
@Bean("delayExchange")
public DirectExchange delayExchange(){
DirectExchange directExchange = new DirectExchange(DELAY_EXCHANGE_NAME, true, false);
directExchange.setDelayed(true);
return directExchange;
}
//@Bean
//public CustomExchange delayExchange() {
//Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct");
//return new CustomExchange("delayedExchange", "x-delayed-message", true, false, args);
//}
@Bean
Binding bindingExchangeOrderMessage() {
return BindingBuilder.bind(delayOrderQueue()).to(delayExchange()).with(ROUTING_KEY_DELAY);
}
}
生产者
//订单超时
rabbitTemplate.convertAndSend("exchange_delay","routing.order.delay",result,message->{
message.getMessageProperties().setDelay(5000);
return message;
});
消费者
@RabbitListener(queues = "order_delay")
public void receiveMessage(Message message, Channel channel) throws Exception {
JSONObject result= JSON.parseObject(new String( message.getBody()));
log.info(" 收到订单超时消息: " + result);
//开启confirm模式,ack应答
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
延迟插件的问题作者有解释,请看链接: Delay interval predictability · Issue #72 · rabbitmq/rabbitmq-delayed-message-exchange · GitHub
作者的回复大概意思是这种设计不适合百万级别延迟消息,该插件依赖Erlang计数器,存活一段时间后会抢夺调度器资源,会随着时间累加而累加。插件的优化不是他们的优先任务。
2. RocketMQ
RocketMQ支持基于18个等级的延迟方案
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
分别对应 1~18个等级,0表示不延迟,超过最大等级按最大等级18延迟2h。不支持自定义延迟时间。
Message<String> message = MessageBuilder.withPayload("hello").build();
SendResult result = rocketMQTemplate.syncSend("sync-tags", message, 15000, 3);
实现流程:
①生产者投递消息给broker的commitLog服务。
②commitLog服务对于接收的消息判断是普通消息还是延时消息(延迟级别大于0),如果是延时消息,将实际的topic和queueId保存到message的属性中,重新设置topic为SCHEDULE_TOPIC_XXXX,根据延迟级别确定投递到那个队列下。
if (msg.getDelayTimeLevel() > 0) {
// 如果延迟级别超过最大级别,就设置延迟级别为18
if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
}
// 设置延迟消息的topic和queue
topic = ScheduleMessageService.SCHEDULE_TOPIC;
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// Backup real topic, queueId
// 将真正的topic和queueId存起来,存到property属性中
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
// 将消息的topic和queue替换为延迟队列的
msg.setTopic(topic);
msg.setQueueId(queueId);
}
③消息延迟服务(ScheduleMessageService)启动后会创建定时器扫描个自己对应延迟等级的队列,消息到期的消息,会根据存入消息的原始topic和queueId重新设置,存储到commitLog,最后投递到原来的队列中由消费者消费
/**
* 在启动broker的时候,会初始化这里的timer
* 并且会根据延迟级别,创建对应的timer任务
*/
public void start() {
if (started.compareAndSet(false, true)) {
this.timer = new Timer("ScheduleMessageTimerThread", true);
// 这里for循环,会为每一个延迟级别创建一个延迟任务
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
/**
* level是设置的延迟级别
*/
Integer level = entry.getKey();
/**
* value是延迟级别所对应的延迟时间
* 以及对应的offset 偏移量?
*/
Long timeDelay = entry.getValue();
Long offset = this.offsetTable.get(level);
if (null == offset) {
offset = 0L;
}
// 初始化时,第一次延迟时间是1S,在后面任务执行之后(DeliverDelayedMessageTimerTask),会修改任务延迟时间
if (timeDelay != null) {
this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
}
}
// 这里是每隔10s,就把延迟队列的最大消息偏移量写入到磁盘中
this.timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
if (started.get()) ScheduleMessageService.this.persist();
} catch (Throwable e) {
log.error("scheduleAtFixedRate flush exception", e);
}
}
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
}
}
每个等级开启一个定时器,用于将队列的延迟消息重新投递到原始的topic和队列
if (timeDelay != null) { this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);}
先介绍到这里,具体逻辑就暂不放,我真是太懒了,copy都懒得copy