首先简单介绍下RabbitMQ。RabbitMQ使用高并发性的erlang语言开发编写,性能比较好,并且可靠性比较高,支持事务。RabbitMQ这些特性都比较适合我们当前的业务,对可靠性和稳定性都要求比较高,很适合作为我们系统的消息总线和异步解耦。而Spring boot集成RabbitMQ也比较简单,网上资料很多,下面是我刚开始使用的一种集成方案:
首先maven要引入相关的依赖包:
然后就是在配置文件里面配置队列、交换机、路由关键词等,其中关键的一个配置如下:
@Bean(name = "rabbitListenerContainerFactory")
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setMaxConcurrentConsumers(5);
factory.setConcurrentConsumers(5);
factory.setPrefetchCount(5);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
最后是写生产者和消费者的代码。其中消费者的代码如下:
@RabbitHandler
@RabbitListener(queues = RabbitConf.QUEUE_NAME_PAY_NOTIFY, containerFactory = "rabbitListenerContainerFactory")
public void process(@Payload Map
map, Channel channel, Message message) throws Exception { try{
log.info("支付回调消息队列{}接收数据,消息体:{}", RabbitConf.QUEUE_NAME_PAY_NOTIFY, map);
// 这里业务代码省略
}catch(Exception e){
log.error(e,"消息队列消费发生异常");
}
}
通过使用Spring的注解可以很简洁地实现了集成RabbitMQ的的功能,但也为后面的稳定运行埋下了隐患。从9月份初开始上线,一直都很正常,直到11月底的某一天,突然出现了问题,只能看到生产者发送消息成功的日志,但没有消费者接受到任何消息的日志。于是我们开始紧张的排查原因,当天确实也有网络不太稳定的一些因素,但我们的日志没有打印出任何的异常信息。上面的代码大家也可以看到就算我们用try{}catch了消费者方法体里的所有代码,也没看到日志打印。更诡异的是RabbitMQ监控管理台也没看到有Unacked的消息,也就是说消息没有堆积,但就是消费不了。因为线上问题比较急,果断重启服务器,当两台服务器重启之后消息队列又正常了。问题虽然是暂时解决了,但我们必须要彻底的找到答案。开始我们是怀疑线程数过多导致被kill掉的情况,鉴于此我们修改了下配置,就是把之前的并发线程数由5个修改成1个:
@Bean(name = "rabbitListenerContainerFactory")
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setMaxConcurrentConsumers(1);
factory.setConcurrentConsumers(1);
factory.setPrefetchCount(1);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
就这样放了一个临时版本上生产,当然也好好的跑了两天,以至于我们觉得快要正常的时候,又突然发生了异常。这一次仍然看不到任何异常的日志,但是RabbitMQ的管理控制台却能看到Unacked的消息了,这次是明显的消息堵塞堆积。线上问题比较急,我们没有太多的时间去查找答案,但这次重启服务器也不管用了,明显的是集成RabbitMQ的方法是有问题的,过多的Spring封装,让我们看不到任何有帮助的日志,这个可以查看Spring源码看到日志量几乎没有,大多是debug级别的,而我们的系统只打印info级别以上的日志,这也说明了一点,Spring方式虽然使用简单但是并不能让开发者搞清楚他写的每行代码或每个参数都是什么作用,这样的使用很容易导致问题。最终我们觉得应该是消息内容转换上面有问题,使用了注解@Payload,看spring源码是有可能会导致异常的,决定快速换一种集成RabbitMQ的方法。下面就是我们解决问题的一种集成方法:
新增监听器来消费信息:
@Bean
public SimpleMessageListenerContainer payNotifyContainer(@Qualifier(QUEUE_NAME_PAY_NOTIFY) Queue queueMessage, ConnectionFactory connectionFactory) {
LogUtils.info("支付回调消费端处理");
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setQueues(queueMessage);
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setMessageConverter(new Jackson2JsonMessageConverter());
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//设置支付回调的监听器
container.setMessageListener(payNotifyListener);
return container;
}
@Component
@RabbitListener(queues = RabbitConf.QUEUE_NAME_PAY_NOTIFY)
public class PayNotifyListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
//省略业务代码
}
}
最终通过以上代码的修改消息队列终于恢复正常,去掉了@Payload这种spring封装消息内容自动转换导致的异常可能性。
最后总结一下经验:
1.当消息为空或者消息body为空字符串时都有可能会导致消息不掉的情况,导致消息阻塞,所以要尽量避免这种情况。
2.当线上消息出现堆积的时候可以通过RabbitMQ控制台把这种错误消息消费掉。
3.生产者和消息者设置的MessageConverter要一致。
4.使用Spring虽然让代码变得简单方便,但必须要对它的使用方法深入了解,甚至要查看它的源码写法。
5.有时候该用原生就用原生的,官方文档很详细,网上资料也很多,方便定制各种特性,排查问题也方便。
另附一个高可靠性的设计方案作为后期我们项目参考: