线下门店一旦同时有取号机、几个窗口的叫号屏、员工手持终端在操作同一个排队队列,问题就来了:两台取号机打出重复号码,两个窗口叫到同一个号,过号之后屏幕还显示在等待,顾客重取的号和原号对不上。这些看似是界面问题,根子都在“序号从哪来、状态由谁定、变更怎么同步”。这篇给一套可直接落地的排队队列设计。
先把典型故障归类,后面方案逐条对应:
共性原因是序号有多来源、状态变更没有原子性、各终端各自维护一份队列。解决思路也对应三句话:序号单一来源、状态集中管理、变更统一广播。
无论有多少取号入口,序号都不能在终端本地生成,必须向同一个序号源申请。实践中按“门店+业务线+日期”划分独立队列,每条队列维护一个自增序列,取号即对该序列做一次原子递增:
# 以键 queue:{门店}:{业务线}:{日期}:seq 记录当日已发到的最大序号
INCR queue:store01:normal:20260916:seq拿到的序号配合队列前缀展示(如 A023),序号只增不复用。选择序号源时把握两点:递增操作必须是原子的,避免两个请求读到同一个值;序列按自然日重置,第二天从初始号开始,避免跨天混号。业务线(如个人业务、对公业务)分开计数,不同队列互不影响。
每个号码从取走到结束,应有一条确定的状态流转路径,而不是用几个零散布尔值拼:
已取号(WAITING)→ 已叫号(CALLING)→ 办理中(SERVING)→ 已完成(DONE);任意环节可进入 过号(PASSED)或 弃号(CANCELLED),过号可被重新叫回。
队列主记录建议落库,缓存承载热状态,表结构至少包含序号、队列、状态、取号时间、各状态变更时间和所在窗口:
CREATE TABLE queue_ticket (
id BIGINT PRIMARY KEY,
store_id VARCHAR(32) NOT NULL,
queue_key VARCHAR(32) NOT NULL, -- 业务线+日期
ticket_no INT NOT NULL, -- 当日序号
status VARCHAR(16) NOT NULL, -- 状态机取值
window_no VARCHAR(16),
created_at DATETIME NOT NULL,
called_at DATETIME,
finished_at DATETIME,
UNIQUE KEY uk_queue_ticket (queue_key, ticket_no)
);唯一约束从存储层再兜一次底,即使上游出现异常也不会插入同一队列的重复序号。
“叫下一个”是并发竞争最集中的动作:多个窗口同时点,必须保证一个待叫号码只被一个窗口拿到。做法是在单一数据源上用原子操作弹出当前队首并置为已叫号,下面是一段示意的原子脚本逻辑:
-- KEY: 队列有序集合(score=序号, member=号码);ARGV: 窗口号、当前时间
local waiting = redis.call('ZRANGE', KEYS[1], 0, 0)
if #waiting == 0 then return nil end
local ticket = waiting[1]
-- 只有仍处于 WAITING 才允许被叫,避免重复
local ok = redis.call('HSETNX', 'call:'..ticket, 'window', ARGV[1])
if ok == 0 then return nil end
redis.call('ZREM', KEYS[1], ticket)
redis.call('HSET', 'call:'..ticket, 'status', 'CALLING', 'at', ARGV[2])
return ticket要点是“检查待叫+占用号码”在一个原子操作内完成,占用成功才返回号码,失败的窗口拿到空结果再重试,自然不会两个窗口叫到同一位。叫号后给一个确认时限,超时未确认办理则号码回到待叫或转过号,避免号码被“叫走却没人跟”。
所有叫号、过号、完成动作都只改服务端这一份权威状态,终端不自行推算。变更发生后,由服务端向该门店所有在线终端广播状态变更事件,大屏、手持端、顾客端订阅同一频道:
// 终端订阅示意:收到事件就按服务端给的全量/增量状态渲染
subscriber.on("queue:event", (event) => {
if (event.storeId !== currentStore) return;
applyQueueEvent(event); // 增量更新本地视图
renderBoard(currentSnapshot());
});
// 断线重连不补发历史,而是拉一次该队列的权威快照对齐
subscriber.onReconnect(async () => {
const snapshot = await fetchQueueSnapshot(queueKey, lastVersion);
replaceWithSnapshot(snapshot);
});两个关键设计:一是每条变更带单调递增的版本号,终端据此判断是否漏事件;二是断线重连后不信任本地缓存,直接拉一次权威快照对齐,从机制上杜绝“重连后停在旧队列”。
特殊情况要在状态机里提前定死,避免现场自由发挥:
建议把排队能力做成门店系统里的独立服务,对内提供取号、叫号、过号、完成、查询快照五类接口,序号生成、状态流转、并发控制收敛在服务端,终端只负责展示和触发。队列热状态放缓存、主记录落库并加唯一约束,事件广播带版本号,重连以快照对齐。这套抽象不绑定具体行业,银行、政务、餐饮、医疗等需要多窗口排队的门店场景都能复用。
Q:取号机断网了还能取号吗?
A:取号必须向单一序号源申请,断网时不应在本地先编号,避免恢复后重号;可短暂排队等网络恢复,或切换到备用的统一序号源。
Q:过号和弃号有什么区别?
A:过号是叫到时未响应、仍可能回来办理,保留号码可重叫;弃号是顾客明确不再办理,状态终止、序号不回收。
Q:多个业务线要共用一个队列吗?
A:通常按业务线分列计数和叫号,避免办理时长差异大的业务互相阻塞;大厅可统一展示,但序号和状态各自独立。
多终端排队不乱的关键,不是把界面做多复杂,而是让序号只有一个来源、状态只有一份权威、每次变更都原子且可同步。把这三点在服务端做扎实,再多取号入口和叫号窗口也能保持同一个秩序。本文为后端工程实践分享,具体实现以业务规则为准。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。