前言
邮件模块的功能:当 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消息中间件的优化,有问题可留言。