RabbitMQ项目实战(持续更新)

RabbitMq项目实战

实战一:使用延迟队列和备份队列实现考试信息定时存储

需求分析:

在线考试系统:

考生考试——随机生成试卷——考生规定时间内答卷——提交试卷——计算分数——考生每道题目入库

分析:

假如考试时间为2个小时,考生生成试卷后,如果遇到掉线或者其他情况未能在规定时间内交卷的,系统默认交卷并计算成绩和题目入库。并通过微信告诉学生,您的试卷已经自动提交。

image-20210927145007058

实现方式一(不推荐):通过死信队列实现延迟队列

image-20210927225316272

将过期消息入死信队列,消费死信队列。

创建交换机和队列

package marchsoft.modules.rabbitmq.examprocess;

import lombok.Getter;

@Getter
public enum ExamProcessEnum {
    
    
    /**
     * 计算考试成绩队列(死信队列)
     */
    QUEUE_EXAM_PROCESS("exam.process.direct", "exam.process.computed", "exam.process.computed"),
    /**
     * 考级结果计算通知ttl队列
     */
    QUEUE_TTL_PROCESS_COMPUTED("exam.process.direct.ttl", "exam.process.computed.ttl", "exam.process.computed.ttl"),
    /**
     * 计算考试成绩队列(备份队列)
     */
    QUEUE_BACKUP_PROCESS("exam.process.direct.backup", "exam.process.computed.backup", ""),
    /**
     * 计算考试成绩队列(报警队列)
     */
    QUEUE_WARN_PROCESS("exam.process.direct.backup", "exam.process.computed.warn", "");



    /**
     * 交换名称
     */
    private String exchange;
    /**
     * 队列名称
     */
    private String name;
    /**
     * 路由键
     */
    private String routeKey;

   ExamProcessEnum(String exchange, String name, String routeKey) {
    
    
        this.exchange = exchange;
        this.name = name;
        this.routeKey = routeKey;
    }
}

@Configuration
public class ExamProcessConfig {
    
    


    //死信交换机
    @Bean
    public DirectExchange examProcessDirect() {
    
    
        System.out.println("创建交换机");
        return (DirectExchange) ExchangeBuilder
                .directExchange(ExamProcessEnum.QUEUE_EXAM_PROCESS.getExchange())
                .durable(true)
                .build();
    }
    //死信队列
    @Bean
    public Queue examProcessQueue() {
    
    
        return new Queue(ExamProcessEnum.QUEUE_EXAM_PROCESS.getName());
    }


    //死信队列与死信交换机进行绑定
    @Bean
    public Binding bindingExamProcessQueueToExchange(@Qualifier("examProcessQueue") Queue queue, @Qualifier("examProcessDirect") DirectExchange customExchange) {
    
    
        return BindingBuilder
                .bind(queue)
                .to(customExchange)
                .with(ExamProcessEnum.QUEUE_EXAM_PROCESS.getRouteKey());
    }


    //备份交换机
    @Bean
    public FanoutExchange examProcessBackUpFanout() {
    
    
        return (FanoutExchange) ExchangeBuilder
                .fanoutExchange(ExamProcessEnum.QUEUE_BACKUP_PROCESS.getExchange())
                .durable(true)
                .build();
    }

    //备份队列
    @Bean
    public Queue examBackUpProcessQueue() {
    
    
        return new Queue(ExamProcessEnum.QUEUE_BACKUP_PROCESS.getName());
    }


    //备份队列与备份交换机进行绑定
    @Bean
    public Binding bindingExamProcessBackUpQueueToExchange(@Qualifier("examBackUpProcessQueue") Queue queue, @Qualifier("examProcessBackUpFanout") FanoutExchange fanoutExchange) {
    
    
        return BindingBuilder
                .bind(queue)
                .to(fanoutExchange);
    }

    //警告队列
    @Bean
    public Queue examProcessWarnQueue() {
    
    
        return new Queue(ExamProcessEnum.QUEUE_WARN_PROCESS.getName());
    }

