rabbitmq 实现固定延时等级的延时消息
通过学习rocketmq延时消息的实现(指路:RocketMQ延时消息实现原理探究),我们了解到rocketmq将不同延时等级的消息放入不同queue中,然后转发到目标topic。
对于未使用rocketmq的团队来说,如果只是为了实现固定延时时间而引入一个新的中间件,成本可能比较大,包括学习使用、运维成本等。但我们也可以仿照rocketmq的方式,结合rabbitmq死信队列,使用rabbitmq实现一个类似rocketmq固定延时等级的延时消息功能。
大概介绍
大概实现方式:
- 先声明一个topic类型的死信exchange,用来转发死信(延时后)的消息。
- 定义一组用来暂存消息的延时delayQueue,并设置死信exchange,死信路由默认为消息原始路由。
- 再声明一个topic类型的延时exchange与延时queue绑定,绑定路由为
$delayQueueName.#
, 即通过routing key前缀识别应转发到哪个延时队列。 - 然后声明需要消费的业务队列 bizQueue,再将其绑定到死信exchange上,路由关系为
*.$bizQueueName
(也可以是正常的routingKey, 这个死信exchange的绑定关系就和普通定义发送消息的定义差不多了) - 最后发送消息时,根据其延时等级,在routingKey前面拼接上对应的 delayQueueName 就可以了。
消息大概流程如下
代码实现
- 先定义一个延时等级的枚举类
public enum DelayLevelEnum implements DelayLevel { SECOND_1(1000, "1s"), SECOND_5(5 * 1000, "5s"), SECOND_10(10 * 1000, "10s"), SECOND_30(30 * 1000, "30s"), MINUTE_1(60 * 1000, "1m"), MINUTE_2(2 * 60 * 1000, "2m"), MINUTE_3(3 * 60 * 1000, "3m"), MINUTE_4(4 * 60 * 1000, "4m"), MINUTE_5(5 * 60 * 1000, "5m"), MINUTE_6(6 * 60 * 1000, "6m"), MINUTE_7(7 * 60 * 1000, "7m"), MINUTE_8(8 * 60 * 1000, "8m"), MINUTE_9(9 * 60 * 1000, "9m"), MINUTE_10(10 * 60 * 1000, "10m"), MINUTE_20(20 * 60 * 1000, "20m"), MINUTE_30(30 * 60 * 1000, "30m"), HOUR_1(60 * 60 * 1000, "1h"), HOUR_2(2 * 60 * 60 * 1000, "2h") ; private final long delayTimeInMills; private final String desc; DelayLevelEnum(long delayTimeInMills, String desc) { this.delayTimeInMills = delayTimeInMills; this.desc = desc; } public long getDelayTimeInMills() { return delayTimeInMills; } public String getDesc() { return desc; } }
- 为更方便使用,还提供用户自定义延时等级
public class CustomDelayLevel implements DelayLevel { private final long delayTimeInMills; private final String desc; public CustomDelayLevel(long delayTimeInMills, String desc) { this.delayTimeInMills = delayTimeInMills; this.desc = desc; } @Override public long getDelayTimeInMills() { return delayTimeInMills; } @Override public String getDesc() { return desc; } }
- 然后对每个延时等级定义一个延时队列
private void declareCommonDelayExchangeAndQueues() { // 声明 topic exchange 作为延时exchange, 所有消息通过该 exchange 路由到对应延迟的 queue TopicExchange delayExchange = new TopicExchange(DELAY_EXCHANGE, true, false); rabbitAdmin.declareExchange(delayExchange); // 延迟后的死信 exchange, 将延迟后的消息转发到对应的业务 queue TopicExchange dlxDelayExchange = new TopicExchange(DELAY_EXCHANGE_DLX, true, false); rabbitAdmin.declareExchange(dlxDelayExchange); // 获取用户声明的延时等级bean, 和 默认的延时等级 Collection<DelayLevel> customDelayLevels = applicationContext.getBeansOfType(DelayLevel.class).values(); List<DelayLevel> delayLevels = new ArrayList<>(Arrays.asList(DelayLevelEnum.values())); delayLevels.addAll(customDelayLevels); // 声明各个延时等级的 queue, 及绑定 delay_exchange for (DelayLevel level : delayLevels) { Map<String, Object> params = new HashMap<>(); params.put("x-message-ttl", level.getDelayTimeInMills()); params.put("x-dead-letter-exchange", DELAY_EXCHANGE_DLX); // 指定死信exchange, 不指定死信routingKey params.put("x-queue-mode", "lazy"); // 声明为惰性队列, 避免消息积压占用大量内存 Queue queue = new Queue(getDelayQueueName(level), true, false, false, params); rabbitAdmin.declareQueue(queue); Binding binding = new Binding(queue.getName(), Binding.DestinationType.QUEUE, DELAY_EXCHANGE, queue.getName() + ".#", Collections.emptyMap()); rabbitAdmin.declareBinding(binding); } } public static String getDelayQueueName(DelayLevelEnum delayLevelEnum) { return DELAY_QUEUE_PREFIX + delayLevelEnum.getDesc(); }
- 声明业务队列并绑定死信exchange
其实这部分也可以自己手动声明和绑定,不过这里也提供一个声明队列并自动绑定死信exchange的方式, 方便使用/** * 延时后被投递的业务队列, 提供给业务方消费 */ public class DelayConsumQueue extends Queue { /** * 与死信exchange 绑定的 routing key, 默认为 queueName */ private String bindRoutingKey; public DelayConsumQueue(String name) { super(name); } public DelayConsumQueue(String name, boolean durable) { super(name, durable); } public DelayConsumQueue(String name, boolean durable, boolean exclusive, boolean autoDelete) { super(name, durable, exclusive, autoDelete); } public DelayConsumQueue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) { super(name, durable, exclusive, autoDelete, arguments); } public String getBindRoutingKey() { return bindRoutingKey; } public void setBindRoutingKey(String bindRoutingKey) { this.bindRoutingKey = bindRoutingKey; } }
- 然后在config中,获取到这些队列的bean,定义queue并绑定死信exchange
private void declareAndBindingBizQueues() { Collection<DelayConsumQueue> bizQueues = applicationContext.getBeansOfType(DelayConsumQueue.class).values(); // 声明业务队列,并绑定 for (DelayConsumQueue queue : bizQueues) { rabbitAdmin.declareQueue(queue); String bindingRoutingKey = queue.getBindRoutingKey(); if (bindingRoutingKey == null || bindingRoutingKey.isEmpty()) { bindingRoutingKey = queue.getName(); } Binding binding = new Binding(queue.getName(), Binding.DestinationType.QUEUE, DELAY_EXCHANGE_DLX, "*." + bindingRoutingKey, Collections.emptyMap()); rabbitAdmin.declareBinding(binding); } }
- 最后定义一个发送延时消息的类就可以了
public class RabbitDelayMsgSender { private RabbitTemplate rabbitTemplate; public RabbitDelayMsgSender(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } /** * @param routingKey 被死信exchange转发时的routingKey, 默认为 consumeQueueName */ public void sendDelayMessage(String routingKey, Object message, DelayLevelEnum delayLevelEnum) { String delayRoutingKey = getDelayQueueName(delayLevelEnum) + "." + routingKey; rabbitTemplate.convertAndSend(DELAY_EXCHANGE, delayRoutingKey, message); } public void send(String routingKey, Message message, DelayLevelEnum delayLevelEnum) { String delayRoutingKey = getDelayQueueName(delayLevelEnum) + "." + routingKey; rabbitTemplate.send(DELAY_EXCHANGE, delayRoutingKey, message); } }
大概就完成了,使用的话,大概如下
/**
* 定义一个消费队列,用来接收延时后的消息提供业务消费
*/
@Bean("testQueue")
public DelayConsumQueue testQueue(){
return new DelayConsumQueue("test-delay");
}
/**
* 自定义一个延时等级 15s
*/
@Bean("delayLevel15s")
public DelayLevel delayLevel15s() {
return new CustomDelayLevel(15000, "15s");
}
/**
* 测试发送延时消息
*/
@Resource(name = "delayLevel15s")
private DelayLevel delayLevel15;
@Test
public void testDelaySend() {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String msg = "message === " + sf.format(new Date());
rabbitDelayMsgSender.sendDelayMessage("test-delay", msg, DelayLevelEnum.SECOND_5);
rabbitDelayMsgSender.sendDelayMessage("test-delay", msg, delayLevel15);
}
源码地址:https://github.com/wsJava/rabbitmq-delay
简单分析
仿照rocketmq的方式,在发送消息时,先将其转存到对应延时等级的队列中,然后在等待消息到期(死亡)后,经由死信exchange再转发到对应的业务队列中提供消费。
优点:
- 支持同一业务消息使用不同的延时时间
- 基于rabbitmq简单封装实现,支持镜像队列复制,实现高可用
- 且延时等级方便修改增加,以适应业务需求
- 使用lazy-mode,且消息默认持久化消息,延时队列发生堆积时不会导致磁盘换页。
缺点:
- 仍旧只支持固定延时等级