《微服务即使通讯中RabbitMQ的作用》

在本项目中, RabbitMQ 扮演着"消息中转站"和"流量缓冲池"的核心角色,主要解决了 写 扩散带来的数据库压力系统解耦 问题。

1. RabbitMQ 存储了什么?

RabbitMQ 本质上是一个消息队列,它 暂存 了需要被异步处理的业务消息。在本项目中,主要存储的是:

  • 待持久化的聊天消息 :
    • 生产者 :Transmite Service(转发服务)。
    • 消费者 :Message Service(消息存储服务)。
    • 内容 :序列化后的 Protobuf 消息对象(包含发送者 ID、接收者/群 ID、消息内容、时间戳等)。
    • 流向 : Transmite Service -> Exchange -> Queue -> Message Service 。

2. 为什么要用 RabbitMQ? (核心价值)

A. 削峰填谷 (Traffic Shaping) - 最核心原因

  • 场景 :假设一个 500 人的群,有人发了一条消息。在"写扩散"模式下,如果不加 MQ,Message Service 需要瞬间向数据库写入 500 条记录(或者更新 500 个会话的最新消息)。如果此时是跨年夜,每秒有 1万条群消息,数据库瞬间面临百万级的 QPS 写入压力,直接宕机。
  • MQ 的作用 :
    • Transmite Service 收到消息后,只负责推送到 MQ,耗时极短(毫秒级),立刻返回成功
    • Message Service 可以根据自己的处理能力(比如每秒处理 2000 条),平滑地从 MQ 拉取消息写入 MySQL
    • 举例 :MQ 像一个大水库,把瞬间的洪水(流量高峰)蓄起来,然后通过水管(消费者)平缓地流向下游(数据库),保护了脆弱的数据库

B. 异步解耦 (Asynchronous Decoupling)

  • 同步调用的痛点 :如果没有 MQ,Transmite Service 必须通过 RPC 同步调用 Message Service。如果 Message Service 卡顿或挂了,Transmite Service 也会跟着卡顿,甚至导致整个聊天功能不可用(级联故障)
  • MQ 的作用 :Transmite Service 只需要把消息扔给 MQ 就不管了(Fire-and-Forget)。即使 Message Service 挂了,消息也会安全地存在 MQ 里,等 Message Service 重启后继续消费,不会丢消息,也不影响用户发送消息的体验

C. 提升响应速度 (Low Latency)

  • 用户体验 :用户发消息时,最在乎的是"转圈圈"转多久。
  • 对比 :
    • 无 MQ :等待(数据库写磁盘 + 索引构建 + 网络 IO)= 50ms ~ 200ms
    • 有 MQ :等待(写入内存队列)= 1ms ~ 5ms
    • 结论 :引入 MQ 将耗时的"落库"操作从主链路中剥离,实现了极致的发送速度

3. 代码实现细节分析 (rabbitmq.hpp)

  • 底层库 :使用了 AMQP-CPP 库,配合 libev 事件循环库。这意味着 MQ 的客户端是 全异步、非阻塞 的,非常适合高并发场景
  • 核心方法 :
    • declareComponents : 声明交换机 (Exchange) 和队列 (Queue) 并绑定。这里使用的是 direct 类型交换机,通过 routing_key 精确匹配。
    • publish : 发送消息。注意它是异步的,调用后立即返回
    • consume : 消费消息。通过注册回调函数 cb 来处理收到的消息,处理完后必须手动 ack (确认),确保消息不丢失
    • 线程模型 :开启了一个独立的线程 _loop_thread 运行 ev_run 事件循环,处理网络包的收发,不会阻塞主业务线程。

3. 总结

RabbitMQ 是该即时通讯系统应对 高并发写入 的"减震器"。它用极小的延迟换取了系统极大的稳定性,是工业级 IM 架构中不可或缺的组件。

4. 待持久化的聊天消息的存储详细过程

"待持久化的聊天消息"是指那些已经由用户发送、经过转发服务处理,但尚未写入数据库和搜索引擎的"在途"消息。整个持久化过程是一个经典的生产者-消费者模型,涉及多个服务和组件的协作

