大概从 2013 年开始,我就开始了自己和 RabbitMQ 的接触,到现在已经有七年多了。
在这七年中,既有一些对 RabbitMQ 的深度体验,更有无数的血泪史。
而根据我这么多年的使用经验,我将 RabbitMQ 的心得形成一些提醒或者规范分享给大家,这样,大家以后使用 RabbitMQ 的时候,就不会再走我走过的弯路了。
我想把我这些关于 RabbitMQ 的经验和心得,分成三篇来写:
这次咱们先从开发前的规范开始谈起。
我曾经一直都很奇怪,为何大家使用开发语言有开发规范,使用数据库有数据库规范,但是使用 MQ 却很少见一些规范。
使用 MQ 缺少规范,这是普遍的问题?还只是我身边的个例?
不管答案是哪个,在 RabbitMQ 使用时,为了避免在开发中少出现问题,为了事半功倍,都需要提前规范好一些配置和事项。
我们在使用数据库的时候,会在一个数据库应用里建立多个不同的数据库去给不同的项目使用,而不用在不同的服务器专门每个项目都安装个数据库应用。
在 RabbitMQ 的 vhost,也是类似的理念。
vhost 本质上是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、绑定、交换器和权限控制,当在 RabbitMQ 中创建一个用户时,用户通常会被指派给至少一个 vhost,并且只能访问被指派 vhost 内的队列、交换器和绑定,vhost 之间是绝对隔离的。
所以,不同的 vhost 对应不同的项目,互不影响,而这些 vhost 其实都是在一台主机一个 RabbitMQ 应用上。
但是,现在的状况是大部分使用 RabbitMQ 的技术团队往往就使用默认的 vhost:“/”,如果多出一个项目了,就再去创建一个 RabbitMQ 的进程。这样做,非常浪费开发资源。
推荐一个项目对应一个 vhost。
很多公司使用 RabbitMQ 都是直接使用 RabbitMQ 自己的 java 版本客户端,但是由于 RabbitMQ 本身内在的复杂性和多样性,有很多技术细节需要独自处理。
比如网络连接的处理,比如异常的处理,比如消息失败的处理等等等。这些,如果手头没有一套成熟的框架,那么很可能由于一些细节处理不到位,导致非常多的问题,这都是不必要的成本。
所以,要么使用一套已有的 RabbitMQ 客户端框架(比如 Spring 的 RabbitMQ 框架),要么自己封装出一套底层 RabbitMQ 客户端框架,而不是单独使用 RabbitMQ 的客户端
ACK 机制就是消费者从 RabbitMQ 收到消息并处理完成后,反馈给 RabbitMQ,然后 RabbitMQ 收到反馈后才将此消息从队列中删除。
由于 ACK 机制本身必须回复给 RabbitMQ,消息才会丢弃这个特点。对于何时给 ACK,我们做开发的时候一定要在开发项目前提前规划好、设计好。
我们使用 RabbitMQ 通常不想在收到消息就立即给回 ACK 的,也不会设置 autoACK 机制即消费端收到自动返回一个 ACK 响应。一般来讲,我们都会根据业务逻辑的不同,会在不同的位置手动返回 ACK。
这时候,就可能出现问题:当收到消息,有时候处理业务逻辑报错了,往往在处理完业务逻辑就会忽略 ACK,这会导致消息始终卡死在 queue 里……如果数量越来越多,后续处理非常麻烦。
为什么那么多人不设置 dead letter exchanges?这是我非常疑惑的点。
出去和各类使用 RabbitMQ 的项目团队交流,发现很少人设置了 dead letter exchanges。这个是有问题的。
我们得知道,有时候消息投递出错,并不总是在应用接收的时候出了问题,会有很多非应用的问题。比如:
消费端有问题,发出的消息被拒绝了。并且我们也设置了 requeue=false;
消息可能因为没有收到 ACK 超时被删除,或者消费端消费速度跟不上导致消息超时被删除;
消息数量超过了队列最大长度限制被抛弃;
消息总大小超过了队列消息总大小限制被抛弃。
对于这些问题,设置 dead letter exchanges 算是一个解决办法。
当消息一旦出现我上面列举出来的情况,就会被发送到我们设置的 dead letter exchanges。然后我们就可以对这些特殊情况的消息进行单独处理,这样的做法可以让我们的项目更健壮,更容易追踪问题。
RabbitMQ 的Exchange 就是消息交换机,它指定消息按什么规则,路由到哪个队列。
这家伙有四种类型:
Direct:处理路由键,需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键为“green”,则只有路由键为“green”的消息才被转发,不会转发路由键为"red",只会转发路由键为“green”。
Topic:将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“*”只能匹配一个词。
Fanout:不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到该类型交换机的消息都会被广播到与该交换机绑定的所有队列上。
Headers:不处理路由键,而是根据发送的消息内容中的 headers 属性进行匹配。在绑定 Queue 与 Exchange 时指定一组键值对;当消息发送到 RabbitMQ 时会取到该消息的 headers 与 Exchange 绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。
在这四种类型里,Direct 类型的 Exchange 投递消息是最快的。其他的 Exchange,MQ还得花时间计算投递的位置。
所以,能使用 Direct 类型的建议使用 Direct。
以上就是在使用 RabbitMQ 前,需要考虑使用的规范,有了这些规范,很多程序员就能据此写出比较稳定健壮的程序,而不会导致各种莫名其妙的问题。
强烈建议大家在团队使用之前,先弄一份规范。否则,如果很多程序员使用 RabbitMQ 非常随意,容易导致出现各种幺蛾子问题。
在下一篇文章里,我将写一些开发中需要注意的事项。
虽说是开发需要注意的事项,其实就是我在开发一套成熟的 RabbitMQ 客户端框架时,碰到的各种问题的总结。如果有些读者所在的开发团队恰好也有类似的经历,那么我说的所有问题,你会发现你可能都会遇到。
我们下篇文章见!