    //警告队列与备份交换机进行绑定
    @Bean
    public Binding bindingExamProcessWarnQueueToExchange(@Qualifier("examProcessWarnQueue") Queue queue, @Qualifier("examProcessBackUpFanout") FanoutExchange fanoutExchange) {
    
    
        return BindingBuilder
                .bind(queue)
                .to(fanoutExchange);
    }

    //普通队列与普通交换机进行绑定
    @Bean
    public Binding bindingExamProcessTtlQueueToExchange(@Qualifier("examProcessTtlQueue") Queue queue, @Qualifier("bindingExamProcessTtlExchangeToBackUpExchange") DirectExchange directExchange) {
    
    
        return BindingBuilder
                .bind(queue)
                .to(directExchange)
                .with(ExamProcessEnum.QUEUE_TTL_PROCESS_COMPUTED.getRouteKey());
    }


    //普通队列绑定死信交换机
    @Bean
    public Queue examProcessTtlQueue() {
    
    
        Map<String, Object> args = new HashMap<>(2);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", ExamProcessEnum.QUEUE_EXAM_PROCESS.getExchange());
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", ExamProcessEnum.QUEUE_EXAM_PROCESS.getRouteKey());
        //druable 持久化  后面输入队列的名称
        return QueueBuilder.
                durable(ExamProcessEnum.QUEUE_TTL_PROCESS_COMPUTED.getName())
                .withArguments(args)
                .build();
    }


    //普通交换机绑定备份交换机
    @Bean
    public DirectExchange bindingExamProcessTtlExchangeToBackUpExchange() {
    
    
        //普通交换机绑定备份交换机
        return (DirectExchange) ExchangeBuilder.directExchange(ExamProcessEnum.QUEUE_TTL_PROCESS_COMPUTED.getExchange())
                .durable(true)
                .withArgument("alternate-exchange", ExamProcessEnum.QUEUE_BACKUP_PROCESS.getExchange())
                .build();
    }


}

生产者

@Component
@Slf4j
public class ExamProcessSender {
    
    

    @Autowired
    private RabbitTemplate rabbitTemplate;


    public void sendMessage(Long recordId, long delayTimes) {
    
    
        //延长五分钟。
        long waitMinute = 1000 * 60 * 5;
        long delayTime = waitMinute + 1000 * delayTimes;
        //给延迟队列发送消息
        rabbitTemplate.convertAndSend(ExamProcessEnum.QUEUE_TTL_PROCESS_COMPUTED.getExchange(), ExamProcessEnum.QUEUE_TTL_PROCESS_COMPUTED.getRouteKey(), recordId, new MessagePostProcessor() {
    
    
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
    
    
                //给消息设置延迟毫秒值
                message.getMessageProperties().setExpiration(String.valueOf(delayTime));
                return message;
            }
        }, new CorrelationData(UUID.randomUUID().toString()));
        log.info("操作人:{},向消息队列中发送了一场考试记录,id为:{},消息过期时间为:{}", SecurityUtils.getCurrentUserId(), recordId, delayTime);
    }

}


发送具体业务
        @Override
    @Transactional(rollbackFor = Exception.class)
    public void sureBeginExam(Long recordId) {
    
    
        //isSendQueue 如果确定考试,考试信息入队列。
        Map<String, Long> stringLongMap = judgeOperateStatus(recordId);
        Long examTimeLimit = stringLongMap.get("examTimeLimit");
        EExamRecord examRecord = new EExamRecord();
        examRecord.setId(recordId);
        examRecord.setBeginTime(LocalDateTime.now());
        examRecord.setExamStatus(ExamStatusEnum.IN_ANSWER.getCode());
        //清除考试记录缓存
        ExamCacheUtils.clearExamRecordCache(recordId);
        if (eExamRecordMapper.updateById(examRecord) <= 0) {
    
    
            BaseUtils.errorLog(ResultEnum.UPDATE_OPERATION_FAIL, "禁止考试,请登录重试", recordId);
        }
        //消息队列发送一条消息
        examProcessSender.sendMessage(recordId, examTimeLimit);
    }

消费者

@Component
@Slf4j
public class ExamProcessCustomer {
    
    

    @Autowired
    private ExamTaskService examTaskService;


