微人事-邮件发送模块-rabbitmq可靠性的优化

3 篇文章 0 订阅
2 篇文章 0 订阅
前言

邮件模块的功能:当 hr 向系统中录入一个员工时,录入成功后,系统会自动向消息中间件 RabbitMQ 发送一条消息,这条消息包含了新入职员工的基本信息,然后 mailserver 则专门用来从 RabbitMQ 上消费消息,根据收到的消息,自动的发送一封入职欢迎邮件。
当然,在理想情况下邮件一定可以发送成功,但是一旦到生产环境下,就会有很多不可控的元素比如网络抖动怎么办?如何确保消息的可靠性?

rabbitmq优化(简说)

(1)数据库建立邮件发送日志表

public class MailSendLog {
    private Integer id;
    private String msgId;//消息的唯一标识符
    private Integer empId;//每个员工的id
    private Integer status; // 0投递中 1投递成功 2投递失败
    private String routeKey;//队列的健值名
    private String exchange;//交换机的名字
    private Integer count; // 重试次数
    private Date tryTime; // 重试时间(一般在创建时间+1分钟)
    private Date createTime;//创建时间
    private Date updateTime;//重新发送时会进行更新

    // getter/setter
}

(2)设置RabbitConfig

RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
        // 消息发送到交换机的回调
        rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
            String msgId = data.getId();
            if (ack) {
                logger.info(msgId + ":消息发送成功");
                mailSendLogService.updateMailSendLogStatus(msgId, 1);//修改数据库中的记录,消息投递成功
            } else {
                logger.info(msgId + ":消息发送失败");
            }
        });
        // 消息从交换机发送到队列的回调
        rabbitTemplate.setReturnCallback((msg, repCode, repText, exchange, routingkey) -> {
            logger.info("消息发送失败");
        });
        return rabbitTemplate;
## 开启 confirm 回调
spring.rabbitmq.publisher-confirms=true
## 开启 return 回调
spring.rabbitmq.publisher-returns=true

(3)发送成功的消息status已经置为1,数据库默认是0,那如果发送失败,则采用轮询的方式重新发送。

 @Scheduled(cron = "0/10 * * * * ?")
    public void mailResendTask() {
        //select * from mail_send_log where status=0 and tryTime < sysdate()
        List<MailSendLog> logs = mailSendLogService.getMailSendLogsByStatus();
        if (logs == null || logs.size() == 0) {
            return;
        }
        logs.forEach(mailSendLog->{
            if (mailSendLog.getCount() >= 3) {
                //重试了3次后还是失败,直接设置该条消息发送彻底失败
                mailSendLogService.updateMailSendLogStatus(mailSendLog.getMsgId(), 2);
            }else{
                //重新发送
                mailSendLogService.updateCount(mailSendLog.getMsgId(), new Date());
                Employee emp = employeeService.getEmployeeById(mailSendLog.getEmpId());
                rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp,
                        new CorrelationData(mailSendLog.getMsgId()));
            }
        });
    }

(4)上面的问题解决的是发送的可靠性,下面要解决消费的可靠性,可能由于网络延迟等原因,一条消息被重复消费了多次,这样会造成多封入职邮件被发送给员工。
(5)这种情况被叫做MQ消费者的幂等性问题,解决方法也有多种,这里采用了redis,主要思路是你需要让生产者发送每条数据的时候,里面加一个全局唯一的id,类似订单id之类的东西(我们这里是msgId),然后你这里消费到了之后,先根据这个id去比如redis里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个id写进redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。具体实现如下

 @RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
    public void handler(Message message, Channel channel) throws IOException {
        Employee employee = (Employee) message.getPayload();
        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,说明该消息已经被消费过
            logger.info(msgId + ":消息已经被消费");
            channel.basicAck(tag, false);//确认重复的消息已消费,如果直接return掉,则消息还回到队列中
            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);
            redisTemplate.opsForHash().put("mail_log", msgId, "javaboy");
            //spring.rabbitmq.listener.simple.acknowledge-mode=manual 配置完必须手动确认
            channel.basicAck(tag, false);
            logger.info(msgId + ":邮件发送成功");
        } catch (MessagingException e) {
            //消息确认发送失败
            /*
            	手动确认消息消费失败
                b 是否批处理
                b1 是否返回到队列里还是直接舍弃
             */
            channel.basicNack(tag, false, true);
            e.printStackTrace();
            logger.error("邮件发送失败:" + e.getMessage());
        }
    }
spring.rabbitmq.listener.simple.acknowledge-mode=manual
##ACK确认模式 三种:不确认、自动确认、手动确认
spring.rabbitmq.listener.simple.prefetch=100
##一个消费者最多可处理的nack消息数量
总结

以上就是rabbitmq消息中间件的优化,有问题可留言。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值