三条我都认,但选主那条我建议别自己手写心跳——浏览器的定时器节流会让这套逻辑在真实场景里误判。
后台标签页的 setTimeout 会被 Chrome 压到 1 分钟甚至更久,主标签一旦切到后台,2s 心跳就变成了 60s 一次,其他标签 5s 收不到直接集体竞选,结果就是「谁在后台谁被弹劾」。用 Web Locks API 才是对的:navigator.locks.request('ws-leader', {mode:'exclusive'}, () => new Promise(() => {})) 把锁一直握到标签销毁,锁随标签关闭/崩溃由浏览器自动释放,其他标签在 locks 队列里静默等着,拿到即接管,不需要任何心跳和竞选广播。降级才回到 BroadcastChannel + 心跳那套。另外别把「拉未读数」也绑给主标签——这个活儿谁可见谁干,visibilitychange 里每个标签自己拉一次就完了,绑给主角色反而多一层故障点。
full jitter 我服,固定档位乘系数确实只是错开一点。你提到 at-least-once,那去重的 key 得说死:服务端发单调递增的 seq(Redis INCR 或库里的自增),前端按 uid+seq 存最近 200 条的 LRU,只管「响铃弹窗」这一层,角标更新本身幂等,照更不误——这样同一条消息在三个标签页也只有主标签响一次。注意 processing 残留重入队那一步也会重复投递,去重不能省。
结论我同意:Clara 是 PHP + MySQL、不依赖 Redis,真上 Workerman 就得额外背宝塔守护、自动重启、反代三件套。站内「有新回复/被 @」用自带 notify 通知挂插件钩子,配 Cron::register 懒触发定时任务轮询兜底就够,WebSocket 留给实时聊天和在线状态。

正式会员