    //死信队列,存储过期的消息
    @RabbitListener(queues = "exam.process.computed")
    public void handleExamProcessComputed(Long recordId) {
    
    
        log.info("进入消费队列, recordId:{}",recordId);
        examTaskService.completeExamByMq(recordId);
        log.info("exam process recordId:{}", recordId);
    }


    //备份队列
    @RabbitListener(queues = "exam.process.computed.backup")
    public void handleExamBackUpComputed(Long recordId) {
    
    
        log.info("进入备份队列, recordId:{}",recordId);
        //todo 问题:当我为消息设置过期时间时,如果消息进入备份队列,会忽略过期时间。
        //examTaskService.completeExamByMq(recordId);

        log.info("exam process recordId:{}", recordId);
    }
    //报警队列
    @RabbitListener(queues = "exam.process.computed.warn")
    public void handleExamWarnComputed(Long recordId) {
    
    
        log.info("进入报警队列, recordId:{}",recordId);
    }
}

消费具体业务
     @Override
    public void completeExamByMq(Long recordId) {
    
    
        //1.判断考试是否进行算分
        //  1.1 已算分
        //    1.1.1判断考试是否入库   入库:不做任何处理  未入库:入库
        //  1.2 未算分
        //    1.2.1进行算分   并提示用户已进行算分。
        log.info(StrUtil.format("【计算考试成绩(消息)】考试记录id:{}", recordId));
        EExamRecord examRecord = eExamRecordMapper.selectById(recordId);
        if (ObjectUtil.isNull(examRecord)) {
    
    
            BaseUtils.errorLog(ResultEnum.UPDATE_OPERATION_FAIL, "数据不存在", recordId);
            return;
        }
        if (examRecord.getExamStatus().equals(ExamStatusEnum.SOCER_ANSWER.getCode())) {
    
    
            log.info(StrUtil.format("【成绩已计算,无需重复计算】考试记录id:{}", recordId));
            if (!examRecord.getDetailEnter()) {
    
    
                //考试信息入库
                this.examDetailToDataBase(examRecord);
                log.info(StrUtil.format("【考试详情入库(消息)】考试记录id:{}", recordId));
            } else {
    
    
                return;
            }
        } else if (!examRecord.getExamStatus().equals(ExamStatusEnum.DATA_LOSE)) {
    
    
            //计算成绩
            this.computedUserExamScore(examRecord);
            //考试信息入库
            this.examDetailToDataBase(examRecord);
        }
    }

消息/交换机未接收回调

注意:因为我在这里配置了备份交换机,当交换机向队列发送消息出现问题时直接走的备份交换机,并没有走回调。但是当消息向交换机传递出问题时,此时无法转备份交换机,会走回调。

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    
    
    @Autowired
    private RabbitTemplate rabbitTemplate;


    //将创建的消息接收的回调对象添加到rabbitTemplate中。
    @PostConstruct
    public void init() {
    
    
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 交换机确定是否收到消息的回调方法
     * 1.发消息  交换机成功接受到了  回调
     * 1.1CorrelationData保存回调消息的ID及相关信息
     * 1.2交换机收到消息   ack:true
     * 1.3cause 失败的原因  cause:null
     * 2.发消息  交换机没有成功接收   回调
     * 2.1CorrelationData保存回调消息的ID及相关信息
     * 2.2交换机收到消息   ack:false
     * 2.3 cause:失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
        System.out.println(correlationData);
        String id = correlationData == null ? "" : correlationData.getId();
        if (ack) {
    
    
            log.info("交换机已经收到 id 为:{}的消息", id);
        } else {
    
    
            log.info("交换机还未收到 id 为:{}消息,由于原因:{}", id, cause);
        }

    }

    //当消息无法路由的时候的回调方法
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String
            exchange, String routingKey) {
    
    
        log.error(" 消 息 {}, 被 交 换 机 {} 退 回 , 退 回 原 因 :{}, 路 由 key:{}", new
                String(message.getBody()), exchange, replyText, routingKey);
    }
}