以下是代码级别的详细解析,涉及 Transmite Service(生产者)和 Message Service(消费者)两部分。

4.1 第一阶段:消息生产 (Transmite Service)

当用户发送消息时, Transmite Service 负责将消息封装并发布到 MQ,而不直接写库。

核心代码位置 : transmite_server.hpp 中的 GetTransmitTarget 方法。

  1. 完整性校验与封装 :

    • 调用 User Service : 根据 user_id 获取发送者的详细信息(昵称、头像等),因为前端发来的消息只有 ID。
      • transmite_server.hpp:L67 : stub.GetUserInfo(...)
    • 生成 MessageID : 为消息生成全局唯一的 message_id 。
      • transmite_server.hpp:L73 : message.set_message_id(uuid())
  2. 发布到 MQ :

    • 序列化 : 将封装好的 MessageInfo 对象序列化为二进制字符串。
    • 异步发布 : 调用 _mq_client->publish 将消息发送到 RabbitMQ 的 msg_exchange 交换机。
    • transmite_server.hpp:L81 : _mq_client->publish(_exchange_name, message.SerializeAsString(), ...)
    • 关键点 : 这里只发 MQ,不写 MySQL。只要 MQ 返回 true ,就认为消息发送成功,极大降低了用户等待时间。

4.2 第二阶段:消息消费与持久化 (Message Service)

Message Service 作为一个后台消费者,持续从 MQ 拉取消息并落库。

核心代码位置 : message_server.hpp 中的 onMessage 方法。

  1. 监听与反序列化 :

    • Message Service 启动时会订阅 MQ 队列,当有新消息时触发 onMessage 回调。
    • message_server.hpp:L255 : void onMessage(const char *body, size_t sz)
    • message_server.hpp:L259 : message.ParseFromArray(body, sz)
  2. 分类处理与文件上传 :

    • 如果是 文本消息 :直接准备存入 ES。
    • 如果是 多媒体消息 (图片/语音/文件):
      • 消息体中包含文件的二进制数据,不能直接存 MySQL(太大了)。
      • 调用 File Service 上传文件,获取返回的 file_id 。
      • message_server.hpp:L286 : _PutFile(...)
  3. 双重持久化 (Dual Persistence) :

    • 写 ES (全文检索) :
      • 仅对文本消息 ( MessageType::STRING ) 建立索引,方便后续搜索。
      • message_server.hpp:L271 : _es_message->appendData(...)
    • 写 MySQL (永久存储) :
      • 无论什么类型的消息,都要存入 MySQL。对于文件消息,只存 file_id 、 file_name 等元数据,不存文件内容。
      • message_server.hpp:L329 : _mysql_message->insert(msg)

4.3 总结

整个过程的代码流向为: NewMessageReq (用户请求)

-> Transmite::GetTransmitTarget (补全信息)

-> MQ Publish (RabbitMQ)

-> Message::onMessage (异步消费)

-> File Service (上传大文件)

-> MySQL Insert & ES Index (最终落盘)

这种设计确保了高并发下的系统稳定性,即使 MySQL 暂时写入缓慢,MQ 也能堆积消息,不会阻塞用户的发送操作。

5. 消息的一致性

"消息的一致性"是 IM 系统的核心挑战,即 确保消息不丢、不重、不乱 。在本项目中,主要通过 RabbitMQ 的可靠性机制 和 业务层的去重逻辑 来保证。

以下是代码参与的关键环节:

5.1 生产端一致性:确保消息成功发给 MQ

在 Transmite Service 中,必须保证消息成功投递到 MQ,否则会返回错误给客户端,让客户端重试。

核心代码 : transmite_server.hpp:L81-L85

