std::function无法用于跨进程RPC事件总线,因其不可序列化、无法自动路由远程服务,强行使用会导致运行时panic;真正需解耦的是事件契约与传输协议,事件必须是可序列化的POD,依赖Protobuf定义跨语言兼容结构体,并通过抽象Transport接口(如Zmq/Grpc/Kafka)实现publish/subscribe的透明性。

为什么不能直接用 std::function + 队列模拟事件总线
因为 RPC 场景下,事件发布者和订阅者大概率跨进程(甚至跨机器),std::function 无法序列化,也无法自动路由到远程服务。硬塞进去只会导致编译通过但运行时 panic——比如 std::bad_function_call 或空指针解引用,尤其在异步回调中极难 debug。
真正要解耦的不是“函数调用”,而是“事件契约”与“传输协议”的分离。核心是让服务只依赖事件结构体(POD)和注册接口,不感知序列化方式、网络层、重试逻辑。
- 事件必须是可序列化的 Plain Old Data:禁用虚函数、指针成员、非 public 构造函数
- 每个事件类型需有唯一
event_id(如uint16_t),用于反序列化时路由到正确 handler - 发布端不关心谁订阅,订阅端不关心谁发布,两者只通过
EventBus::publish()和EventBus::subscribe交互()
如何定义跨语言兼容的事件结构体
用 Protobuf 是最稳妥的选择——C++ 生成的 MyEvent::SerializeAsString() 输出可被 Go/Python/Java 直接解析,且字段增删向后兼容。别手写 JSON 或自定义二进制格式,除非你已准备好长期维护 schema 版本映射表。
关键约束:
- 所有字段设
optional(proto3 默认),避免新增字段导致旧服务解析失败 - 禁止使用
oneof做业务分支判断——它会让 C++ 反序列化后需手动has_xxx()判断,易漏处理 - 为每个 event 定义明确的
enum EventType,与 Protobuf message 一一对应,用于注册时校验
示例:
message UserCreatedEvent {
optional int64 user_id = 1;
optional string email = 2;
optional int64 timestamp_ms = 3;
}
对应 C++ 中 UserCreatedEvent 类型,注册时传入 EventType::kUserCreated。
如何实现 subscribe/publish 的跨进程透明性
底层必须抽象出 Transport 接口,常见实现有:ZmqTransport(低延迟局域网)、GrpcTransport(强一致性、带流控)、KafkaTransport(高吞吐、持久化)。业务代码完全不感知差异——只要 EventBus 构造时注入具体 transport 实例。
关键设计点:
-
subscribe内部会将() T::descriptor()(或硬编码event_id)注册到本地 handler 表,并通知 transport 层监听该 event_id 的网络消息 -
publish(const T&)先序列化为std::string,再交由 transport 发送;transport 负责添加 header(含 event_id、trace_id、压缩标记) - 务必支持
publish的异步回调(std::function),否则网络超时会卡死业务线程
错误典型:直接在 publish() 里阻塞等待 ACK —— 一旦 Kafka 分区不可用,整个服务 publish 调用全部 hang 住。
如何避免事件重复消费与顺序错乱
RPC 事件总线默认不保证 exactly-once,只承诺 at-least-once。所以业务 handler 必须幂等:比如用 user_id + event_id + timestamp_ms 拼成唯一 key 存 Redis,先查后处理。
顺序问题更隐蔽:Kafka 单 partition 有序,但多 service 实例订阅同一 topic 时,若未按 user_id % N 做 key 分区,同一用户的多个事件可能被不同实例消费,导致状态不一致。
- 强制要求事件携带
sharding_key字段(如user_id),transport 层据此哈希到固定分区 - 禁止在 handler 中做耗时操作(如 HTTP 调用),否则会拖慢同分区其他事件;必须异步派发到 worker 线程池
- 日志中必须打印
event_id和trace_id,否则线上排查重复/乱序毫无头绪
最容易被忽略的是 transport 层的重试策略——ZMQ 的 REQ/REP 模式天然不支持重试,而 gRPC streaming 必须手动处理 DEADLINE_EXCEEDED 后的 reconnect 与 offset 恢复。这些细节不提前约定,上线后就是半夜告警。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