回调

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    
    
    @Autowired
    private RabbitTemplate rabbitTemplate;


    //将创建的消息接收的回调对象添加到rabbitTemplate中。
    @PostConstruct
    public void init() {
    
    
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 交换机确定是否收到消息的回调方法
     * 1.发消息  交换机成功接受到了  回调
     * 1.1CorrelationData保存回调消息的ID及相关信息
     * 1.2交换机收到消息   ack:true
     * 1.3cause 失败的原因  cause:null
     * 2.发消息  交换机没有成功接收   回调
     * 2.1CorrelationData保存回调消息的ID及相关信息
     * 2.2交换机收到消息   ack:false
     * 2.3 cause:失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
        System.out.println(correlationData);
        String id = correlationData == null ? "" : correlationData.getId();
        if (ack) {
    
    
            log.info("交换机已经收到 id 为:{}的消息", id);
        } else {
    
    
            log.info("交换机还未收到 id 为:{}消息,由于原因:{}", id, cause);
        }

    }

    //当消息无法路由的时候的回调方法
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String
            exchange, String routingKey) {
    
    
        log.error(" 消 息 {}, 被 交 换 机 {} 退 回 , 退 回 原 因 :{}, 路 由 key:{}", new
                String(message.getBody()), exchange, replyText, routingKey);
    }
}

问题:

使用上述方式,我们实现了所需的需求,但是遇到了两个问题。

问题1:消息过期时间不同造成消息阻塞

我们将死信队列当作延迟队列使用,出现的问题是如果消息的过期时间不同时,假如第一条消息10分钟到期,第二条消息2分钟到期,此时会出现第一条消息没有过期而阻塞第二条消息消费的情况。

如果每一场考试的时间相同还好,但是如果时间不同,那么就会造成消息阻塞的情况,因此使用此种发似实现并不是最优解。

问题2:带有过期时间的消息入备份队列,过期时间不生效

rabbitmq为一个普通交换机设置了备份交换机,我向普通交换机发送了一条5分钟后进行消费的消息,但是普通交换机出现问题走了备份交换机,但是备份交换机的消费者直接把信息消费了,并没有等5分钟,导致我这场考试直接被提交了。

实现方式二(推荐):使用延迟插件

使用方式一实现时,出现最明显的一个问题就是消息阻塞的问题。因此使用延迟插件,可以很好地解决这个问题。

image-20210928210611834

注意:延迟插件的使用需要安装。

创建交换机和队列

@Getter
public enum UnCommitExamEnum {
    
    
    /**
     * 计算考试成绩队列(死信队列)
     */
    UNCOMMIT_EXAM_COMPUTED("uncommit.exam.computed.direct", "uncommit.exam.computed", "uncommit.exam.computed");


    /**
     * 交换名称
     */
    private String exchange;
    /**
     * 队列名称
     */
    private String name;
    /**
     * 路由键
     */
    private String routeKey;

    UnCommitExamEnum(String exchange,String name,String routeKey) {
    
    
        this.exchange = exchange;
        this.name = name;
        this.routeKey = routeKey;
    }
}
@Configuration
public class UnCommitExamConfig {
    
    

    @Bean
    public CustomExchange examComputedExchange() {
    
    
        Map<String, Object> arguments = new HashMap<>();
        //设置自定义交换机的类型。
        arguments.put("x-delayed-type", "direct");
        //1.交换机名称
        //2.交换机的类型
        //3.是否需要持久化
        //4.是否需要自动删除
        //5.其他参数
        return new CustomExchange(UnCommitExamEnum.UNCOMMIT_EXAM_COMPUTED.getExchange(), "x-delayed-message", true, false, arguments);
    }

    @Bean
    public Queue examComputedQueue() {
    
    
        return QueueBuilder
                .durable(UnCommitExamEnum.UNCOMMIT_EXAM_COMPUTED.getName())
                .build();
    }

    @Bean
    public Binding bindingExamComputedQueueToExchange() {
    
    
        return BindingBuilder
                .bind(examComputedQueue())
                .to(examComputedExchange())
                .with(UnCommitExamEnum.UNCOMMIT_EXAM_COMPUTED.getRouteKey())
                .noargs();
    }

}

生产者

@Component
@Slf4j
public class UnCommitExamSender {
    
    
    @Autowired
    private RabbitTemplate rabbitTemplate;


