同事:我们这个消息通知功能,要不要上个队列?
我:现在什么量级?
同事:一天大概两千条吧。
我:那先别上。
这段对话我几乎每年都要重复一次。不是说队列不好,是很多团队引入它的理由经不起追问。一天两千条、峰值不过每秒几条的场景,一张表加一个定时扫描就够了,还顺带白送你可查询、可重放、可人工改状态这三个特性。
先想清楚你要的是哪一种能力
我把使用队列的动机归成四类,每一类对应的技术要求完全不同:
- 削峰。上游能瞬时打进来一万条,下游每秒只能处理两百条。这时你真正需要的是堆积能力,要问的问题是:堆积一千万条会不会把消费性能拖垮?
- 解耦。一个动作要触发五个下游,你不想在主流程里挨个调用。这时你需要的是广播和多消费组,要问的是新增一个消费方需不需要改上游代码。
- 异步。把耗时操作挪出主链路,让接口先返回。这时你要的是低延迟投递,堆积能力反而不重要。
- 顺序。同一个实体的状态变更必须按序处理。这是最麻烦的一类,因为顺序和并发天然打架。
我实际会对比的几个点
- 投递语义。绝大多数产品给的是至少一次,不是恰好一次。所以消费端必须幂等,这是硬需求,不是可选项。我见过团队花两周研究怎么配出精确一次,还不如花两天在业务表上加个唯一键。
- 顺序的粒度。全局顺序基本等于单线程,没人真用。实际用的是分区内顺序,那么分区键怎么选就成了关键。按实体 ID 取模是常见做法,但要注意热点:我们曾经有个大客户占了全部流量的 40%,全落在一个分区上,其他分区闲着。
- 重试与死信。这是我最看重的一点。原生支持延迟重试和死信队列,能省掉一大堆自研代码。如果没有,你迟早要自己写一个重试表加定时器,然后重新发明一遍轮子。
- 运维成本。这条经常被低估。一个需要独立集群、独立协调服务、独立监控的中间件,意味着你团队里得有人能在半夜看懂它的日志。三个人的小组维护不了两套中间件。
一次翻车
说个我踩过的具体的坑。我们有个消费组处理单条消息大概 300 毫秒,某天下游依赖变慢,单条涨到 4 秒。结果消费者在心跳间隔内没能完成一批消息的处理,被判定为失联,触发了重新分配。重新分配又导致所有消费者暂停、重新拉取、重复处理已经处理过但没提交位点的消息,处理更慢,再次超时——形成了一个循环,两个小时没恢复。
最后的解决办法很朴素:把单次拉取的条数从 500 降到 50,把处理超时的上限调大,同时把耗时的下游调用改成异步。这个坑教会我一件事:队列的默认参数是按理想的快速消费场景配的,一旦你的处理逻辑变重,那些参数全都要重新算一遍。
所以我现在选型时会先问一句:出问题的时候,我能不能看懂它在干什么。这个问题的权重,比吞吐量的基准数字高得多。