案例描述:
线上采用canel监听A表的status状态变化,只要有变化就通过mq发送消息到对应的消费者端处理。
消费者端的程序代码这样子写(因涉及公司业务,这里只是举个大致的代码示例)
代码大致的思路就是,监听队列QUEUE_B,然后消费发送到QUEUE_B的消息,如果消费过程中发生异常,就将消息重新发回队列中去。
一开始上线这个功能是正常的,因为doSomeThing业务里面没有任何异常,消息能够被正常消费。
后来某个版本上线后,突然有一天晚上,发现线上的服务,大部分宕机,监控短信响个不停。
经核实,就是因为doSomeThing里面的代码异常了,导致mq消费失败,然后代码里又将消息重新推回队列中。这样子问题就来。
程序本身有问题,mq消费消息永远是失败的,然后又刚好设置重新归队,导致一直在刷mq消费失败的日志,结果80G的磁盘没多久就撑爆了,导致同服务器的其他服务因为磁盘写入日志的问题,全线奔溃。。。代价有点惨重。
@Component
@Slf4j
public class MsgReceiverB {
@RabbitListener(queues = RabbitMqConfig.QUEUE_B)
public void onMessage(Message message, Channel channel) throws Exception {
try {
log.info("接收处理队列MsgReceiverB当中的消息: " + new String(message.getBody()));
//处理某业务的代码,这里以doSomeThing示例
doSomeThing();
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
} catch (Exception e) {
log.error("QUEUE_B消费异常={}",e.getMessage());
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
private void doSomeThing() {
//获取用户登录信息,原先获取不到用户信息是返回null,后来获取不到直接抛错,导致了这次血案的发生
System.out.println("处理业务");
}
}
问题总结:
出现这种问题的原因:开发对应业务的同事对mq的特性不是很熟悉。虽然一开始没有问题,但是埋下了隐患。后来某同事
因需求变动,导致doSomeThing()代码抛错就成了引爆这个导火线的人,直接导致了这次线上事故的发生。
笔者认为合理的处理方式有以下几种
第一种:消息消费失败后,不要设置重新归队,因为代码没有执行手工确认这个动作,消息是不会从mq中移出的,也就是消息
不会丢失,等消费者下次再启动的时候,就会重新消费。
第二种:消息消费失败后,直接让消息进入死信队列,在死信队列里,再起消费端重新消费,如果消费几次还是失败的话,
发消息通知人工处理。(根据消息的重要性判断是否需要人工处理,还是直接丢弃)
第三种:消息消费失败,直接持久化到数据库,nosql数据库或者sql数据库都可以,然后再发消息人工介入处理。(根据消息的重要性判断是否需要人工处理,还是直接丢弃)
以上纯属笔者个人意见,如有更好的方案,欢迎下方评论。