背景:在用户注册或者登录的时候需要获取验证码,使用rabbitmq将需要发送验证码的消息发送到消息队列中
rabbitmq配置
@Configuration
public class RabbitMQConfig {
public static final Logger LOGGER= LoggerFactory.getLogger(RabbitMQConfig.class);
// 配置一个工作模型队列
@Bean
public Queue queueWork1() {
return new Queue("queue_work");
}
@Autowired
CachingConnectionFactory cachingConnectionFactory;
@Autowired
MailSendLogService mailSendLogService;
@Bean
public RabbitTemplate rabbitTemplate(){
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
//成功投递消息到Broker交换机站点的回调函数
rabbitTemplate.setConfirmCallback((data,ack,cause)->{
String msgId = data.getId();
if(ack){
LOGGER.info(msgId+"消息发送成功");
//修改数据库中的记录,消息发送成功,将status设为1
mailSendLogService.updateMailSendLogStatus(msgId, MailConstants.SUCCESS);
}else{
LOGGER.error(msgId+"消息发送失败!");
}
});
//消息投递到Queue队列失败的回调函数
rabbitTemplate.setReturnCallback((msg,repCode,repText,exchange,routingKey)->{
LOGGER.error(msg.getBody()+"----消息从交换机投递到队列失败!\n错误原因:"+repText);
LOGGER.error("发送错误的交换机:"+exchange+",发生错误的路由key:"+routingKey);
});
return rabbitTemplate;
}
@Bean
DirectExchange mailExchange(){
return new DirectExchange(mailExchange,true,false);
}
/**
* 验证码消息队列
* @return
*/
@Bean
Queue mailQueueVerifyCode(){
return new Queue(mailQueueVerifyCode,true);
}
}
@Bean
Binding mailQueueVerifyCodeBinding(){
return BindingBuilder.bind(mailQueueVerifyCode()).to(mailExchange()).with(mailRouteVerifyCode);
}
邮箱发送记录实体类:
public class MailSendLog implements Serializable {
private static final long serialVersionUID = 740872026109078508L;
private String msgId;
/**
* 0:反馈,1:验证码
*/
private Integer contentType;
private String content;
private Integer status;
private String routeKey;
private String exchange;
private Integer count;
private Date tryTime;
private Date createTime;
private Date updateTime;
}
获取验证码接口:
@Autowired
VerifyCodeService verifyCodeService;
/**
* 获取邮箱验证码,并保存到本次会话
* @param session
*/
@GetMapping("/admin/mailVerifyCode")
public RespBean getMailVerifyCode(HttpSession session){
String code = verifyCodeService.getVerifyCode();
//保存验证码到本次会话
session.setAttribute("mail_verify_code",code);
verifyCodeService.sendVerifyCodeMail(code);
return RespBean.ok("验证码已发送到邮箱,请注意查看!");
}
生成验证码和使用rabbitmq发送消息:
@Service("verifyCodeService")
public class VerifyCodeServiceImpl implements VerifyCodeService {
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
MailSendLogService mailSendLogService;
@Value("${mail.exchange:mail-exchange}")
private String mailExchange;
@Value("${mail.route.verifyCode:mail-route-verifyCode}")
private String mailRouteVerifyCode;
public String getVerifyCode() {
StringBuilder code=new StringBuilder();
for (int i = 0; i <4; i++) {
code.append(new Random().nextInt(10));
}
return code.toString();
}
public void sendVerifyCodeMail(String code) {
//添加消息记录
String msgId = UUID.randomUUID().toString();
MailSendLog mailSendLog = new MailSendLog();
mailSendLog.setMsgId(msgId);
mailSendLog.setContent(code);
mailSendLog.setContentType(MailConstants.VERIFY_CODE_TYPE);
mailSendLog.setCount(1);
mailSendLog.setCreateTime(new Date());
mailSendLog.setTryTime(new Date(System.currentTimeMillis()+1000*10*MailConstants.MEG_TIMEOUT));
mailSendLog.setUpdateTime(new Date());
mailSendLog.setExchange(mailExchange);
mailSendLog.setRouteKey(mailRouteVerifyCode);
mailSendLog.setStatus(MailConstants.DELIVERING);
mailSendLogService.insert(mailSendLog);
//CorrelationData 对象,每个发送的消息都需要配备一个 CorrelationData 相关数据对象,CorrelationData 对象内部只有一个 id 属性,用来表示当前消息唯一性。
rabbitTemplate.convertAndSend(mailExchange,mailRouteVerifyCode,code,new CorrelationData(msgId));
}
}
处理验证码消息,防重复消费
@Component
public class VerifyCodeReceiver {
//保证幂等性(重复消费)
//要保证消息的幂等性,这个要结合业务的类型来进行处理。下面提供几个思路供参考:
//
//1、可在内存中维护一个set,只要从消息队列里面获取到一个消息,先查询这个消息在不在set里面,如果在表示已消费过,直接丢弃;如果不在,则在消费后将其加入set当中。
//
//2、如何要写数据库,可以拿唯一键先去数据库查询一下,如果不存在在写,如果存在直接更新或者丢弃消息。
//
//3、如果是写redis那没有问题,每次都是set,天然的幂等性。
//
//4、让生产者发送消息时,每条消息加一个全局的唯一id,然后消费时,将该id保存到redis里面。消费时先去redis里面查一下有么有,没有再消费。
@Autowired
JavaMailSender javaMailSender;
@Autowired
StringRedisTemplate redisTemplate;
private static final Logger LOGGER= LoggerFactory.getLogger(VerifyCodeReceiver.class);
@RabbitListener(queues = "${mail.queue.verifyCode:mail-queue-verifyCode}")
public void getMessage(Message message, Channel channel) throws IOException {
//获取消息内容
String code = message.getPayload().toString();
//获取消息头,消息标志tag
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
//获取消息ID
String msgId = (String) headers.get("spring_returned_message_correlation");
LOGGER.info("【"+msgId+"】-正在处理的消息");
//查看消息是否已消费
if (redisTemplate.opsForHash().entries("mail_log").containsKey(msgId)){
//手动确认消息已消费
channel.basicAck(tag,false);
LOGGER.info("【"+msgId+"】消息出现重复消费");
return;
}
//否则进行消息消费
try{
System.out.println("验证码管理");
SimpleMailMessage msg = new SimpleMailMessage();
msg.setSubject("微言聊天室管理端-验证码验证");
//TODO 使用Thymeleaf,改邮件模板,添加内容:请不要泄露自己的邮箱验证码
msg.setText("本次登录的验证码:"+code);
msg.setFrom("发送者的邮箱地址");
msg.setSentDate(new Date());
msg.setTo("接受者的邮箱地址");
javaMailSender.send(msg);
//消息发送成功,将id放到redis中,不能这样put
//redisTemplate.opsForHash().entries("mail_log").put(msgId,code);
redisTemplate.opsForHash().put("mail_log",msgId,code);
//确认消息消费成功
channel.basicAck(tag,false);
}catch (Exception e){
//不批量处理,将消息重新放回到队列中
channel.basicNack(tag,false,true);
LOGGER.info("【"+msgId+"】消息重新放回到了队列中");
e.printStackTrace();
}
}
}