    public void sendMessage(Long recordId, long examTime) {
    
    
        int oneMinute = 1000 * 60;
        int waitMinute =oneMinute * 5;
        //过期时间 原有时间上推后五分钟。
        long delayTime =(long) 1000 * examTime + waitMinute;
        //延迟插件最大的时间限制,设置时间超过这个时间将没有延迟效果。
        long maxMinute =(long) oneMinute * 60 * 24 * 45;
        if (delayTime > maxMinute) {
    
    
            log.info("考试:{},时间较长,不提供消息支持。考试时间:{}", recordId, delayTime);
            return;
        }
        //过期时间
        int examComputedTime = (int)delayTime;
        //给延迟队列发送消息
        rabbitTemplate.convertAndSend(UnCommitExamEnum.UNCOMMIT_EXAM_COMPUTED.getExchange(), UnCommitExamEnum.UNCOMMIT_EXAM_COMPUTED.getRouteKey(), recordId, new MessagePostProcessor() {
    
    
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
    
    
                //给消息设置延迟毫秒值
                message.getMessageProperties().setDelay(examComputedTime);
                return message;
            }
        }, new CorrelationData(UUID.randomUUID().toString()));
        log.info("操作人:{},向消息队列中发送了一场考试记录,id为:{},消息过期时间为:{}", SecurityUtils.getCurrentUserId(), recordId, examComputedTime);
    }


}

消费者

@Component
@Slf4j
public class UnCommitExamCustomer {
    
    
    @Autowired
    private ExamTaskService examTaskService;


    //死信队列,存储过期的消息
    @RabbitListener(queues = "uncommit.exam.computed")
    public void handleExamProcessComputed(Long recordId) {
    
    
        log.info("进入考试消费队列, recordId:{}",recordId);
        examTaskService.completeExamByMq(recordId);
        log.info("exam process recordId:{}", recordId);
    }

}

遗留问题:

问题1:延迟队列(插件)如何设置备份队列

问题2:备份队列中消息直接被消费,未在消息过期后消费

rabbitmq为一个普通交换机设置了备份交换机,我向普通交换机发送了一条5分钟后进行消费的消息,但是普通交换机出现问题走了备份交换机,但是备份交换机的消费者直接把信息消费了,并没有等5分钟

问题3:为延迟队列(插件)设置未入队回调,出现提示未找到路由key,实际消息到期队列中的消息还是被消费了。

发送消息时,通过日志发现出发了回调(此处回调和实现方式一的回调一摸一样,监听全局消息入队的回调)

image-20210928211238087

到时间后,消息正常被消费。

image-20210928211334561

思考:延迟插件之所以实现延迟效果,是因为消息由交换机入队列时,消息不直接进入队列,而是存储在mnesia(一个分布式数据系统)表中,当消息到期时,则自动入队列进行消费。
ExamByMq(recordId);
log.info(“exam process recordId:{}”, recordId);
}

}




### 遗留问题:

#### 问题1:延迟队列(插件)如何设置备份队列

#### 问题2:备份队列中消息直接被消费,未在消息过期后消费

rabbitmq为一个普通交换机设置了备份交换机,我向普通交换机发送了一条5分钟后进行消费的消息,但是普通交换机出现问题走了备份交换机,但是备份交换机的消费者直接把信息消费了,并没有等5分钟

#### 问题3:为延迟队列(插件)设置未入队回调,出现提示未找到路由key,实际消息到期队列中的消息还是被消费了。

发送消息时,通过日志发现出发了回调(此处回调和实现方式一的回调一摸一样,监听全局消息入队的回调)

[外链图片转存中...(img-XlcNf06U-1632990667944)]

到时间后,消息正常被消费。

[外链图片转存中...(img-Jwn5ozQA-1632990667947)]

思考:延迟插件之所以实现延迟效果,是因为消息由交换机入队列时,消息不直接进入队列,而是存储在`mnesia`(一个分布式数据系统)表中,当消息到期时,则自动入队列进行消费。

猜你喜欢

转载自blog.csdn.net/zhang19903848257/article/details/120568952