上篇文章:rabbitMQ 系列 之 死信 有讲到什么是rabbitMQ的死信以及怎样实现一个延时队列(TTL方式),但是这种方式有一个缺点,就是对于非统一失效时间的事件无法及时失效,比如商品的上下架时间,对于每个商品有不同的上下架时间,那么相应的消息失效时间也不同,而TTL方式,只能在最近一条消息失效变为死信后,才能将之后的失效消息变更为死信。
像上图说明,本来我们期望进入延时队列的消息顺序为A->C->B-D,但是实际情况为只有先messageD失效后,才能是C…显然这种顺序是无法满足我们一些非统一失效时间的消息消费的需求的。
那么怎样才能达到只要失效时间到达就直接进入消费队列中而不是只有等到前置消息进入后才进入呢,这里就只能使用插件了:rabbitmq_delayed_message_exchange(要求rabbitMQ版本为3.6以上)。
过程
- 生产者将消息(message)和路由键(routingKey)发送到指定的延时交换器(exchange)上
- 消息一直存储在exchange中,直至达到消息失效时间。
- 消息失效后,exchange根据routingKey将消息路由到相应绑定的queue中
- 消费者消费queue中的消息
安装
-
下载插件
官网下载地址:https://www.rabbitmq.com/community-plugins.html
[root@coder webdata]# wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v3.8.0/rabbitmq_delayed_message_exchange-3.8.0.ez
-
docker安装rabbitmq
[root@coder webdata]# docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management Unable to find image 'rabbitmq:management' locally management: Pulling from library/rabbitmq 171857c49d0f: Pull complete 419640447d26: Pull complete 61e52f862619: Pull complete 856781f94405: Pull complete fd5f3d3bac09: Pull complete e526190d8f2c: Pull complete bcaa754c1ece: Pull complete 41118e0c01b4: Pull complete ac3f2ab39238: Pull complete cd9ffc55132f: Pull complete efec50445663: Pull complete 598675d7eebd: Pull complete fe4e66a2587e: Pull complete Digest: sha256:70a3b5de3bd8d408cde0b98382887dbb1af1ac6d42c606edccb7edc922a2fcef Status: Downloaded newer image for rabbitmq:management cf7e32deb6c3eaabfdc70dec5698bbf58102913185c7b7c7769f814a7a43d857
-
插件复制到容器内
[root@coder webdata]# docker cp /webdata/rabbitmq/rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq:/plugins
-
重启容器
[root@coder webdata]# docker restart rabbitmq rabbitmq
使用
-
pom依赖
<!--rabbitmq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
配置rabbitmq配置
spring: rabbitmq: password: domi username: domi addresses: 39.101.133.223 port: 5672 listener: # 手动ACK direct: acknowledge-mode: manual simple: acknowledge-mode: manual virtual-host: /
-
配置类编写
@Configuration public class MQConfig { @Bean public MessageConverter messageConverter() { Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); jackson2JsonMessageConverter.setCreateMessageIds(true); return jackson2JsonMessageConverter; } /** * @desc: 延时交换器,注意返回类型是CustomExchange * @return: org.springframework.amqp.core.TopicExchange * @auther: Michael Wong * @email: michael_wong@yunqihui.net * @date: 2020/9/8 18:52 * @update: */ @Bean public CustomExchange customDlxExchange() { Map<String, Object> args = new HashMap<>(); args.put("x-delayed-type", "direct"); return new CustomExchange(MQKeyStatic.EXCHANGE_GOOD_CUSTOM_DLX,"x-delayed-message",true,false,args); } /** * @desc: 商品上下架队列 * @return: org.springframework.amqp.core.Queue * @auther: Michael Wong * @email: michael_wong@yunqihui.net * @date: 2020/9/5 14:46 * @update: */ @Bean public Queue goodOnShelfDlxQueue() { return new Queue(MQKeyStatic.QUEUE_GOOD_DLX_GOOD, true, false, false, null); } @Bean public Binding dlxExchangeBindingGoodOnShelfDlx() { return BindingBuilder.bind(goodOnShelfDlxQueue()).to(customDlxExchange()).with(MQKeyStatic.ROUTING_GOOD_DLX_GOOD).noargs(); } }
-
生产者发送消息
// 延迟队列,上下架 JSONObject messageObject = new JSONObject(); messageObject.put("goodId", good.getId()); messageObject.put("onShelf", 1); long expirStart = good.getStartSellingTime().getTime() - System.currentTimeMillis(); rabbitTemplate.convertAndSend(MQKeyStatic.EXCHANGE_GOOD_CUSTOM_DLX,MQKeyStatic.ROUTING_GOOD_DLX_GOOD,messageObject, message -> { // 这里的失效时间是long类型,普通的TTL方式的类型是String类型 message.getMessageProperties().setHeader("x-delay",expirStart); return message; }); messageObject.put("onShelf", 0); long expirEnd = good.getEndSellingTime().getTime() - System.currentTimeMillis(); rabbitTemplate.convertAndSend(MQKeyStatic.EXCHANGE_GOOD_CUSTOM_DLX,MQKeyStatic.ROUTING_GOOD_DLX_GOOD,messageObject, message -> { // 这里的失效时间是long类型,普通的TTL方式的类型是String类型 message.getMessageProperties().setHeader("x-delay",expirEnd); return message; });
-
消费者消费消息
@Slf4j @Component public class MQListener { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private IGoodService goodService; /** * @desc: 商品上下架处理队列监听 * @param message * @param channel * @return: void * @auther: Michael Wong * @email: michael_wong@yunqihui.net * @date: 2020/9/9 15:59 * @update: */ @RabbitListener(queuesToDeclare = @Queue(name = MQKeyStatic.QUEUE_GOOD_DLX_GOOD, durable = "true", exclusive = "false", autoDelete = "false")) public void goodOnShelfListener(Message message, Channel channel) throws IOException { log.info("开始监听到good:{}",message); if (idempotentMessage(message, channel)) { return; } log.info(" 幂等 完成"); JSONObject messageJson = JSONObject.parseObject(new String(message.getBody())); Integer onShelf = messageJson.getInteger("onShelf"); Integer goodId = messageJson.getInteger("goodId"); try { Good good = goodService.getById(goodId); if (good == null) { log.info("null ack message"); ackMessage(message,channel); } if (onShelf == 1) { // 上架 goodService.onShelf(goodId); } else { // 下架 goodService.downShelf(goodId); } ackMessage(message,channel); } catch (Exception e) { channel.basicRecover(false); } } /** * @param message * @param channel * @desc: 幂等消息 * @return: boolean * @auther: Michael Wong * @email: michael_wong@yunqihui.net * @date: 2020/9/7 10:43 * @update: */ private boolean idempotentMessage(Message message, Channel channel) throws IOException { String messageId = message.getMessageProperties().getMessageId(); String rediskey = RedisKeyStatic.IDEMPOTENT_MESSAGE + messageId; long deliveryTag = message.getMessageProperties().getDeliveryTag(); if (stringRedisTemplate.hasKey(rediskey)) { channel.basicReject(deliveryTag, false); return true; } return false; } /** * @param message * @param channel * @desc: 确认消息公共方法 * @return: void * @auther: Michael Wong * @email: michael_wong@yunqihui.net * @date: 2020/9/5 16:51 * @update: */ private void ackMessage(Message message, Channel channel) throws IOException { try { channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); String rediskey = RedisKeyStatic.IDEMPOTENT_MESSAGE + message.getMessageProperties().getMessageId(); stringRedisTemplate.opsForValue().set(rediskey, "1", 3, TimeUnit.HOURS); } catch (Exception e) { log.error("接收消息失败,重新放回队列,message:{}", message); // 解决方案,剔除此消息,然后记录到db中去补偿 channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); } } }