题目与范围
假设有 2 万个租户、每小时 100 万次操作,短时突发可达 10 倍,授权决策 p95 低于 100 毫秒。客户先购买额度,再执行操作。每次操作成本可能不同,请求可以并发到达,使用事件至少投递一次。设计必须防止余额变负和重复扣费,并保留审计轨迹。这是购买所得价值的账本,不是限流器,也不是周期订阅计费系统。
面试官考察什么
- 区分不可变额度流水与快速余额读模型。
- 让授权、扣取、释放、退款和发放操作具备幂等性。
- 处理并发请求、延迟事件、重试、过期和对账。
- 说明一致性边界、热点租户、可观测性与恢复方案。
可以先问的澄清问题
确认额度是否过期、失败操作是否先预留再释放、退款由谁审批、成本是否在执行前确定,以及一个租户是否会在多个区域消费。再确认决策延迟要求和是否允许临时超额授权。
30 秒回答
我会使用由发放、预留、扣取、释放、退款和过期组成的追加写入不可变账本。事务内维护余额投影以支持快速读取,用幂等键让每次变更可重复执行。授权时原子检查可用额度并创建带过期时间的预留;成功后扣取,失败后释放;回收器处理遗留预留。通过 outbox、可重放事件和对账任务比较投影与账本,并用可审计的补偿流水修复偏差。
分步深入回答
1. 建模额度状态
按租户或账户保存整数额度单位、账本序号,以及发放、预留、扣取、释放、退款、过期总额的投影。每笔变更记录 operation_id、幂等键、原因、操作者和时间。旧流水不修改,纠正只能追加补偿流水。
2. 在不重复消费的前提下授权
对租户热点键使用同一事务或线性一致的条件更新:只有可用额度不少于本次成本时才创建预留。重复幂等键返回原决定;同一键携带不同参数则拒绝。预留保存过期时间,扣取和释放都要带状态条件,不能同时成功。
3. 连接业务执行与事件
业务请求携带预留标识。确认成功后扣取,失败或取消后释放。使用事件可能重复或延迟,消费者按 event_id 去重,对未知预留执行对账,不能直接减少计数器。outbox 在事务提交后发布账本变更。
4. 扩展、替代方案与恢复
按租户分区,让同一租户路由到固定写入区域,并用准入控制或串行命令流隔离极热点租户。租户写入量中等时,关系数据库事务更简单;极热点租户更需要事件溯源命令流,以顺序和可重放性换取查询复杂度。只有能接受延迟的读取才使用副本,并展示数据时间。清扫器使遗留预留过期,重放账本可重建投影。监控负可用额度、过期后扣取、重放延迟和对账差异。
高质量示例回答
我会先确认过期、退款权限、多区域消费,以及成本是否在执行前已知。不可变的每租户账本是真实来源,余额表只是可重建投影。authorize 在幂等键和过期时间约束下原子预留额度,成功后 capture,失败后 release 或等待过期。重复或延迟事件先去重再对账。严格禁止重复消费时采用租户归属写入区域;若必须多区域独立消费,则分配明确的区域预算并显式对账债务,说明最大临时超额。所有纠正都追加补偿流水。
常见失误
- 只有可变计数器 → 重试和退款无法审计 → 追加带操作标识的不可变流水。
- 请求到达就扣费 → 失败操作也消耗额度 → 先预留,成功后才扣取。
- 把重试当作新消费 → 同一操作被扣两次 → 重用同一幂等键。
- 预留从不回收 → 崩溃工作进程永久锁住额度 → 增加过期时间和条件清扫器。
- 用最终一致的跨区读取 → 两个写入者可能超额消费 → 单写入者或明确区域预算。
- 修改旧行修复历史 → 审计轨迹失去可信度 → 追加补偿流水。
追问与回答
客户端在授权后超时,怎么办?
先查询预留,或使用同一幂等键重试下一状态。只有确认原预留已扣取、释放或过期后,才创建新的授权。
月结后才发现需要退款,怎么办?
追加一笔引用原扣取的退款流水,并记录审批信息。投影增加可用额度,账本保留两笔事件及其顺序。
两个区域能同时消费同一账户吗?
严格防止重复消费时,优先使用单写入者或租户归属区域。若必须本地消费,则分配明确区域预算并对账债务,同时说明临时超额上限。
如何证明余额正确?
定期重放账本,比较推导出的可用额度与投影,并把扣取流水与业务结果比较。每次修复写入幂等补偿流水和审计记录。