门店桌台状态在收银台、平板、小程序间对不上:WebSocket实时同步、断线重连与状态对账实践

简介: 门店桌台状态要同时显示在收银大屏、服务员平板和顾客小程序上,早期用轮询既有延迟空窗,还常出现多端状态不一致、重复开台。本文讲改用WebSocket实时同步并把长连接做稳的完整方案:应用层心跳保活加指数退避断线重连消除假在线,给状态加单调版本号挡住消息乱序与重复,服务端作为唯一权威、重连先拉快照对齐再加定时对账兜底,附关键代码与五个踩坑。

导读

线下门店的桌台状态,是要同时出现在好几块屏幕上的:收银台的大屏、服务员手里的平板、顾客那边的小程序,看到的都得是同一份。我们最早图省事用轮询,每隔几秒各自去拉一次状态,结果现场经常对不上——服务员平板上刚开了台,收银大屏还显示空闲,另一头又给同一桌下了一单,等发现时已经乱了。轮询不仅有延迟空窗,还白白发了大量请求。这篇讲我们后来怎么改成 WebSocket 实时同步,并把长连接真正用稳:心跳保活与断线重连、消息版本号防乱序防重复、服务端权威加多端对账,附上关键代码和五个踩坑。

一、先看清:多端实时同步到底难在哪

轮询的问题很直观:拉取间隔就是状态不一致的空窗,间隔越短越浪费、越长越滞后。换成 WebSocket 长连接,服务端状态一变就主动推,方向是对的,但长连接把一个问题换成了四个:连接会断(门店 WiFi 抖动、平板锁屏、切后台),消息会乱、会重、会丢(网络重传导致重复、多路径导致乱序、断线期间漏消息),多个端还会同时改同一张桌台。只把轮询换成 socket 而不处理这四件事,状态只会错得更隐蔽。下面三层就是分别对应它们。

二、连接层:心跳保活加断线重连,别留"假在线"

TCP 连接在移动端不会立刻感知到断开,网线拔了、路由切换了,套接字可能半天才报错,这期间应用层以为还连着,消息发出去石沉大海,这就是假在线。必须在应用层加心跳定时探活,超时没回应就主动判定断线并重连,重连用指数退避,避免一堆客户端同时重连把服务端打穿:

class SocketClient {
   
  constructor(url) {
   
    this.url = url;
    this.retry = 0;
    this.connect();
  }
  connect() {
   
    this.ws = new WebSocket(this.url);
    this.ws.onopen = () => {
    this.retry = 0; this.startHeartbeat(); this.resync(); };
    this.ws.onmessage = (e) => this.dispatch(JSON.parse(e.data));
    this.ws.onclose = () => this.reconnect();
  }
  startHeartbeat() {
   
    clearInterval(this.timer);
    this.timer = setInterval(() => {
   
      this.ws.send(JSON.stringify({
    type: 'ping' }));
    }, 25000); // 固定节奏发心跳
  }
  reconnect() {
   
    clearInterval(this.timer);
    const delay = Math.min(1000 * 2 ** this.retry++, 30000); // 指数退避,封顶30s
    setTimeout(() => this.connect(), delay);
  }
}

注意 onopen 里除了启动心跳,还调了一个 resync(),这是下一节的关键——重连成功后绝不能直接沿用本地旧状态。

三、消息层:给状态加版本号,挡住乱序和重复

实时推送下,同一桌台的状态变更可能连着来好几条。如果"消息到了就直接覆盖",一条因为重试迟到的旧消息,就可能把最新状态盖回去。做法是给每张桌台的状态带一个单调递增的版本号,客户端只接受比本地更新的版本,旧版本和重复版本直接丢弃:

dispatch(msg) {
   
  if (msg.type !== 'table-state') return;
  const local = this.tables[msg.tableId];
  // 版本不大于本地,说明是迟到的旧消息或重复消息,丢弃
  if (local && msg.version <= local.version) return;
  this.tables[msg.tableId] = {
    status: msg.status, version: msg.version };
  this.render(msg.tableId);
}

服务端推进状态时同样用条件更新兜底,保证并发下版本号不会倒退、同一次开台不会重复生效:

UPDATE table_state
SET status = ?, version = version + 1
WHERE table_id = ? AND version = ?;
-- 影响行数为 0,说明期间已被别的端改过,本次提交要重新拉取再决策

这样无论消息重复投递还是乱序到达,最终落到每块屏幕上的状态都是收敛且一致的。

四、状态层:服务端权威,重连先对账,再用定时对账兜底

客户端的状态永远只是服务端的一份缓存,服务端才是唯一事实源。客户端操作可以先乐观渲染让操作跟手,但必须随时准备被服务端的权威状态纠正,绝不能反过来以客户端为准——否则两个端同时开同一桌,必然冲突。

最重要的是重连后的补拉。断线那一会儿服务端可能已经变更好几次,单靠补订阅收不到错过的消息,因此重连后要先拉一次当前全量快照对齐,再进入增量推送:

async resync() {
   
  // 重连后先拉权威快照,覆盖本地,再继续收增量
  const snapshot = await fetch('/api/tables/snapshot').then(r => r.json());
  snapshot.forEach(t => {
    this.tables[t.id] = {
    status: t.status, version: t.version }; });
  this.renderAll();
}

