国家疫苗预约与接种系统

发布: (2026年2月28日 GMT+8 17:30)
12 分钟阅读
原文: Dev.to

Source: Dev.to

几年前,我参加了一场系统设计面试。面试官给了我以下情景:

设计一个全国疫苗预约系统。
数以百万计的公民需要注册并预约时段。诊所必须给药。政府需要审计日志和防欺诈措施。

我的第一想法很简单:只让人们预约一个时段,检查库存,然后确认。我在白板上画了一个基本流程,感觉还不错。然后面试官开始问更难的问题。

  • “如果两个人同时尝试预订最后一个时段怎么办?”
  • “如果诊所在预约已确认后药剂用完怎么办?”
  • “如果在中途资格检查失败,如何撤销操作?”

我没有好的答案。我只设计了理想路径

那次面试一直萦绕在我脑海。几个月后,在为一个互联网信用购买系统研究库存预留模式时,我意识到同样的思路本可以帮助我应对那次面试。于是我回到这个问题,重新设计了一遍。以下是我的方案。

Source:

我最初的提议(面试时)

User selects clinic → slot → vaccine type → system confirms → appointment created

很快出现的问题

问题描述
竞争条件两个人同时点击“预约”最后一个时段。两者都得到确认 → 有一位公民最终没有座位。
库存不匹配时段已确认,但诊所在预约日之前用完了疫苗剂量。
资格迟到失败系统先确认预约,然后才发现公民不符合年龄/保险要求。时段/剂量已经被分配。
没有回滚如果中途出现错误,无法将时段或剂量释放回资源池。

这些问题与我后来在设计互联网信用购买系统时遇到的相同:当你处理有限资源和大量并发用户时,仅有的“幸福路径”是不够的。

核心洞察

不要在所有都验证之前确认任何事项。
使用多阶段流程:临时保留 → 验证 → 最终确认。如果任何一步失败,回滚。
这正是演唱会票务的运作方式:在您付款时座位被暂时保留;如果您未在规定时间内完成付款,座位将被释放。

Source:

完整的改进设计流程

1. 创建临时预约

  1. 用户选择 诊所、时间段和疫苗类型。
  2. 系统 在 Redis 中创建一个带 TTL 的临时预约(例如 5 分钟)。
  3. 预约状态 = PENDING
  4. 位置容量和疫苗剂量 暂时减少,以便其他用户看到可用量下降。

为什么使用 Redis?

  • 快速、内存级别,开箱即支持 TTL。
  • 关系型数据库也能实现,但需要调度任务来清理过期预约。Redis 会自动过期键。

2. 在 Redis 上处理竞争条件

  • 使用原子 DECR 命令对位置计数器进行递减。
  • 如果计数器降至零,则拒绝后续请求。
  • 为了更安全,可将检查‑并‑递减包装在 Lua 脚本 中,使其成为单个原子操作。

3. 进行资格检查(在占位期间)

检查项目描述
年龄某些疫苗仅限 60 岁以上人群。
保险通过外部 API 验证。
既往病史过敏、已接种剂量等。
地域公民必须属于对应的地区。

如果任意检查未通过

  • 删除 Redis 中的预约。
  • 递增临时位置计数器(释放该位置)。
  • 返回明确的错误信息(例如 “您不符合资格,因为 …”)。

4. 最终确认(所有检查均通过)

  1. 在主数据库中持久递减 位置容量和疫苗库存。
  2. 更新预约状态:PENDING → CONFIRMED
  3. 删除 Redis 中的预约(已不再需要)。
  4. 向公民发送确认信息(短信、邮件、推送)。

这一步是 不可逆点。在此之前的所有操作都可以撤销。

5. 到达诊所

  1. 工作人员扫描公民的二维码(包含预约 ID + 验证哈希)。
  2. 服务器将二维码与预约记录进行核对。
  3. 工作人员记录疫苗批号和接种时间。
  4. 预约状态 → ADMINISTERED
  5. 触发事件用于分析、政府报告和审计日志。

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 – 为分析、报告以及子系统之间的最终一致性发出事件。

要点

  1. 永远不要相信顺畅路径 当处理稀缺资源时。
  2. 原子性的临时占用(Redis DECR/Lua)可防止竞争条件。
  3. 多阶段工作流(PENDING → CONFIRMED → ADMINISTERED)提供明确的回滚点。
  4. 基于 TTL 的预留 会自动清理被放弃的尝试。
  5. 断路器和回退机制 在下游依赖失败时保持系统弹性。

将这些库存预留模式付诸实践后,原本天真的“预订‑确认”设计转变为一个稳健、可投入生产的全国疫苗预约系统。

服务概览

服务责任
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’。但面试官并不是在测试我能否设计一个预订表单,而是在测试我能否考虑出错时会发生什么。”

关键要点

  1. 从失败场景开始,而不是 happy path – 在确定任何设计之前,先问自己“每一步可能会出什么错?”
  2. 临时预订是一种模式,而不是 hack – 无论是演唱会门票、秒杀活动,还是疫苗预约,有限的库存和大量用户都会需要 hold‑then‑confirm 流程。
  3. 明确回滚(rollback)细节 – “我们会处理错误”并不是设计。必须说明当出现错误时,数据、库存以及用户的状态会如何处理。
  4. 为外部服务宕机做好规划 – 保险 API 或通知服务可能会宕机。断路器(circuit breakers)和重试队列(retry queues)不是可选项,而是必需的。
  5. 学习库存预订模式 – 我之前关于设计互联网信用购买系统的文章详细介绍了这些模式,并提供了代码示例。核心思路——先预订、再验证、最后提交——在很多系统中都能看到,只要你去寻找。

征求反馈

感谢阅读。如果您遇到类似的面试题或有改进此设计的想法,欢迎在评论中告诉我。

0 浏览
Back to Blog

相关文章

阅读更多 »

设计 URL Shortener

设计一个 URL 短链接服务是最受欢迎的系统设计面试题之一。它看起来很简单,但它考察你对可扩展性、数据库……的理解。

防线:三系统,而非单一

三个系统,而不是一个。“Rate limiting” 常被用作一个统称,指任何拒绝或放慢请求的行为。实际上,它包含三种不同的机制……

2026年企业 Web 开发终极指南

企业网页开发在过去十年中经历了巨大的演变。随着企业对数字平台的依赖日益增加,创建可扩展的、安全的、以及 h...