rabbitmq实战(保证消息一致性)
消息确认
生产者消息确认
- AMQP事务,利用AMQP协议的一部分,发送消息前设置channel为tx模式(channel.txSelect();),如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。(性能较差,一般不用)
- 消息确认(publish confirms),将信道channel设置成confirm模式(channel.confirmSelect();),一旦信道进入confirm模式,所有在信道上发布的消息都会被指派一个唯一的ID号(也就是deliveryTag,从1开始,后面的每一条消息都会加1,deliveryTag在channel范围内是唯一的。),一旦消息被投递到所有匹配的队列后,信道会发送一个发送方确认模式给生产者应用(包含消息的唯一ID),使得生产者知晓消息一经安全到达目的队列,如果消息和队列是持久化的,那么消息只会在队列将消息写入磁盘后才会发出
生产者确认消息代码
// RabbitMQ java Client实现发送确认
//是当前的channel处于确认模式
channel.confirmSelect();
//使当前的channel处于事务模式,与上面的使channel处于确认模式使互斥的
//channel.txSelect();
/**
* deliveryTag 消息id
* multiple 是否批量
* 如果是true,就意味着,小于等于deliveryTag的消息都处理成功了
* 如果是false,只是成功了deliveryTag这一条消息
*/
channel.addConfirmListener(new ConfirmListener() {
//消息发送成功并且在broker落地,deliveryTag是唯一标志符,在channek上发布的消息的deliveryTag都会比之前加1
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
}
//Spring AMQP实现发送确认如下(或者像入门案例一样,生产者实现RabbitTemplate.ConfirmCallback接口)
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 唯一标识,有了这个唯一标识,我们就知道可以确认(失败)哪一条消息了
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("=====消息进行消费了======");
if(ack){
System.out.println("消息id为: "+correlationData+"的消息,已经被ack成功");
}else{
System.out.println("消息id为: "+correlationData+"的消息,消息nack,失败原因是:"+cause);
}
}
});
return rabbitTemplate;
}
消费者确认消息
broker与消费者之间的消息确认称为消费者应答(comsumer acknowledgements),应答机制用于解决消费者与Rabbitmq服务器之间消息可靠传输,它是在消费端消费成功之后通知broker消费端消费消息成功从而broker删除这个消息
RabbitMq消费者可以选择手动和自动确认俩种模式,如果是自动确认模式,消息已到达队列后,rabbitmq会无脑的将消息抛给消费者,一旦发送成功,他会认为消费者已经成功接收,在RabbitMq内部就把消息给删除了。这样的话如何消费端抛出异常这条消息就会丢失。
另外一种就是手动模式,手动模式需要消费者对每条消息进行确认(也可以批量确认),RabbitMq发送完消息之后,会进入到一个待确认(unacked)的队列,如果消费者正在监听队列,那么此时消息进入Unacked,而如果消费者停掉服务,那么消息的状态又变成Ready了。这个机制表明了消息必须是ack确认之后才会在server中删除掉。
如果消费者发送了ack,RabbitMq将会把这条消息从待确认中删除。如果是nack并且指明不要重新入队列,那么该消息也会删除。但是如果是nack且指明了重新入队列那么这条消息将会入队列,然后重新发送给消费者,被重新投递的消息消息头amqp_redelivered属性会被设置成true,客户端可以依靠这点来判断消息是否被确认,可以好好利用这一点,如果每次都重新回队列会导致同一消息不停的被发送和拒绝。消费者在确认消息之前和RabbitMq失去了连接那么消息也会被重新投递。所以手动确认模式很大程度上提高可靠性。自动模式的消息可以提高吞吐量。
消费者确认消息代码
//Rabbit java Client
/**
* basicConsume方法的第二个参数是boolean类型,true自动确认,fals手动确认
* 自动确认有丢消息的可能,因为如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息
* 设置了false,表示需要人为手动的去确定消息,只有消费者将消息消费成功之后给与broker人为确定才进行消息确认
* 这边也有个问题就是如果由于程序员自己的代码的原因造成人为的抛出异常,人工确认那么消息就会一直重新入队列,一直重发?
*/
// 定义队列的消费者
QueueingConsumer consumer = new QueueingConsumer(channel);
// 监听队列,手动返回完成状态
channel.basicConsume(QUEUE_NAME, false, consumer);
//消费者确认消息
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" [x] Received '" + message + "'");
//确认消息
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
//不确认消息Nack
channel.basicNack(envelope.getDeliveryTag(),false,true);
//拒绝消息消费,第二个参数表示是否重新入队列
this.getChannel().basicReject(envelope.getDeliveryTag(),false);
总结
消费端的消息确认分为俩个步骤
- 在channel.basicConsume指定为手动确认
- 具体根据业务逻辑来进行判断什么是ack什么时候nack(又分为要不要重新requeue)
这边有个问题就是nack时候或者reject时候重新入队列如果业务端因为代码逻辑问题一直重发怎样去设置一个次数值?
我的设想就是设置一个重新发送的递增值,这个值与消息id对应,去处理解决它。或者在redis或者memcache等其他保存方式然后记录这个重发次数
spring中手动确认消息需要将SimpleRabbitListenerContainerFactory设置为手动模式或者将MessageListenerContainer设置为手手动模式:
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("queue_name");
//设置为手动确认模式
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {
System.out.println("====接收到消息=====");
System.out.println(new String(message.getBody()));
TimeUnit.SECONDS.sleep(10);
if(message.getMessageProperties().getHeaders().get("error") == null){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
System.out.println("消息确认");
}else {
//channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
System.out.println("消息拒绝");
}
});
return container;
}
设置确认模式是通过在容器中设置RabbitListenerContainerFactory实例的setAcknowledgeMode方法来设定。
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
//默认的确认模式是AcknowledgeMode.AUTO
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = RabbitMQConstant.CONFIRM_EXCHANGE, type = ExchangeTypes.TOPIC,
durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
value = @Queue(value = RabbitMQConstant.CONFIRM_QUEUE, durable = RabbitMQConstant.FALSE_CONSTANT,
autoDelete = RabbitMQConstant.true_CONSTANT),
key = RabbitMQConstant.CONFIRM_KEY),
containerFactory = "containerWithConfirm")
public void process(ExampleEvent msg, Channel channel, @Header(name = "amqp_deliveryTag") long deliveryTag,
@Header("amqp_redelivered") boolean redelivered, @Headers Map<String, String> head) {
try {
log.info("ConsumerWithConfirm receive message:{},header:{}", msg, head);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("consume confirm error!", e);
//这一步千万不要忘记,不会会导致消息未确认,消息到达连接的qos之后便不能再接收新消息
//一般重试肯定的有次数,这里简单的根据是否已经重发过来来决定重发。第二个参数表示是否重新分发
channel.basicReject(deliveryTag, !redelivered);
//这个方法我知道的是比上面多一个批量确认的参数
// channel.basicNack(deliveryTag, false,!redelivered);
}
}
spring的AcknowledgeMode一共有三种模式:AcknowledgeMode.NONE,MANUAL,AUTO,默认是AUTO模式。这里的NONE对应其实就是RabbitMQ的自动确认,MANUAL是手动确认
而AcknowledgeMode.AUTO 是根据方法的执行情况来决定是否确认还是拒绝(是否重新入queue)
- 如果消息消费的过程中没有抛出异常,则自动确认(相当于发送ack)
- 如果方法抛出AmqpRejectAndDontRequeueException异常,则消息会被拒绝,且requeue=false(不重新入队列)(相当于发送nack)
- 如果方法抛出ImmediateAcknowledgeAmqpException异常,则消费者会被确认
- 如果方法抛出其他的异常,则消息会被拒绝,且requeue=true(如果此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。可以通过setDefaultRequeueRejected(默认是true)去设置
TLL
RabbitMQ允许您为消息和队列设置TTL(Time To Live生存时间)。 可以使用可选的队列参数或策略完成(推荐使用策略)。 可以为单个队列,一组队列或单个消息应用消息TTL。
给消息设置TTL
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType("json");
//设置消息的过期时间
messageProperties.setExpiration("30000");
Message message = new Message(body,messageProperties);
给队列设置TTL
@Bean
public Queue queue(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl",30000);
return new Queue("email",true,false,false,arguments);
}
如果同时制定了Message TTL,Queue TTL,则小的那个时间生效。
RetryTemplate
如果是采用SpringBoot的话,可以在application.yml配置中配置如下:
spring:
rabbitmq:
listener:
retry:
# 重试次数
max-attempts: 3
# 开启重试机制
enabled: true
如上,如果消费者失败的话会进行重试,默认是3次。注意这里的重试机制RabbitMQ是为感知的!到达3次之后会抛出异常调用MessageRecoverer。默认的实现为RejectAndDontRequeueRecoverer,也就是打印异常,发送nack,不会重新入队列。 我想既然配置了重试机制消息肯定是很重要的,消息肯定不能丢,仅仅是日志可能会因为日志滚动丢失而且信息不明显,所以我们要讲消息保存下来。可以有如下这些方案(推荐前两种):
- 使用RepublishMessageRecoverer这个MessageRecoverer会发送发送消息到指定队列
- 给队列绑定死信队列,因为默认的RepublishMessageRecoverer会发送nack并且requeue为false。这样抛出一场是这种方式和上面的结果一样都是转发到了另外一个队列。详见DeadLetterConsumer
- 注册自己实现的MessageRecoverer
- 给MessageListenerContainer设置RecoveryCallback
- 对于方法手动捕获异常,进行处理
死信队列
DLX 全称(Dead-Letter-Exchange),称之为死信交换器,当消息变成一个死信之后,如果这个消息所在的队列存在x-dead-letter-exchange参数,那么它会被发送到x-dead-letter-exchange对应值的交换器上,这个交换器就称之为死信交换器,与这个死信交换器绑定的队列就是死信队列。死信队列其实就是普通的队列,只不过一个队列声明的时候指定的属性,会将死信转发到该交换器中。声明死信队列方法如下:
@RabbitListener(
bindings = @QueueBinding(
exchange = @Exchange(value = RabbitMQConstant.DEFAULT_EXCHANGE, type = ExchangeTypes.TOPIC,
durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
value = @Queue(value = RabbitMQConstant.DEFAULT_QUEUE, durable = RabbitMQConstant.FALSE_CONSTANT,
autoDelete = RabbitMQConstant.true_CONSTANT, arguments = {
@Argument(name = RabbitMQConstant.DEAD_LETTER_EXCHANGE, value = RabbitMQConstant.DEAD_EXCHANGE),
@Argument(name = RabbitMQConstant.DEAD_LETTER_KEY, value = RabbitMQConstant.DEAD_KEY)
}),
key = RabbitMQConstant.DEFAULT_KEY
))
其实也就只是在声明的时候多加了两个参数x-dead-letter-exchange和x-dead-letter-routing-key。这里一开始踩了一个坑,因为@QueueBinding注解中也有arguments属性,我一开始将参数声明到@QueueBinding中,导致一直没绑定成功。如果绑定成功可以在控制台看到queue的Featrues有DLX(死信队列交换器)和DLK(死信队列绑定)。如下:
什么样的消息会进入死信队列呢?
- 消息被拒绝(basic.reject/basic.nack)并且requeue=false
- 消息TTL过期
- 队列达到最大长度(或者最大大小)
队列长度
在创建队列的时候可以设置参数来限制队列的容量,可以通过队列消息的长度和大小来限制队列的容量:
1、room.queue队列中最多只有5个消息,当第6条消息发送过来的时候,会删除最早的那条消息。队列中永远只有5条消息
2、house.queue队列中最多只能存储消息body总和的大小为10bytes
代码实现
@Bean
public Queue queue1(){
Map<String, Object> arguments = new HashMap<>();
//表示队列中最多存放5条消息
arguments.put("x-max-length",5);
return new Queue("room.queue",true,false,false,arguments);
}
@Bean
public Queue queue2(){
Map<String, Object> arguments = new HashMap<>();
//表示队列中最多10bytes大小的消息
arguments.put("x-max-length-bytes",10);
return new Queue("house.queue",true,false,false,arguments);
}
创建完成后,查看
总结
Max length(x-max-length) 用来控制队列中消息的数量。 如果超出数量,则先到达的消息将会被删除掉。
ax length bytes(x-max-length-bytes) 用来控制队列中消息总的大小。 如果超过总大小,则最先到达的消息将会被删除,直到总大小不超过x-max-length-byte为止。 消息将会被删除,直到总大小不超过x-max-length-byte为止
RabbitMQ的相关配置
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、application.yml
server:
port: 7003
spring:
application:
name: rabbitmq
rabbitmq:
host: 192.168.31.100
port: 5672
username: guest
password: guest
template:
retry: # 失败重试
enabled: true # 开启失败重试
initial-interval: 10000ms # 第一次重试的间隔时长
max-interval: 300000ms # 最长重试间隔,超过这个间隔将不再重试
multiplier: 2 # 下次重试间隔的倍数,此处是2即下次重试间隔是上次的2倍
exchange: topic.exchange # 缺省的交换机名称,此处配置后,发送消息如果不指定交换机就会使用这个
publisher-confirms: true # 生产者确认机制,确保消息会正确发送,如果发送失败会有错误回执,从而触发重试
publisher-returns: true # 可以确保消息在未被队列接收时返回
# 如果consumer只是接收消息而不发送,就不用配置template相关内容
3、RabbitMQConfiguration配置类
@Configuration
public class RabbitMQConfiguration {
public static final String EXCHANGE_NAME_DIRECT="direct.exchange";//topics类型交换机
public static final String DIRECT_EXCHANGE_WITH_QUEUE="direct_exchange_with_queue";
/**
* 配置RabbitMq 消息服务
*/
@Bean("connectionFactory")//默认和方法名一致
public ConnectionFactory rabbitConnectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
String rabbitmqHost = "192.168.31.100";
String rabbitmqPort = "5672";
String rabbitmqUsername = "guest";
String rabbitmqPassword = "guest";
String rabbitmqVirtualHost = "/mye";
connectionFactory.setHost(rabbitmqHost);
connectionFactory.setPort(Integer.parseInt(rabbitmqPort));
connectionFactory.setUsername(rabbitmqUsername);
connectionFactory.setPassword(rabbitmqPassword);
connectionFactory.setVirtualHost(rabbitmqVirtualHost);
connectionFactory.setPublisherConfirms(true); // 设置后才能显示调用
return connectionFactory;
}
/**
* 消息生产者
*/
@Bean(name = "rabbitTemplate")
//必须是prototype类型
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RabbitTemplate rabbitTemplate() {
return new RabbitTemplate(rabbitConnectionFactory());
}
/**
* 参数详解
* 1、directExchange() 路由模式,也是默认的
* 2、topicExchange() 通配符模式
* 3、fanoutExchange() 发布订阅模式
* 4、headersExchange() 头交换机(路由模式键只有是字符串,而头交换机可以是整型和哈希值)
* 5、durable(true) 持久化,mq重启后交换机还在
* @return 交换机
*/
//声明交换机
@Bean(EXCHANGE_NAME_DIRECT)
public Exchange exchange() {
return ExchangeBuilder.directExchange(EXCHANGE_NAME_DIRECT).durable(true).build();
}
/**
* 参数详解
* public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
* 1、name: 队列的名称
* 2、durable: 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive: 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建,默认false
* 4、autoDelete:是否自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)默认false
* 5、arguments:参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*
* @return
* @throws UnknownHostException
*/
//声明队列
@Bean("queue")
public Queue queue() throws UnknownHostException {
获取本机(或者服务器ip地址)
InetAddress inetAddress = InetAddress.getLocalHost();
return new Queue(inetAddress.getHostName(),true, false, true);
}
//队列绑定交换机
@Bean
public Binding binding(@Qualifier("queue") Queue queue,@Qualifier(EXCHANGE_NAME_DIRECT) Exchange exchange) {
return BindingBuilder.bind(queue).to(exchange).with(DIRECT_EXCHANGE_WITH_QUEUE).noargs();
}
@Bean("customContainerFactory")
public SimpleRabbitListenerContainerFactory containerFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
@Autowired ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConcurrentConsumers(1); //设置线程数
factory.setMaxConcurrentConsumers(1); //最大线程数
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);//手动确认消息
configurer.configure(factory, connectionFactory);
return factory;
}
}
案例
1、数据库
CREATE TABLE `msg_log` (
`id` varchar(255) NOT NULL DEFAULT '' COMMENT '消息唯一标识',
`msg` text COMMENT '消息体, json格式化',
`exchange` varchar(255) NOT NULL DEFAULT '' COMMENT '交换机',
`routingKey` varchar(255) NOT NULL DEFAULT '' COMMENT '路由键',
`status` int(11) NOT NULL DEFAULT '0' COMMENT '状态: 0投递中 1投递成功 2投递失败 3已消费',
`tryCount` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
`nextTryTime` bigint(20) DEFAULT NULL COMMENT '下一次重试时间',
`createTime` bigint(20) DEFAULT NULL COMMENT '创建时间',
`updateTime` bigint(20) DEFAULT NULL COMMENT '更新时间',
`alertId` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `unq_msg_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='消息投递日志';
2、实现类
webhookController.java
@RestController
@RequestMapping("/api/webhook")
public class WebHookController extends ApiController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ConfirmCallbackService confirmCallbackService;
@Autowired
private ReturnCallbackService returnCallbackService;
@Autowired
private MsgLogBiz msgLogBiz;
private static final Logger logger = LoggerFactory.getLogger(WebHookController.class);
@RequestMapping(value = "/dcim1", produces = "application/json;charset=UTF-8")
@ResponseBody
public void getAlarm2(@RequestBody String alarmJson) {
logger.info("收到原始告警信息: {}", alarmJson);
List<Alert> alertList;
try {
JSONObject jsonObject = JSONUtil.parseObj(alarmJson);
JSONArray alerts = jsonObject.getJSONArray("alerts");
alertList = alerts.toList(Alert.class);
logger.info("alertList: {}", alertList);
} catch (Exception e) {
logger.error("收到告警信息Json格式化出现问题: " + alarmJson);
return;
}
if (CollectionUtil.isEmpty(alertList)) {
return;
}
this.sendAlert(alertList);
}
private void sendAlert(List<Alert> alertList) {
String msgId = "alert-" + UUIDUtil.generateUUID();
MsgLog msgLog = new MsgLog(msgId, JSONUtil.toJsonStr(alertList), "alertDirectExchange", "alert", MsgLogStatusEnum.DELIVERING.getKey(), 0, System.currentTimeMillis() + 2 * 60 * 1000L, System.currentTimeMillis(), 0L);
msgLogBiz.insert(msgLog);
/**
* 当mandatory标志位设置为true时
* 如果exchange根据自身类型和消息routingKey无法找到一个合适的queue存储消息
* 那么broker会调用basic.return方法将消息返还给生产者
* 当mandatory设置为false时,出现上述情况broker会直接将消息丢弃
*/
rabbitTemplate.setMandatory(true);
//confirmCallback接口用于实现消息发送到rabbitmq交换机后接受ack回调
rabbitTemplate.setConfirmCallback(confirmCallbackService);
//returnCallback接口用于实现消息发送到rabbitmq交换器。但无相应队列与交换机绑定式的回调
rabbitTemplate.setReturnCallback(returnCallbackService);
rabbitTemplate.convertAndSend("alertDirectExchange", "alert", JSONUtil.toJsonStr(msgLog), message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
}, new CorrelationData(msgId));
}
}
ConfirmCallbackService.java
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
private static final Logger logger = LoggerFactory.getLogger(ConfirmCallbackService.class);
@Autowired
private MsgLogBiz msgLogBiz;
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (!ack) {
logger.error("消息发送异常!");
} else {
String msgId = correlationData.getId();
msgLogBiz.updateStatus(msgId, MsgLogStatusEnum.DELIVER_SUCCESS.getKey(),System.currentTimeMillis());
logger.info("发送者已经收到确认,correlationData={} ,ack={}, cause={}", correlationData.getId(), ack, cause);
}
}
}
ReturnCallbackService.java
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {
private static final Logger logger = LoggerFactory.getLogger(ConfirmCallbackService.class);
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.error("returnedMessage ===> replyCode={} ,replyText={} ,exchange={} ,routingKey={}", replyCode, replyText, exchange, routingKey);
}
}
ResendMsg.java
@Component
public class ResendMsg {
private static final Logger logger = LoggerFactory.getLogger(ResendMsg.class);
@Autowired
private MsgLogBiz msgLogBiz;
@Autowired
private RabbitTemplate rabbitTemplate;
// 最大投递次数
private static final int MAX_TRY_COUNT = 3;
/**
* 每30s拉取投递失败的消息, 重新投递
*/
@Scheduled(cron = "0/30 * * * * ?")
public void resend() {
logger.info("开始执行定时任务(重新投递消息)");
long currentTimeMillis = System.currentTimeMillis();
List<MsgLog> msgLogs = msgLogBiz.selectTimeoutMsg(currentTimeMillis);
if (CollectionUtil.isEmpty(msgLogs)) {
logger.info("开始执行定时任务(重新投递消息),数据为空终止执行.");
return;
}
msgLogs.forEach(msgLog -> {
String msgId = msgLog.getId();
if (msgLog.getTryCount() >= MAX_TRY_COUNT) {
msgLogBiz.updateStatus(msgId, MsgLogStatusEnum.DELIVER_FAIL.getKey(), currentTimeMillis);
logger.info("超过最大重试次数, 消息投递失败, msgId: {}", msgId);
} else {
int count = msgLog.getTryCount() + 1;
msgLog.setTryCount(count);
Long nextTryTime = msgLog.getNextTryTime();
if (nextTryTime == 0) {
nextTryTime = currentTimeMillis;
}
msgLog.setNextTryTime(nextTryTime + count * 60 * 1000L);
msgLog.setUpdateTime(currentTimeMillis);
msgLogBiz.updateTryCount(msgLog);
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(msgLog.getExchange(), msgLog.getRoutingKey(), JSONUtil.toJsonStr(msgLog), correlationData);// 重新投递
logger.info("第 " + (msgLog.getTryCount()) + " 次重新投递消息: " + msgId);
}
});
logger.info("定时任务执行结束(重新投递消息)");
}
}
WebHookMqAlertListener.java
@Component
public class WebHookMqAlertListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ConfirmCallbackService confirmCallbackService;
@Autowired
private ReturnCallbackService returnCallbackService;
@Autowired
private MsgLogBiz msgLogBiz;
private static final Logger logger = LoggerFactory.getLogger(WebHookMqAlertListener.class);
@RabbitListener(containerFactory = "customContainerFactory", bindings = @QueueBinding(
value = @Queue("alertDirectQueue"),
exchange = @Exchange(value = "alertDirectExchange", type = ExchangeTypes.DIRECT),
key = "alert"
))
public void process(@Payload String msg, Channel channel, Message message) {
logger.info("收到告警消息:" + msg);
MsgLog msgLog = JSONUtil.toBean(msg, MsgLog.class);
MsgLog msgLogOld = msgLogBiz.selectByMsgId(msgLog.getId());
if (null == msgLogOld || msgLogOld.getStatus().equals(MsgLogStatusEnum.CONSUMED_SUCCESS.getKey())) {
logger.info("重复消费, msgId: {}", msgLog.getId());
msgAck(channel, message, msgLog, false);
return;
}
System.out.println(JSONUtil.toList(msgLog.getMsg(), Alert.class));
System.out.println(msgLog.getId());
msgAck(channel, message, msgLog, false);
}
private void msgAck(Channel channel, Message message, MsgLog msgLog, boolean isRepetition) {
try {
if (!isRepetition) {
msgLogBiz.updateStatus(msgLog.getId(), MsgLogStatusEnum.CONSUMED_SUCCESS.getKey(), System.currentTimeMillis());
}
//deliveryTag:该消息的index
//requeue:确认后是否加入队列
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
logger.info("告警消息手动确认:" + message.getMessageProperties().getDeliveryTag());
} catch (Exception e) {
//获取消息中是否再次投递,true:再次投递
if (message.getMessageProperties().getRedelivered()) {
logger.error("告警消息已重复处理失败,拒绝再次接收...");
try {
//deliveryTag:该消息的index
//消息发送成功并且在broker落地,deliveryTag是唯一标志符,在channek上发布的消息的deliveryTag都会比之前加1
//requeue:被拒绝后是否重新加入队列
//channel.basicReject和channel.basicNack的区别是basicNack可以拒绝多条消息,而basicReject一次只能拒绝一条消息
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 拒绝消息
} catch (IOException ioException) {
ioException.printStackTrace();
}
} else {
logger.error("告警消息即将再次返回队列处理...");
try {
//deliveryTag:该消息的index
//multiple:是否批量true:将一次性拒绝所有小于deliveryTag的消息
//requeue:被拒绝后是否重新加入队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
}
3、发送警告的数据
{
"alerts": [
{
"annotations": {
"description": "温度指标告警告警,当前值是: 28.3!",
"summary": "温度指标告警告警"
},
"endsAt": "0001-01-01T00:00:00Z",
"fingerprint": "e467f0ec6d95a4b4",
"generatorURL": "http://d8a0df8cb5a1:9090/graph?g0.expr=temperature_guage%7BcomponentInstanceId%3D%2233%22%2CcomponentSignalId%3D%221%22%7D+%3C+30&g0.tab=1",
"labels": {
"alertname": "温度指标告警闪断震荡测试",
"componentId": "6",
"componentInstanceId": "33",
"componentSignalId": "1",
"description": "获取温度信息",
"dynamicId": "1",
"hostGroupId": "1",
"instance": "192.168.10.125:8112",
"job": "test-dcim",
"nameChs": "温度指标",
"oid": "1.3.6.1.4.1.123456.1.1.1.1.0",
"severity": "minor",
"signalCollectId": "1",
"snmpAddress": "udp:192.168.10.125/161",
"threshold": "27",
"thresholdInfo": "温度指标告警 < 30℃,当前值= 28.3℃"
},
"startsAt": "2021-08-17T08:06:38.885Z",
"status": "firing"
}
]
}