实时通道再稳也可能漏消息,所以还要有一道不依赖推送的兜底:每隔一段时间、以及每次进前台时做一次轻量对账,比对本地版本和服务端版本,发现落后就拉齐。

这里交代下选型背景。这套实时层是搭在门店一站式 SaaS 底座上做的,这里用的是乔拓云:底座把商品、会员、订单这些通用模块和多端界面托管掉,也提供了基础的消息下发通道;但"桌台状态在多块屏幕之间严格一致"这种贴着具体门店流程的实时逻辑,底座给的通道只是一根管道,状态机、版本号、重连补拉和对账都得我们在通道之上自己实现。哪些直接用、哪些必须自研,开工前划清楚,后面才不会互相甩锅。

五、踩坑清单

  • 坑1:建了 WebSocket 却不做应用层心跳。移动网络下会出现大量假在线连接,以为消息发出去了其实早断了,必须固定心跳加超时判定,断线立刻走重连。
  • 坑2:消息到了就覆盖,不带版本号。重试迟到的旧消息会把新状态盖回去,必须给状态配单调版本,客户端只认更高版本,乱序和重复一律丢弃。
  • 坑3:重连后直接沿用本地状态。断线期间的变更全丢了,重连成功后要先拉一次权威快照对齐,再进入增量订阅,顺序不能反。
  • 坑4:以客户端状态为准、多端各自改。并发开台必然冲突,服务端才是唯一事实源,客户端只做乐观呈现并随时接受纠正,提交走条件更新。
  • 坑5:只靠实时推送、不做对账兜底。漏推一条就永久不一致,必须有定时或回前台时的全量/版本对账作为最终纠偏手段,实时和对账两条腿走路。

结语

多端状态同步的难点从来不是"把 WebSocket 连上",而是连接之后那一堆脏活:心跳和重连解决连接不可靠,版本号解决消息乱序重复,服务端权威加快照对账解决断线遗漏和并发冲突。把这三层补齐,桌台状态在收银台、平板、小程序之间才能真正做到同屏同态。这套思路并不只属于桌台,库存、叫号、工单这类需要多端实时一致的场景,面对的是同一组问题,照着这三层搭基本不会错。

相关文章
|
2月前
|
缓存 NoSQL 测试技术
商品详情页高并发读:多级缓存与击穿、雪崩、穿透三件套防护实践
商品详情页是商城读流量最大的入口,大促时缓存一旦击穿、雪崩、穿透,会直接打垮数据库。本文讲清多级缓存架构(本地缓存+Redis+数据库)与三类故障的成因和防护:互斥重建、逻辑过期、布隆过滤器,并给出Java与Redis命令和五个生产踩坑。
|
监控 安全 网络安全
安全防御之授权和访问控制技术
授权和访问控制技术是安全防御中的重要组成部分,主要用于管理和限制对系统资源(如数据、应用程序等)的访问。授权控制用户可访问和操作的系统资源,而访问控制技术则负责在授权的基础上,确保只有经过授权的用户才能访问相应的资源。
1152 0
|
机器学习/深度学习 传感器 数据采集
机器学习赋能制造业:预测性维护、质量控制和智能物流优化
制造业借助机器学习和深度学习提升效率,通过预测性维护避免设备故障,利用质量控制模型检测产品缺陷,及运用智能物流优化降低运输成本。示例代码包括基于LSTM的设备故障预测和随机森林分类器的质量控制模型。这些技术革新生产流程,提高效率,降低成本,增强企业竞争力。
|
监控 前端开发 JavaScript
Qt Quick调试之道:跟踪、输出与打印信息的全面攻略
Qt Quick调试之道:跟踪、输出与打印信息的全面攻略
1458 0
Squid 安装与配置
本文转载自:https://help.aliyun.com/knowledge_detail/41342.html Squid 介绍 Squid 是一个缓存 Internet 数据的软件,其接收用户的下载申请,并自动处理所下载的数据。
2086 0
|
2月前
|
存储 人工智能 安全
DeepSeek Harness深度解析:核心功能、Cordis内核架构与本地部署实战教程
DeepSeek Harness 是由 DeepSeek AI 开发的开源 agent harness(智能体框架),采用“一切皆插件”的架构设计,由 Cordis 微内核驱动。该框架解决了传统 AI 应用开发中工具调用硬编码、平台适配成本高等痛点,通过插件化架构实现模型、工具、技能的自由组合,大幅降低 Agent 开发门槛。
469 0
|
3月前
|
IDE 编译器 开发工具
【devc++】Dev C++下载+安装+汉化+使用一篇搞定(2026全网最详细)
Dev C++ 是一款免费开源、轻量易用的 Windows C/C++ 集成开发环境(IDE),内置 MinGW 编译器,安装即用。支持一键编译(F9)和编译运行(F11),界面简洁,汉化完善,是高校教学、计算机等级考试及编程入门首选工具。
57781 7
|
2月前
|
前端开发 NoSQL 数据库
商城重复下单怎么彻底防住:前端防抖、接口幂等与唯一约束的三层防线
重复下单不只来自用户连点,还包括前端超时重试、刷新重放、消息重复投递和并发插入。本文搭建三层纵深防线:前端用提交状态锁改善体验,服务端用预签发幂等键加Redis去重保证同一请求只处理一次,数据库用唯一索引和状态机条件更新做永不失效的兜底,附可复用代码与5个踩坑点。