返回首页

电影院 QR 票务验证系统设计

本文详解了电影院 QR 票务验证系统的设计。从离线签名到带 PostgreSQL 事务的幂等 API。讨论了超时问题、HoL 阻塞和权衡取舍。

如何设计电影院可靠的票务验证
Advertisement 728x90

QR票务核验系统设计:从离线模式到幂等性保障

检票员扫描二维码,系统在数据库中检查并标记为已使用。核心要求:一票一入。看似简单的任务因网络故障、排队和多检票员而变得复杂。让我们探讨从离线核验到可靠服务器端处理的解决方案演进。

离线模式使用数字签名(ECC或RSA)。二维码包含票务数据和签名;检票员使用公钥验证,无需网络连接。问题:检票员之间缺乏同步。票务截图可在每个闸机独立使用。

服务器端状态与原子更新

集中式存储系统解决了同步问题。每次扫描都是向服务器发送的请求,附带原子状态更新。

Google AdInline article slot

PostgreSQL示例:

UPDATE tickets 
SET status = 'used', used_at = NOW()
WHERE id = :ticket_id AND status = 'active'
RETURNING *;

如果返回行,票务已兑换并允许入场。如果无行返回,票务已使用或不存在。

经典问题:丢失服务器响应。检票员在信号弱区域发送请求,服务器更新数据库,但响应未到达。发生超时,尝试重新扫描——服务器拒绝,客户端尽管有付费票却无法入场。

Google AdInline article slot

API层幂等性

解决方案是使用唯一键(UUID)的幂等请求,在打开扫描会话时生成。该键记录尝试,允许重试而无重复。

事务内逻辑(读已提交):

begin ;

-- 1. 记录尝试
INSERT INTO processed_requests (ticket_id, idempotency_key)
VALUES (:ticket_id, :idempotency_key)
ON CONFLICT (ticket_id, idempotency_key) 
DO NOTHING RETURNING *;

-- 2. 如果 rows_affected == 0: 回滚,返回 SUCCESS

-- 3. 兑换票务
UPDATE tickets 
SET status = 'used', used_at = NOW()
WHERE id = :ticket_id AND status = 'active'
RETURNING *;

-- 4. 如果 rows_affected == 0: 回滚,返回 ERROR

commit;
-- 5. 返回 SUCCESS

事务确保原子性:要么更新键和状态,要么无操作。在 (ticket_id, idempotency_key) 上的唯一索引防止竞态条件——第二个请求等待或执行 DO NOTHING。

Google AdInline article slot

同一会话内的重试返回成功,即使首次响应丢失。

队头阻塞与上下文丢失

实际问题:检票员在排队压力下关闭会话而无响应。新扫描生成新的 idempotency_key。服务器看到已使用票务带有不同键——拒绝。

检票员无法区分合法客户与使用截图的欺诈者。队头阻塞要求在下张票前获得最终响应。

折中方案:返回最后使用时间戳。检票员评估:“2分钟前——可能是同一人。”风险:共享二维码的团体顺序通过。

关键挑战:

  • 无状态丢失的网络超时
  • 多检票员同步
  • 排队中的人为因素
  • 平衡可靠性与用户体验

关键要点

  • 带条件 status='active' 的原子 UPDATE 防止双重兑换
  • 通过 UUID + 唯一索引的幂等性保证重试无重复
  • 事务(读已提交)确保变更的顺序可见性
  • 队头阻塞对于无生物识别的100%可靠性不可避免
  • 时间戳折中将决策转移给操作员

未来改进

静态二维码正变得过时。动态令牌或带一次性OTP的挑战-响应解决问题。检票员从服务器请求随机数,客户端签名——服务器验证并原子标记。

生物识别(人脸ID)或NFC消除截图但需要硬件。QUIC最小化丢包但不解决上下文丢失。

扩展:Redis缓存 idempotency_keys,按 ticket_id 分片。监控:按时间戳的失败扫描指标,折中方案的A/B测试。

— Editorial Team

Advertisement 728x90

继续阅读