谁偷了我的 SignalR 消息?—— 记一次 MassTransit 分布式“抢占”深坑排查

谁偷了我的 SignalR 消息?—— 记一次 MassTransit 分布式“抢占”深坑排查

西街长安
2026-02-05 / 0 评论 / 3 阅读 / 正在检测是否收录...

被坑惨了!SignalR 消息“随机消失”的灵异事件复盘

兄弟们,最近在写 IM 系统的推送,被一个“玄学” Bug 折磨了好几天。那种“一下有、一下没有、断点还随缘命中”的感觉,真的差点让我怀疑人生。今天终于破案了,必须给大伙儿避避坑。

01 诡异的“概率性”失灵

在搞 IM 开发的时候,你有没有遇到过这种鬼故事:

  • 代码逻辑一模一样,入口 A 稳如泰山,入口 B 就“一下有一一下没有”。
  • 断点时而能断住,时而像被幽灵遮蔽了一样,程序直接滑过去。
  • MQ 后台显示消息发了,也被人领走了,但你的控制台就是一片死寂。

今天复盘的这个案例,就是因为 “多实例竞争消费” 导致的 SignalR 推送失灵。

02 案发现场

我的发消息入口很简单:就是一个 Web API Controller,接收前端 Post 过来的消息。

但奇怪的是:只要是从这个 API 发出来的消息,大概率会失联,前端死活收不到推送。

03 地毯式排查(全是血泪)

怀疑点 A:AutoMapper 映射翻车?

我先去翻了 RabbitMQ 的错误队列(_error),还真抓到了现行!报错显示 MessageCreatedEventMessageBaseVo 失败了。

教训: MassTransit 消费端对字段类型很敏感。哪怕是 sbyteEnum 这种微小差别,都能让映射直接崩溃。
结果: 修正了 Profile 映射,报错没了,但消息消失的问题 依然存在

怀疑点 B:数据库事务“赛跑”?

因为 API 逻辑里涉及不少更新(比如对话表、未读数),我怀疑是事务还没提交,MQ 消息就先发出去了,导致消费者去查库的时候出现了“幻读”。

教训: 记得把 Publish 放在 transaction.Commit() 后面。
结果: 顺序调对了,但推送还是“看心情”出现。

怀疑点 C:真凶现身 —— 消失的 “Unacked”

我盯着 RabbitMQ 后台看,发现消息进了一个叫 Unacked 的状态。这意思就是:消息已经发给某个程序了,但那程序领了之后没回信(Ack)。

就在我盯着断点发呆的时候,我脑子里闪过一道闪电:卧槽,我是不是同时开了两个项目实例?

04 真相大白:竞争消费者模式 (Competing Consumers)

MassTransit 默认会让 Consumer 监听同名队列(比如 SignalREventHandler)。
如果你在本地:

  1. VS 里开着调试,这是 项目 A
  2. 后台终端或者别的窗口还跑着一个没关掉的 项目 B

RabbitMQ 可不管谁是谁,它会觉得这是同一个服务的两个节点,于是玩起了 “轮询机制”

  • 消息 1:发给了项目 A(你在调试的),断点中了,推送成功。
  • 消息 2:发给了项目 B(后台挂着的旧实例)。项目 B 默默消费了消息,但它没连着前端页面啊!消息直接进了黑洞。

05 解决方案

方案一:开发环境给队列“实名制”

别让大家挤在一个队列里。给每个实例整一个唯一的队列名,把 Exchange 变成“广播”模式。

cfg.ReceiveEndpoint($"SignalR-Queue-{Guid.NewGuid()}", e => {
    e.AutoDelete = true; // 程序一停,队列自动销毁,干净卫生
    e.ConfigureConsumer<SignalREventHandler>(context);
});

方案二:生产环境上 Redis 底板

多实例部署时,一定要配 SignalR Redis Backplane。这样不管消息被哪个节点抢走了,它都会通过 Redis 吼一嗓子,通知所有节点一起推送,确保用户不管连在哪都能收到。

06 总结

解决这种分布式 Bug 有三大法宝:

  • 看颜色: 给不同 Handler 的 Console.WriteLine 加上显眼的颜色,谁在干活一眼便知。
  • 看后台: RabbitMQ 的 UnackedConsumers 数量永远不会骗你。
  • 清门户: 永远检查你身后是不是还开着一个“幽灵”实例。

要是你也遇到了断点断不住的灵异事件,先别怀疑人生,去看看 RabbitMQ 里的消费者列表吧!


1

评论 (0)

取消