cpp 复制代码
// 尝试发布消息
bool ret = _mq_client->publish
(_exchange_name, message.
SerializeAsString(), _routing_key);
if (ret == false) {
    // 如果发布失败(例如网络断了),打印
    错误并返回失败响应
    // 客户端收到失败响应后,会触发重试机
    制(客户端逻辑)
    return err_response
    (request->request_id(), "持久化消
    息发布失败:!");
}
  • 机制 :同步检查 publish 的返回值。虽然 AMQP-CPP 的 publish 是异步的,但如果有连接级错误会立即感知
  • 优化空间 :当前代码只检查了 publish 调用的返回值。更严格的做法是开启 RabbitMQ 的 Confirm 模式 (Publisher Confirms),等待 Broker 返回 Ack 才算真正成功。

5.2 消费端一致性:确保消息成功落库

在 Message Service 中,必须保证从 MQ 拿到的消息被成功写入 MySQL/ES,不能"拿出来就丢了"。

核心代码 : rabbitmq.hpp:L89

cpp 复制代码
_channel->consume(queue, 
"consume-tag")
    .onReceived([this, cb](const 
    AMQP::Message &message, 
    uint64_t deliveryTag, bool 
    redelivered) {
        // 1. 执行业务回调(即 
        MessageServer::onMessage,负
        责写库)
        cb(message.body(), message.
        bodySize()); 
        
        // 2. 只有业务处理完了,才手动发
        送 Ack 给 MQ
        _channel->ack(deliveryTag);
    })
  • 机制 : 手动 Ack (Manual Acknowledge) 。
    • RabbitMQ 推送消息给消费者后,消息处于 Unacked 状态。
    • 只有当 cb(...) (即写数据库逻辑)执行完毕后,代码才调用 _channel->ack(deliveryTag) 。
    • 异常处理 :如果 Message Service 在执行 cb 过程中挂了(没来得及 Ack),RabbitMQ 会发现连接断开,自动将该消息 重新入队 ,发给其他消费者,确保 消息不丢失 。

5.3 幂等性:防止消息重复

由于 RabbitMQ 的"失败重试"机制(或者网络抖动), Message Service 可能会收到两条一样的消息。如果不处理,数据库会有重复聊天记录。

核心代码 : message_server.hpp:L320-L329

cpp 复制代码
// 构造消息对象,包含全局唯一的 
message_id
bite_im::Message msg(message.
message_id(), ...);

// 尝试插入 MySQL
ret = _mysql_message->insert(msg);
  • 机制 : 数据库唯一索引 (Unique Key) 。
    • 虽然代码里没明写"去重逻辑",但 message_id 是由 UUID 生成的全局唯一 ID。
    • 在 MySQL 建表时( sql/message.sql ), message_id 应该是主键或唯一索引。
    • 当插入重复的 message_id 时,MySQL 会报错( Duplicate entry ),插入失败。这样就天然保证了 业务幂等性 。

总结

  1. 发没发成功? 靠 Transmite Service 检查 MQ 发布结果 + 客户端超时重试。
  2. 存没存成功? 靠 RabbitMQ 的 手动 Ack 机制,存完再删 MQ 里的消息。
  3. 会不会重? 靠 MySQL 的 message_id 唯一约束 自动去重。
相关推荐
waves浪游2 小时前
Ext系列文件系统
linux·服务器·开发语言·c++·numpy
XH华2 小时前
备战蓝桥杯,第五章:string字符串
c++·职场和发展·蓝桥杯
Francek Chen2 小时前
【大数据基础】大数据处理架构Hadoop:03 Hadoop的安装与使用
大数据·hadoop·分布式·架构
pcm1235672 小时前
设计C/S架构的IM通信软件(2)
java·c语言·架构
2301_817497332 小时前
C++中的适配器模式实战
开发语言·c++·算法
HellowAmy2 小时前
我的C++规范 - 数据存储器
开发语言·c++·代码规范
Max_uuc2 小时前
【C++ 硬核】消灭 void*:用 std::variant 实现嵌入式“类型安全”的多态 (Type-Safe Union)
开发语言·c++
枫叶丹42 小时前
【Qt开发】Qt系统(十)-> Qt HTTP Client
c语言·开发语言·网络·c++·qt·http
王老师青少年编程2 小时前
2025信奥赛C++提高组csp-s复赛真题及题解:道路修复
c++·真题·csp·信奥赛·csp-s·提高组·复赛