QR票务核验系统设计:从离线模式到幂等性保障
检票员扫描二维码,系统在数据库中检查并标记为已使用。核心要求:一票一入。看似简单的任务因网络故障、排队和多检票员而变得复杂。让我们探讨从离线核验到可靠服务器端处理的解决方案演进。
离线模式使用数字签名(ECC或RSA)。二维码包含票务数据和签名;检票员使用公钥验证,无需网络连接。问题:检票员之间缺乏同步。票务截图可在每个闸机独立使用。
服务器端状态与原子更新
集中式存储系统解决了同步问题。每次扫描都是向服务器发送的请求,附带原子状态更新。
PostgreSQL示例:
UPDATE tickets
SET status = 'used', used_at = NOW()
WHERE id = :ticket_id AND status = 'active'
RETURNING *;
如果返回行,票务已兑换并允许入场。如果无行返回,票务已使用或不存在。
经典问题:丢失服务器响应。检票员在信号弱区域发送请求,服务器更新数据库,但响应未到达。发生超时,尝试重新扫描——服务器拒绝,客户端尽管有付费票却无法入场。
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。
同一会话内的重试返回成功,即使首次响应丢失。
队头阻塞与上下文丢失
实际问题:检票员在排队压力下关闭会话而无响应。新扫描生成新的 idempotency_key。服务器看到已使用票务带有不同键——拒绝。
检票员无法区分合法客户与使用截图的欺诈者。队头阻塞要求在下张票前获得最终响应。
折中方案:返回最后使用时间戳。检票员评估:“2分钟前——可能是同一人。”风险:共享二维码的团体顺序通过。
关键挑战:
- 无状态丢失的网络超时
- 多检票员同步
- 排队中的人为因素
- 平衡可靠性与用户体验
关键要点
- 带条件 status='active' 的原子 UPDATE 防止双重兑换
- 通过 UUID + 唯一索引的幂等性保证重试无重复
- 事务(读已提交)确保变更的顺序可见性
- 队头阻塞对于无生物识别的100%可靠性不可避免
- 时间戳折中将决策转移给操作员
未来改进
静态二维码正变得过时。动态令牌或带一次性OTP的挑战-响应解决问题。检票员从服务器请求随机数,客户端签名——服务器验证并原子标记。
生物识别(人脸ID)或NFC消除截图但需要硬件。QUIC最小化丢包但不解决上下文丢失。
扩展:Redis缓存 idempotency_keys,按 ticket_id 分片。监控:按时间戳的失败扫描指标,折中方案的A/B测试。
— Editorial Team
暂无评论。