代表性面试主题

系统设计面试:为 SaaS 设计预付使用额度账本

系统设计困难
Offer.cc 编辑团队发布 更新

题干

一个数据增强 SaaS 销售预付额度。请设计服务来发放额度、授权不同成本的操作、处理重试与退款,并在至少一次投递的使用事件下保持可审计。

题目与范围

假设有 2 万个租户、每小时 100 万次操作,短时突发可达 10 倍,授权决策 p95 低于 100 毫秒。客户先购买额度,再执行操作。每次操作成本可能不同,请求可以并发到达,使用事件至少投递一次。设计必须防止余额变负和重复扣费,并保留审计轨迹。这是购买所得价值的账本,不是限流器,也不是周期订阅计费系统。

面试官考察什么

  • 区分不可变额度流水与快速余额读模型。
  • 让授权、扣取、释放、退款和发放操作具备幂等性。
  • 处理并发请求、延迟事件、重试、过期和对账。
  • 说明一致性边界、热点租户、可观测性与恢复方案。

可以先问的澄清问题

确认额度是否过期、失败操作是否先预留再释放、退款由谁审批、成本是否在执行前确定,以及一个租户是否会在多个区域消费。再确认决策延迟要求和是否允许临时超额授权。

30 秒回答

我会使用由发放、预留、扣取、释放、退款和过期组成的追加写入不可变账本。事务内维护余额投影以支持快速读取,用幂等键让每次变更可重复执行。授权时原子检查可用额度并创建带过期时间的预留;成功后扣取,失败后释放;回收器处理遗留预留。通过 outbox、可重放事件和对账任务比较投影与账本,并用可审计的补偿流水修复偏差。

分步深入回答

1. 建模额度状态

按租户或账户保存整数额度单位、账本序号,以及发放、预留、扣取、释放、退款、过期总额的投影。每笔变更记录 operation_id、幂等键、原因、操作者和时间。旧流水不修改,纠正只能追加补偿流水。

2. 在不重复消费的前提下授权

对租户热点键使用同一事务或线性一致的条件更新:只有可用额度不少于本次成本时才创建预留。重复幂等键返回原决定;同一键携带不同参数则拒绝。预留保存过期时间,扣取和释放都要带状态条件,不能同时成功。

3. 连接业务执行与事件

业务请求携带预留标识。确认成功后扣取,失败或取消后释放。使用事件可能重复或延迟,消费者按 event_id 去重,对未知预留执行对账,不能直接减少计数器。outbox 在事务提交后发布账本变更。

4. 扩展、替代方案与恢复

按租户分区,让同一租户路由到固定写入区域,并用准入控制或串行命令流隔离极热点租户。租户写入量中等时,关系数据库事务更简单;极热点租户更需要事件溯源命令流,以顺序和可重放性换取查询复杂度。只有能接受延迟的读取才使用副本,并展示数据时间。清扫器使遗留预留过期,重放账本可重建投影。监控负可用额度、过期后扣取、重放延迟和对账差异。

高质量示例回答

我会先确认过期、退款权限、多区域消费,以及成本是否在执行前已知。不可变的每租户账本是真实来源,余额表只是可重建投影。authorize 在幂等键和过期时间约束下原子预留额度,成功后 capture,失败后 release 或等待过期。重复或延迟事件先去重再对账。严格禁止重复消费时采用租户归属写入区域;若必须多区域独立消费,则分配明确的区域预算并显式对账债务,说明最大临时超额。所有纠正都追加补偿流水。

常见失误

  • 只有可变计数器 → 重试和退款无法审计 → 追加带操作标识的不可变流水。
  • 请求到达就扣费 → 失败操作也消耗额度 → 先预留,成功后才扣取。
  • 把重试当作新消费 → 同一操作被扣两次 → 重用同一幂等键。
  • 预留从不回收 → 崩溃工作进程永久锁住额度 → 增加过期时间和条件清扫器。
  • 用最终一致的跨区读取 → 两个写入者可能超额消费 → 单写入者或明确区域预算。
  • 修改旧行修复历史 → 审计轨迹失去可信度 → 追加补偿流水。

追问与回答

客户端在授权后超时,怎么办?

先查询预留,或使用同一幂等键重试下一状态。只有确认原预留已扣取、释放或过期后,才创建新的授权。

月结后才发现需要退款,怎么办?

追加一笔引用原扣取的退款流水,并记录审批信息。投影增加可用额度,账本保留两笔事件及其顺序。

两个区域能同时消费同一账户吗?

严格防止重复消费时,优先使用单写入者或租户归属区域。若必须本地消费,则分配明确区域预算并对账债务,同时说明临时超额上限。

如何证明余额正确?

定期重放账本,比较推导出的可用额度与投影,并把扣取流水与业务结果比较。每次修复写入幂等补偿流水和审计记录。

公开来源

同类题目

相关面试工具

用 Solve 整理系统设计回答

从澄清需求开始,展开规模、架构、组件选择和取舍。

查看工具