RabbitMq
基础知识:
交换机常用类型,(headers 为头部参数模式,交换机和队列同时有相同的参数则匹配x=1,direct直连模式,fanout扇形模式(没有routing key),topic 主题模式),是否持久化,
,anto delete 是否自动删除。
Rabbitmq使用场景(主要作用,解耦,异步,削峰):再根据具体业务讲解,把日志文件放到mq,再使用elk 去消费,做日志监控。分布式事务,下单,抢票,
过期时间TTL
过期时间ttl表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收,过了时间后会自动删除或者放到死信队列,可以对队列设置ttl,也可以对单独的消息设置ttl.
死信队列:
RabbitMq分布式事务(跨jvm级别的事务):
2PC TCC mq消息异步(Mq 事务消息)
实现要点:1、构建本地消息表及定时任务,扫描状态为未投递成功的消息,确保消息发送到mq,就可以更改消息状态为成功;2、RabbitMQ可靠消费;3、redis保证幂等
今天花费一整天的时间搞rabbitmq,记录一下:这里不讲搭建过程,只讲rabbitmq如何保证消息的发送,不丢失,和死信队列。
生产者如何保证消息被消费(高可靠,高可用):
保证生产者发送消息:
我们开启Mandatory, 触发回调函数,如果发送到了交换机ack为true,否则进行补偿,补偿我在消息发送的时候把 //msgId和message关系保存redis,用于补偿,补偿玩删除缓存。correlationData 里面保存了消息的唯一id
@ApiOperation("发送消息到消息队列 DirectExchange模式")
@PostMapping("/send")
public String send() {
String msg = "hello";
String msgId = UUID.randomUUID().toString();
rabbitTemplate.convertAndSend("myDirectExchange", "my.direct.routing", msg,new CorrelationData(msgId)); // 指定 交换机和路由 发送到 myDirectQueue 队列上
//msgId和message关系保存redis。用于补偿
ArrayList<String> strings = new ArrayList<>();
strings.add("myDirectExchange");
strings.add("my.direct.routing");
strings.add(msg);
redisTemplate.opsForValue().set(msgId,strings,3, TimeUnit.MINUTES);
return "success";
}
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
// 开启Mandatory, 才能触发回调函数,无论消息推送结果如何都强制调用回调函数
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
// 当消息发送到交换机(exchange)时,该方法被调用.如果达到了交换机 ack=true,没有则为false
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("ConfirmCallback: "+"相关数据:" + correlationData);
System.out.println("ConfirmCallback: "+"数据到交换机确认情况:" + ack);
System.out.println("ConfirmCallback: "+"原因:" + cause);
// 发到交换机失败
if (!ack) {
// 从缓存中获取数据,进行补偿重试机制
ArrayList<String> msg = (ArrayList<String>) redisTemplate.opsForValue().get(correlationData.getId());
rabbitTemplate.convertAndSend(msg.get(0), msg.get(1), msg.get(2));
redisTemplate.delete(correlationData.getId());
System.out.println(msg);
}
}
});
当消息从交换机到队列失败时,下面方法被调用。(若成功,则不调用),同样进行补偿
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
// 当消息从交换机到队列失败时,该方法被调用。(若成功,则不调用)
// 上面的confirm方法也会被调用,且ack = true
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("ReturnCallback: "+"消息:" + message);
System.out.println("ReturnCallback: "+"回应码:" + replyCode);
System.out.println("ReturnCallback: "+"回应信息:" + replyText);
System.out.println("ReturnCallback: "+"交换机:" + exchange);
System.out.println("ReturnCallback: "+"路由键:" + routingKey);
// 补偿
rabbitTemplate.convertAndSend(exchange, routingKey, message);
}
});
return rabbitTemplate;
}
保证消费者拿到消息:
开启手动确认ack,并开启confirm 和 return回调:
rabbitmq: #对于rabbitMQ的支持
host: 127.0.0.1
port: 5672
username: guest
password: guest
# ------- 消息确认配置项 --------
# 开启confirms 回调
publisher-confirms: true
# 开启return 回调
publisher-returns: true
listener:
simple:
acknowledge-mode: manual # 手动确认消息是否被消费
如果消费了消息,我们会手动确认,删除消费的队列,如果出现了异常,我们放到死信队列(后面讲什么是死信队列)
@RabbitHandler
@RabbitListener(queues = "myDirectQueue") // 队列
public void process(Message msg, Channel channel) throws IOException {
try{
// 先幂等性判断 是否重复消费
// int a = 1 / 0; 抛异常
System.out.println("消费到消息"+msg);
// 手动确认 删除队列消息
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), true);
}catch (Exception e){
log.error("消费消息失败了【】error:"+ msg);
// 放到死信队列
channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false,false);
// 将消息重新放回队列 multiple=false 不从队列删除
// channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,true);
}
}
死信队列:
通俗的说死信队列和普通队列一样,DLX, Dead-Letter-Exchange。利用DLX, 当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX。消息变成死信一向有一下几种情况:
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息TTL过期
- 队列达到最大长度
原理:我们首先会创建一个死信队列,
//死信交换机
public static final String X_DEAD_LETTER_EXCHANGE = "x-dead-letter-exchange";
//死信路由
public static final String X_DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
//死信队列
public static final String DEAD_QUEUE = "dead-queue";
然后吧这个队列和普通队列进行关联,当普通队列出现死信对列时,就会转交到死信队列处理,(原队列还有其他业务要处理,一致重试下去不好),我们在去消费死信队列的消息就好了。
//创建普通队列
@Bean
public Queue getNormalQueue(){
Map args = new HashMap();
//当消息发送异常的时候,消息需要路由到的交换机和routing-key,这里配的直接是发送至死信队列
args.put("x-dead-letter-exchange",X_DEAD_LETTER_EXCHANGE);
args.put("x-dead-letter-routing-key",X_DEAD_LETTER_ROUTING_KEY);
//创建队列的时候,将死信绑定到队列中
return QueueBuilder.durable(myDirectQueue).withArguments(args).build();
}
//监听死信队列
@RabbitListener(queues = {RabbitQueueAndExchange.DEAD_QUEUE})
public void receiver(Message msg, Channel channel) throws IOException {
// 手动确认 删除队列消息
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), true);
System.out.println("dead queue 收到消息>>>>>>>>>"+msg);
}
对比3附图,发现了dead-queue 和 myDirectQueue 的关系,当出现了异常,myDirectQueue没有消息,消息丢到了dead-queue 队列里。
以上是第一种方式:
还有种方式,在发送消息时,把消息的信息发送到一张日志表里面,并初始化状态未消费,然后在、setConfirmCallback里面,如果发送成功修改状态为成功,在定义个计数器去扫描日志表,扫描未消费的重试几次。
Employee emp = employeeMapper.getEmployeeById(employee.getId());
//生成消息的唯一id
String msgId = UUID.randomUUID().toString();
MailSendLog mailSendLog = new MailSendLog();
mailSendLog.setMsgId(msgId);
mailSendLog.setCreateTime(new Date());
mailSendLog.setExchange(MailConstants.MAIL_EXCHANGE_NAME);
mailSendLog.setRouteKey(MailConstants.MAIL_ROUTING_KEY_NAME);
mailSendLog.setEmpId(emp.getId());
mailSendLog.setTryTime(new Date(System.currentTimeMillis() + 1000 * 60 * MailConstants.MSG_TIMEOUT));
mailSendLogService.insert(mailSendLog);
rabbitTemplate.convertAndSend(MailConstants.MAIL_EXCHANGE_NAME, MailConstants.MAIL_ROUTING_KEY_NAME, emp, new CorrelationData(msgId));
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;
定时器重试 :
@Scheduled(cron = "0/10 * * * * ?")
public void mailResendTask() {
List<MailSendLog> logs = mailSendLogService.getMailSendLogsByStatus();
if (logs == null || logs.size() == 0) {
return;
}
logs.forEach(mailSendLog->{
if (mailSendLog.getCount() >= 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()));
}
});
}