RabbitMQ消息重复消费问题

业务背景

当添加一名员工时,给其发送入职欢迎邮件。上一章解决了消息的可靠性问题,但这又会带来新的问题,就是消息可能会被重复投递。一个员工入职了,结果收到两封入职欢迎邮件

解决思路

大致的思路是这样,首先将 RabbitMQ 的消息自动确认机制改为手动确认,然后每当有一条消息消费成功了,就把该消息的唯一 ID 记录在
Redis 上,然后每次收到消息时,都先去 Redis 上查看是否有该消息的 ID,如果有,表示该消息已经消费过了,不再处理,否则再去处理

配置文件

spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
# 将RabbitMQ 的消息自动确认机制改为手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual  
spring.rabbitmq.listener.simple.prefetch=100
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0

消费端

@Component
@Slf4j
public class MailReceiver {
    @Autowired
    JavaMailSender javaMailSender;
    @Autowired
    MailProperties mailProperties;
    @Autowired
    TemplateEngine templateEngine;
    @Autowired
    StringRedisTemplate redisTemplate;

    @RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME) //监听
    public void handler(Message message, Channel channel) throws IOException {
        Employee employee = (Employee) message.getPayload();
        log.info(employee.toString());
        MessageHeaders headers = message.getHeaders();
        Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
        String msgId = (String) headers.get("spring_returned_message_correlation");
        //处理幂等性问题
        if (redisTemplate.opsForHash().entries("mail_log").containsKey(msgId)) {
            //redis包含改key,说明该消息已被消费过
            log.info(msgId+":消息已被消费过");
            channel.basicAck(tag,false); //手动确认消息已被消费
            return;
        }
        //收到消息,发送邮件
        MimeMessage msg = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(msg);
        try {
            helper.setTo(employee.getEmail());
            helper.setFrom(mailProperties.getUsername());
            helper.setSubject("入职欢迎");
            helper.setSentDate(new Date());
            Context context = new Context();
            context.setVariable("name", employee.getName());
            context.setVariable("posName", employee.getPosition().getName());
            context.setVariable("joblevelName", employee.getJobLevel().getName());
            context.setVariable("departmentName", employee.getDepartment().getName());
            String mail = templateEngine.process("mail", context);
            helper.setText(mail, true);
            javaMailSender.send(msg);
            //发送成功,把msgId存储到redis中
            redisTemplate.opsForHash().put("mail_log",msgId,"javaboy");
            channel.basicAck(tag,false);
            log.info(msgId+":邮件发送成功");
        } catch (MessagingException e) {
            //发送失败,手动消息添加到消费队列中
            channel.basicNack(tag,false,true);
            e.printStackTrace();
            log.error("邮件发送失败:"+e.getMessage());
        }
    }
}

参考文章:https://www.javaboy.org/2020/0226/springboot-rabbitmq.html
源码地址:https://github.com/lenve/vhr

你可能感兴趣的:(spring,boot,rabbitmq,spring,boot,redis)