题目重点:从购物车写入一致性切到商品评分事件:设计小时/天/任意窗口平均分与 Top 商品查询,处理小样本偏差、迟到事件、窗口聚合和精确查询成本。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 购物车支持用户查看、加入、变更数量和删除 SKU;评分是独立用户行为事件,不把购物车内某商品数量当评分权重。题目并未给结算/支付要求,结算作为接口边界而非题目主轴。
- 每条评分事件包含 eventId、productId、userId、rating、eventTime;本文训练口径取 1–5 分,窗口 [start,end),均分为窗口内评分总和除评分条数,零条评分商品不参加排名。
- ‘任意给定时间’解释为查询任意起止时间段,而不是某个绝对时刻;若面试官指截至指定时刻的历史快照,需要改 API 契约。
- 示例规模 100 万商品、每秒 1 万评分写入,仅用于容量推演,不代表 Uber 实际流量;Top 查询可先给精确但较慢的基线,再按 SLA 优化。
解题路线
- 购物车 API 以 (userId,sku) 做唯一键,变更数量带版本号或条件写入防并发覆盖;读取可以合并商品价格/库存展示,但下单时必须重新校验。评分存储与购物车状态分离,保证频繁评分不会争抢购物车写锁。
- 评分写入先验证 ID 与分值,把 eventId 作为去重键落持久日志或数据库,再异步更新各商品时间桶的 sum/count;同一 eventId 重放不能让聚合翻倍。修改/撤销评分若要支持,应写带版本的负向修正事件,但需先问清业务规则。
- 单商品任意时间窗的精确均分可对该商品按 eventTime 排序的评分索引做范围扫描,聚合 sum/count;小时/天热点查询可用分钟或小时桶预聚合,窗口边界读原始事件补齐。不同分辨率的桶要按窗口精度和保留成本取舍。
- Top 商品不是先算一个商品就结束:精确基线要在窗口内有评分的全部商品上得到 sum/count,再按 (平均分, 样本数, productId) 排序;平均值比较可用交叉乘法避免浮点并列误差。若候选集过大,可维护固定窗口的增量排行榜,任意历史窗口仍需走离线索引/较慢查询或提供明确近似 SLA。
- 评分刚写入到查询可见有传播延迟,API 返回 dataAsOf/watermark;迟到事件回填所属历史桶并失效相关榜单缓存。监控重复率、迟到率、聚合延迟和原始事件对账差异。
手动走一遍
在最近 1 小时,商品 A 两条评分 5 与 1,平均 3;商品 B 一条评分 5,平均 5。若原题按纯均分,B 胜出;但产品可能要求最少 10 条评分或贝叶斯平滑,必须把这当成另行确认的排名政策,不能悄悄改题。新评分 eventId 重试两次后 sum/count 只能增加一次。
规模与取舍
窗口内评分事件数 E、出现过的商品数 P:精确朴素查询 O(E+P log P) 时间、O(P) 聚合空间。单商品范围索引查询 O(log R+r),R 为该商品历史评分数、r 为命中数;时间桶按商品×活跃桶数占空间,固定窗口增量榜单可把常见查询降到索引查找加输出 Top K 的成本,但任意窗口不能由单个预计算榜单直接回答。
易错点与追问
容易失分的边界
- 把总评分数作为平均分,忘记 sum/count 或窗口内零条评分的定义。
- 全量扫描所有商品却宣称任意历史窗口 Top 可毫秒返回,未给分区、索引或近似策略。
- 评分重试使聚合重复,或迟到评分只加当前桶而丢失原事件时间语义。
- 在商品详情展示旧价格后直接按旧价格结算;购物车与订单库存/价格校验边界应分清。
继续深挖
- 若要求一个用户对一个商品始终只有一条有效评分,如何用 (userId,productId) 版本化状态及撤销事件更新历史桶?
- 如果任意窗口 Top 也要低延迟,允许多大近似误差、最小样本数及时间精度?可考虑分层桶和候选重排。
- 跨时区的‘最近一天’是滚动 24 小时还是当地自然日?夏令时对事件分桶有哪些影响?
