国家疫苗预约与接种系统
Source: Dev.to
几年前,我参加了一场系统设计面试。面试官给了我以下情景:
设计一个全国疫苗预约系统。
数以百万计的公民需要注册并预约时段。诊所必须给药。政府需要审计日志和防欺诈措施。
我的第一想法很简单:只让人们预约一个时段,检查库存,然后确认。我在白板上画了一个基本流程,感觉还不错。然后面试官开始问更难的问题。
- “如果两个人同时尝试预订最后一个时段怎么办?”
- “如果诊所在预约已确认后药剂用完怎么办?”
- “如果在中途资格检查失败,如何撤销操作?”
我没有好的答案。我只设计了理想路径。
那次面试一直萦绕在我脑海。几个月后,在为一个互联网信用购买系统研究库存预留模式时,我意识到同样的思路本可以帮助我应对那次面试。于是我回到这个问题,重新设计了一遍。以下是我的方案。
Source: …
我最初的提议(面试时)
User selects clinic → slot → vaccine type → system confirms → appointment created
很快出现的问题
| 问题 | 描述 |
|---|---|
| 竞争条件 | 两个人同时点击“预约”最后一个时段。两者都得到确认 → 有一位公民最终没有座位。 |
| 库存不匹配 | 时段已确认,但诊所在预约日之前用完了疫苗剂量。 |
| 资格迟到失败 | 系统先确认预约,然后才发现公民不符合年龄/保险要求。时段/剂量已经被分配。 |
| 没有回滚 | 如果中途出现错误,无法将时段或剂量释放回资源池。 |
这些问题与我后来在设计互联网信用购买系统时遇到的相同:当你处理有限资源和大量并发用户时,仅有的“幸福路径”是不够的。
核心洞察
不要在所有都验证之前确认任何事项。
使用多阶段流程:临时保留 → 验证 → 最终确认。如果任何一步失败,回滚。
这正是演唱会票务的运作方式:在您付款时座位被暂时保留;如果您未在规定时间内完成付款,座位将被释放。
Source: …
完整的改进设计流程
1. 创建临时预约
- 用户选择 诊所、时间段和疫苗类型。
- 系统 在 Redis 中创建一个带 TTL 的临时预约(例如 5 分钟)。
- 预约状态 =
PENDING。 - 位置容量和疫苗剂量 暂时减少,以便其他用户看到可用量下降。
为什么使用 Redis?
- 快速、内存级别,开箱即支持 TTL。
- 关系型数据库也能实现,但需要调度任务来清理过期预约。Redis 会自动过期键。
2. 在 Redis 上处理竞争条件
- 使用原子
DECR命令对位置计数器进行递减。 - 如果计数器降至零,则拒绝后续请求。
- 为了更安全,可将检查‑并‑递减包装在 Lua 脚本 中,使其成为单个原子操作。
3. 进行资格检查(在占位期间)
| 检查项目 | 描述 |
|---|---|
| 年龄 | 某些疫苗仅限 60 岁以上人群。 |
| 保险 | 通过外部 API 验证。 |
| 既往病史 | 过敏、已接种剂量等。 |
| 地域 | 公民必须属于对应的地区。 |
如果任意检查未通过:
- 删除 Redis 中的预约。
- 递增临时位置计数器(释放该位置)。
- 返回明确的错误信息(例如 “您不符合资格,因为 …”)。
4. 最终确认(所有检查均通过)
- 在主数据库中持久递减 位置容量和疫苗库存。
- 更新预约状态:
PENDING → CONFIRMED。 - 删除 Redis 中的预约(已不再需要)。
- 向公民发送确认信息(短信、邮件、推送)。
这一步是 不可逆点。在此之前的所有操作都可以撤销。
5. 到达诊所
- 工作人员扫描公民的二维码(包含预约 ID + 验证哈希)。
- 服务器将二维码与预约记录进行核对。
- 工作人员记录疫苗批号和接种时间。
- 预约状态 →
ADMINISTERED。 - 触发事件用于分析、政府报告和审计日志。
Source: …
故障处理场景
| 故障 | 处理 |
|---|---|
| 未出现 | 计划任务会扫描已 CONFIRMED 且时间窗口已过的预约。状态 → NO_SHOW;库存被释放回去。 |
| 公民取消 | 通过门户取消后,库存会立即释放。 |
| 诊所取消时段 | 所有受影响的预约会被标记,公民会收到通知,并可优先重新预约。 |
| 外部 API 故障(例如保险) | 使用 circuit‑breaker 模式。连续 N 次失败后,暂时停止调用该 API。预约要么进入重试队列(指数退避),要么临时允许并标记以供人工审查。 |
| Redis 故障 | 回退到 database‑level reservations,并配合清理任务。速度较慢,但仍可完成预约。 |
高层架构
+-------------------+ +-------------------+ +-------------------+
| Frontend | ---> | API Gateway | ---> | Auth Service |
| (Booking portal | | (Auth, rate‑limit | | (Login, ID check)|
| & Clinic dashboard) | , routing) | +-------------------+
+-------------------+ +-------------------+ |
+-------------------+
| Booking Service |
| (Reservation, |
| eligibility, |
| confirmation) |
+-------------------+
|
+-------------------+----------------------+-------------------+
| | | |
+-------------------+ +-------------------+ +-------------------+ +-------------------+
| Redis Cache | | Relational DB | | External APIs | | Messaging / |
| (Temp holds, TTL) | | (Appointments, | | (Insurance, | | Event Bus |
| | | Stock, Logs) | | Medical, etc.) | | (Kafka, SNS…) |
+-------------------+ +-------------------+ +-------------------+ +-------------------+
- Frontend – 为市民提供的 Web/移动门户;为诊所工作人员提供的仪表盘。
- API Gateway – 处理身份验证、全局限流(在大规模预约时至关重要)、以及路由到微服务。
- Auth Service – 国家身份证验证、令牌颁发。
- Booking Service – 核心业务逻辑:临时预约、资格检查、最终确认、取消处理。
- Redis Cache – 基于 TTL 的快速临时占位。
- Relational DB – 持久化存储预约信息、库存水平、审计日志。
- External APIs – 保险验证、病史查询等。
- Messaging / Event Bus – 为分析、报告以及子系统之间的最终一致性发出事件。
要点
- 永远不要相信顺畅路径 当处理稀缺资源时。
- 原子性的临时占用(Redis
DECR/Lua)可防止竞争条件。 - 多阶段工作流(PENDING → CONFIRMED → ADMINISTERED)提供明确的回滚点。
- 基于 TTL 的预留 会自动清理被放弃的尝试。
- 断路器和回退机制 在下游依赖失败时保持系统弹性。
将这些库存预留模式付诸实践后,原本天真的“预订‑确认”设计转变为一个稳健、可投入生产的全国疫苗预约系统。
服务概览
| 服务 | 责任 |
|---|---|
| Patient Service | 医疗记录,疫苗接种历史 |
| Clinic Service | 时段管理,员工排班,容量 |
| Inventory Service | 各诊所疫苗库存,批次追踪 |
| Appointment Service | 管理预约、确认和状态变更 |
| Eligibility Service | 规则引擎 + 外部 API 调用 |
| Notification Service | 短信、电子邮件、推送;如投递失败则重试 |
| Audit Service | 追加式日志记录每一次状态变更(符合政府合规要求) |
数据层
- PostgreSQL – 永久数据存储
- Redis – 临时预约和缓存
异步消息
- Kafka 事件主题:
AppointmentReserved
AppointmentConfirmed
AppointmentAdministered
AppointmentCancelled
这些事件保持服务解耦,并默认使系统可审计。
Source: …
面试的经验教训
“回顾那次面试,我最大的失误并不是技术层面,而是思维方式。我因为觉得完整就直接走了‘happy path’。但面试官并不是在测试我能否设计一个预订表单,而是在测试我能否考虑出错时会发生什么。”
关键要点
- 从失败场景开始,而不是 happy path – 在确定任何设计之前,先问自己“每一步可能会出什么错?”
- 临时预订是一种模式,而不是 hack – 无论是演唱会门票、秒杀活动,还是疫苗预约,有限的库存和大量用户都会需要 hold‑then‑confirm 流程。
- 明确回滚(rollback)细节 – “我们会处理错误”并不是设计。必须说明当出现错误时,数据、库存以及用户的状态会如何处理。
- 为外部服务宕机做好规划 – 保险 API 或通知服务可能会宕机。断路器(circuit breakers)和重试队列(retry queues)不是可选项,而是必需的。
- 学习库存预订模式 – 我之前关于设计互联网信用购买系统的文章详细介绍了这些模式,并提供了代码示例。核心思路——先预订、再验证、最后提交——在很多系统中都能看到,只要你去寻找。
征求反馈
感谢阅读。如果您遇到类似的面试题或有改进此设计的想法,欢迎在评论中告诉我。