消息队列:那个帮你擦屁股的中间人
写在前面:本文不教你怎么用 RabbitMQ 或 Kafka,那些文档写得比你好多了。本文只想聊聊——为什么你的系统加了消息队列反而更烂了。
01. 你是怎么入坑的?
“哎呀,这个接口太慢了,用户等半天。”
“加上消息队列吧,异步处理,体验丝滑!”
恭喜你,你踏上了消息队列这条贼船。
然后呢?
然后你的系统就变成了:用户下单成功 → 消息发出去 → 消费者没收到 → 用户看到“下单成功”但没卵用 → 客服电话被打爆。
欢迎来到消息队列的真实世界。
02. 我交过的学费
学费一:消息丢失——你永远不知道它有没有到
那年我负责一个订单系统。用户体验要“快”,于是把所有非核心逻辑都丢给了消息队列。库存扣减、发优惠券、短信通知、推送提醒——全TM是消息。
结果呢?
用户下单成功,收到“下单成功”的页面。然后就没有然后了。库存没扣、优惠券没发、短信毛都没有。
查日志?消息确实发出去了。
查消费者?消费者表示压根没收到。
后来才发现:消费者重启期间的消息,全丢了。因为我那时候太年轻,不知道消息需要持久化,不知道要手动ACK,更不知道RabbitMQ的默认配置就是为了让你踩坑。
教训:消息不持久化 = 消息不稳定 = 你的KPI不稳定。
学费二:顺序错乱——谁TM知道哪个先到的
后来学乖了,消息持久化了。但又遇到一个新问题:订单状态更新消息和支付回调消息,哪个先到?
答案是:看心情。
用户支付成功 → 订单状态变成“已支付”消息发出
用户取消订单 → 订单状态变成“已取消”消息发出
如果这两个消息同时发出,消费时顺序错乱,那就是:
- 订单显示已取消,但显示已支付
- 库存显示扣减了,但又给你退回来了
你永远不知道用户看到的页面是什么鬼。
教训:跨消息的顺序一致性 = 分布式系统噩梦。解决方案?加序号、加版本号、或者——别TM在不同消息里放有关联的数据。
学费三:重复消费——它来了,它来了,它带着重复来了
“消费者处理失败了怎么办?”
“重试啊!”
于是你加了重试机制。然后发现:用户收到了10条短信。
因为重试的时候,消息没被ACK,系统以为没处理成功,于是疯狂重试。消费者的逻辑是“发短信”,于是用户手机炸了。
教训:消费者的逻辑必须是幂等的。处理10次和处理1次,结果必须一样。想不明白这点,你就等着被用户投诉到倒闭吧。
03. 正确的打开方式
3.1 消息不只是一种“异步”
很多人把消息队列当成“让系统变快”的工具。这是对消息队列最大的误解。
消息队列的本质是解耦和削峰填谷,不是让你偷懒的。
- 解耦:A系统不需要知道B系统、C系统、D系统怎么玩,只需要把消息发出去,有人处理就行
- 削峰:流量高峰期,消息积压住,后慢慢处理,别把数据库干挂
如果你只是为了“异步”,不好意思,你可能不需要消息队列。线程池就行,还简单。
3.2 消费者才是大爷
很多人把消息队列当成了一个“存放任务的地方”。消费者?那不重要,反正会消费的啦。
错了。
消费者才是整个链路的核心。你要保证:
- 消费者能独立运行,不依赖Producer的存在
- 消费者处理失败有明确的策略(重试?死信队列?人工介入?)
- 消费者的幂等性
Producer可以随时发消息,但消费者必须强壮到能处理各种狗血情况。
3.3 监控!监控!监控!
你的消息队列健康吗?你知道吗?你不知道。
你必须监控:
- 消息积压数量(积压太多说明消费不过来了)
- 消息处理耗时(突然变慢说明出问题了)
- 消息失败率(失败不要紧,要紧的是你不知道它失败了)
很多系统刚上线时消息队列用得飞起,三个月后积压了100万条消息没人处理。这就是没有监控的下场。
04. 什么时候不该用消息队列?
这是最重要的问题。因为很多人是“逢人便加MQ”,好像不加消息队列就显得自己不够高级。
别TM装了。
以下情况不需要消息队列:
- 同步调用就够的情况——调用方必须知道结果,还TM加什么消息队列?
- 低并发场景——每秒10个请求,你加个消息队列不是增加复杂度吗?
- 简单业务逻辑——一个接口搞定的事,别给自己加戏
- 团队里没人懂消息队列——这个最致命。技术选型不是炫技,是解决问题
05. 我的最佳实践
- 消息要有ID,方便追踪
- 消息要有业务含义,别把消息当成杂货铺
- 消费者幂等性是底线,做不到就别上线
- 死信队列必须有,处理失败的消息要有归宿
- 监控要到位,看不到数据 = 裸奔
- 别TM乱用,简单方案能解决的事,复杂方案就是灾难
06. 写在最后
消息队列不是银弹,不是救世主,不是让你变成“架构师”的魔法棒。
它只是一个工具。一个帮助你在分布式系统中传递消息的工具。
用得好,系统稳如狗。
用得不好,事故多如毛。
所以——
在加消息队列之前,先想清楚:真的需要吗?
想清楚了,再加。
本文作者:小龙虾
微信公众号:搞点事情