FrankenPHP+Mercure不保证消息顺序,需业务层通过单worker串行publish、服务端生成单调序号、前端按seq排序及后端持久化兜底来保障。

FrankenPHP + Mercure 本身不保证消息顺序
Mercure 是一个基于 Server-Sent Events(SSE)的发布/订阅协议实现,它只负责把消息从服务端推给客户端,**不提供任何顺序性语义保障**。即使你用 FrankenPHP 启动 Mercure Hub,它也不会对 publish 调用做序列化、排队或偏移量管理。如果你并发调用两次 publish(),且底层 HTTP 连接或内核调度有微小差异,接收端就可能看到乱序——尤其在弱网、多 tab、重连场景下。
真正起作用的是“谁发消息”和“怎么发”
聊天室的消息顺序,本质是业务层对“同一会话上下文内事件时序”的约束。Mercure 只是管道,关键控制点在:
- 所有发往同一聊天室(比如
room:123)的消息,必须由**同一个 PHP worker 进程**串行处理并publish();不能多个 worker 同时向同一个 topic 写入 - 消息体里必须携带明确的、服务端生成的单调递增序号(如
message_id或seq),而不是依赖客户端时间戳或自增 ID - 前端收到 SSE 消息后,不能按接收顺序直接渲染,而要依据
seq字段做内存排序或丢弃旧序号消息(类似 TCP 的滑动窗口)
FrankenPHP 的 worker 模式正好能帮你锁住第一个条件:启用 php-worker 并让所有聊天消息路由到该 worker,就能避免多进程并发 publish 导致的写入竞态。
别踩这个坑:用 Mercure 的 private 模式当消息队列
有人会想把用户私有 topic(如 user:456)当消息通道,靠 Mercure 的 JWT 鉴权来投递“属于某人的消息”。这会导致两个严重问题:
立即学习“PHP免费学习笔记(深入)”;
- 群聊消息要为每个在线用户分别
publish()一次,放大 N 倍流量,且无法保证这些 publish 调用的全局顺序 - Mercure Hub 不提供“批量原子推送”或“事务性广播”,某次 publish 失败不会回滚其他已发消息,造成部分用户看到跳号或缺失
正确做法是:统一用公开 topic(如 room:123),前端通过 mercure.subscribe() 订阅,服务端只做一次 publish();顺序控制交给 worker + 序号 + 前端缓冲,而不是靠 Mercure 自身。
复杂点在于重连与断线重续
Mercure 的 SSE 协议本身支持 Last-Event-ID,但 FrankenPHP 默认的 Mercure Hub 实现(基于 Symfony Mercure Bundle)对它的支持较弱,且不保存历史消息。这意味着用户切后台、网络中断再回来时,会丢失中间消息——你没法靠 Mercure 自己补全顺序。
所以实际项目中,seq 字段必须配合后端持久化(比如 Redis Stream 的 XRANGE 或 MySQL 的 created_at + id 复合查询)做兜底拉取。否则“顺序”只在连接活跃时成立,一断就碎。



















