Files
examination/topics/architecture/seckill-design/fill_blank.json
T
2026-09-04 12:05:07 +08:00

128 lines
12 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"topic": "seckill-design",
"type": "fill_blank",
"schema_version": "1.0.0",
"generated": "2026-09-04T00:00:00+08:00",
"questions": [
{
"id": "fb-001",
"type": "fill_blank",
"difficulty": 1,
"tags": ["分层削峰", "分层", "削峰", "CDN", "网关", "异步", "消息队列"],
"question": "秒杀系统的核心矛盾是:极小并发窗口 vs 极大瞬时流量。其设计本质是通过____,逐层把海量请求挡在外层,只让极少数\"真买家\"穿透到后端。典型的请求链路为:CDN/Nginx(静态资源) → 网关(限流/限购/风控) → 秒杀服务 → Redis(原子扣库存) → ____ → 数据库(异步落库)。",
"answer": ["分层削峰", "消息队列"],
"answer_rule": "ordered",
"explanation": "秒杀系统面临的核心问题是:几秒内可能涌入几万到几百万 QPS,但真正能卖出的商品只有十几件。因此绝不能把所有流量都打到数据库,而是采用分层削峰策略:每一层都把海量请求挡在外面,只让极少数真买家穿透到后端。在链路上,Redis 负责原子扣库存(保证不超卖),扣减成功后把订单写入消息队列,再由消费者异步批量落库,数据库只承接削峰后极少量的写入,避免了高并发直接压垮 MySQL。第一空应填\"分层削峰\"描述设计本质,第二空按链路顺序应填\"消息队列\"(在 Redis 与数据库之间异步削峰)。",
"source": null,
"related": []
},
{
"id": "fb-002",
"type": "fill_blank",
"difficulty": 2,
"tags": ["限流", "429", "Retry-After"],
"question": "秒杀限流时,被限流 ≠ 售罄 ≠ 故障,应用不同状态码区分\"还有机会\"与\"已经没了\"。当请求被限流(还有机会)时,应返回 HTTP 状态码 ____,并且该响应必须携带 ____ 响应头,让客户端平滑重试,避免重试风暴二次打穿系统;而售罄通常用 404 或业务码表示,服务过载用 503 表示。",
"answer": ["429", "Retry-After"],
"answer_rule": "ordered",
"explanation": "429(Too Many Requests)表示\"请求过多被限流,但还有机会\",与售罄、故障在语义上区分开。429 响应必须携带 Retry-After 响应头(如 Retry-After: 5),告诉客户端几秒后重试,从而把客户端重试的节奏拉到系统可承受的范围,避免所有被挡住的用户同时疯狂重试形成重试风暴,再次打穿系统。Nginx 下用 limit_req_status 429 覆盖默认的 503;前端拿到 429 应展示\"已自动排队\"的友好文案,而不是\"系统繁忙\"(否则会诱导用户手动刷新)。",
"source": null,
"related": []
},
{
"id": "fb-003",
"type": "fill_blank",
"difficulty": 2,
"tags": ["Lua", "Redis原子性", "超卖", "单线程"],
"question": "在秒杀扣库存场景,用两条普通命令(先读库存、再 DECR)在并发下会发生____(超卖/欠卖)问题。解决方法是把\"读库存→判断够不够→扣减\"封装进一个____脚本整体原子执行,因为 Redis 主线程是____(并发/单线程)event loop,执行该脚本期间不会插入任何其他命令,从物理上保证了原子性。",
"answer": ["超卖", "Lua", "单线程"],
"answer_rule": "all",
"explanation": "普通两条命令会超卖:并发下多个请求都先读到\"库存还够\",然后各自 DECR,总数就会超过库存。解法是写 Lua 脚本,把读库存、判断、扣减作为一个整体放进一个脚本(if...return 0...decrby),Redis 主线程是单线程 event loop,一次只执行一个命令,执行 Lua 时主线程被占用、绝不插入其他命令,因此整个脚本是原子的、不可被切分。这也是与 MULTI/EXEC 的本质区别:MULTI/EXEC 只能连续执行命令,无法写 if 判断逻辑,而秒杀的\"够才扣\"必须依赖 Lua 的判断能力。",
"source": null,
"related": []
},
{
"id": "fb-004",
"type": "fill_blank",
"difficulty": 3,
"tags": ["乐观锁", "超卖", "数据库"],
"question": "即便 Redis Lua 已做到原子扣减,数据库层仍需做兜底防超卖。可用乐观锁:在 stock 表增加____字段,执行扣减时同时让它 +1,并通过 WHERE ____来约束,只有当某行的库存>(0) 且版本号匹配时才更新成功;当 RowsAffected()==0 就表示库存不足或版本冲突,应返回售罄。",
"answer": ["version", "stock>0 AND version=?"],
"answer_rule": "ordered",
"explanation": "数据库乐观锁通过版本号字段控制并发写:每次更新都让 version 自增 1,并在 WHERE 里同时校验 stock>0 和 version=?(等于读出来的版本)。这样多个并发事务只有第一个能匹配到当前版本并更新成功,其余因为版本不匹配而被更新 0 行(RowsAffected()==0),从而避免数据层超卖。它兜底了绕过 Redis 或 Redis 异常导致的超卖风险,是防超卖的最后一道防线。",
"source": null,
"related": []
},
{
"id": "fb-005",
"type": "fill_blank",
"difficulty": 3,
"tags": ["限购", "Lua", "Redis原子性", "一人限购"],
"question": "秒杀\"一人限购一件\"时,\"判已购 + 扣库存\"必须放在____(单个方式,如 Lua 脚本/两条独立命令)中一次性完成,否则会出现\"扣了库但没标记\"(可重复买)或\"标了但没扣\"(库存错乱)。若在数据库兜底,可对 ____表建唯一约束 UNIQUE KEY(goods_id, user_id),重复 INSERT 会触发 Duplicate entry,使一人一件在物理上不可能被破坏。",
"answer": ["同一个 Lua 脚本(一次原子操作)", "已购记录"],
"answer_rule": "all",
"explanation": "\"判已购 + 扣库存\"必须放进同一个 Lua 脚本原子完成:脚本先检查已购标记 key(如 seckill:bought:{goodsID}:{userID})是否存在,存在则返回 -1(已购过);否则再判断库存是否够,够则 decrby 扣库存并 set 已购标记、设置过期时间,返回 1。若拆成两条独立命令,并发下会出现\"扣了库存但还没标记\"(可重复购买)或\"标记了但没扣库存\"(库存错乱)。而数据库层建 (goods_id, user_id) 唯一约束则把一人一件落到物理层面,任何 ID 插到已存在的用户商品组合都会因唯一索引冲突失败。",
"source": null,
"related": []
},
{
"id": "fb-006",
"type": "fill_blank",
"difficulty": 4,
"tags": ["hash tag", "Redis Cluster", "跨节点"],
"question": "当 Redis 采用 Cluster 模式时,Lua 脚本同时操作 stock 与 user 两个不同 key 会因____(多个 key 落在不同节点导致)而无法原子执行并报错。解决方法是使用____(如 `{seckill:<goodsID>}:stock` 与 `{seckill:<goodsID>}:user:<userID>`),利用哈希槽规则把多个 key 强制路由到____(不同/同一个)槽位即同一节点上,从而让跨 key 的 Lua 脚本仍能在该节点内原子执行。",
"answer": ["跨节点/跨槽位", "hash tag", "同一个"],
"answer_rule": "ordered",
"explanation": "在 Redis Cluster 中,key 按 hash 结果(如 CRC16 取模)分布到不同节点/槽位,Lua 脚本操作多个 key 时要求这些 key 必须 hash 到同一个槽,否则会报错无法保证原子。hash tag 用花括号 { } 圈住关键部分,只有花括号内的部分参与 hash 计算,例如 {seckill:<goodsID>}:stock 与 {seckill:<goodsID>}:user:<userID> 花括号内容相同,hash 结果相同,就会被强制分配到同一个节点(同一槽位),从而保证同一个 Lua 脚本内对这两个 key 的操作仍然原子。若用单实例 Redis(秒杀常用)则没有这个限制。",
"source": null,
"related": []
},
{
"id": "fb-007",
"type": "fill_blank",
"difficulty": 2,
"tags": ["异步", "消息队列", "削峰", "落库"],
"question": "秒杀扣库存成功后,________(同步/异步)直接写库,而是先写入____(如 MQ),由消费者____(把多条记录批量合在一起)写入数据库(如攒一批后多条 INSERT),以大幅降低数据库交互次数、保护数据库。",
"answer": ["不要", "消息队列", "批量"],
"answer_rule": "ordered",
"explanation": "秒杀扣库存成功只是 Redis 侧扣减完成,此时若每条秒杀成功都同步写一次库,会在瞬间产生大量 DB 写请求,把好不容易靠分层削峰保护下来的 MySQL 再次压垮。正确做法是把扣减成功的订单推进消息队列,由消费者攒批(例如积累 100 条)再一次性批量 INSERT 落库,把海量单条写换成少数批量写,大幅减少 DB 交互次数,实现对数据库的异步削峰保护。",
"source": null,
"related": []
},
{
"id": "fb-008",
"type": "fill_blank",
"difficulty": 1,
"tags": ["Redis", "key设计", "命名规范"],
"question": "Redis 的 key 底层是一个平坦的____(map/dict),没有文件夹或层级,冒号 : 只是开发者命名约定,对 Redis 而言只是字符串。秒杀中\"商品库存\"推荐使用 key 模式____(如 seckill:stock:1001)来表示,\"每用户限购标记\"则用____(如 seckill:bought:1001:9527)这样的 key。",
"answer": ["字典(dict)", "seckill:stock:{商品ID}", "seckill:bought:{商品ID}:{用户ID}"],
"answer_rule": "ordered",
"explanation": "Redis 的 key 实际存储在一个平坦的字典(dict)中,不存在文件系统那样的文件夹或层级结构。冒号 : 只是业界约定的命名分隔符,对 Redis 来说就是普通字符串。规范的 key 命名是\"业务:对象:标识\",例如库存 key 用 seckill:stock:{商品ID}(多用户共享、可减量),而\"一人一件\"的已购标记用 seckill:bought:{商品ID}:{用户ID}(按用户独立标记,避免互相污染)。限流 key 则是 seckill:limit:{用户ID/IP}。",
"source": null,
"related": []
},
{
"id": "fb-009",
"type": "fill_blank",
"difficulty": 3,
"tags": ["限流", "Redis", "INCR", "EXPIRE"],
"question": "用 Redis key 计数实现\"每X秒限N次\"的限流:key = seckill:limit:{userID},用____命令进行原子计数,并在首次访问(count==1)时执行____命令设置过期时间(如 5 秒),当计数超过阈值则返回 HTTP 状态码____(此时还应附带排重响应头如 Retry-After)。",
"answer": ["INCR", "EXPIRE", "429"],
"answer_rule": "ordered",
"explanation": "基于 Redis 的计数限流:用 INCR 对 seckill:limit:{userID} 原子自增,当返回值 count==1 说明是这一轮首次访问,立即给它设置 EXPIRE(比如 5 秒),这样窗口一过 key 自动过期清零,从而做到\"每 N 秒限 M 次\"。当 INCR 后的 count 超过阈值时返回 429(Too Many Requests),表示被限流但还有机会。这个方案简单高效,且 INCR 本身原子,天然适合高并发的频控场景,不需要额外加锁。",
"source": null,
"related": []
},
{
"id": "fb-010",
"type": "fill_blank",
"difficulty": 5,
"tags": ["Redis原子性", "Lua", "单线程", "性能"],
"question": "Redis 的 Lua 脚本虽然能解决原子性,但也必须保持短小、____(不阻塞/支持返回值),不可包含死循环、文件 IO、网络 IO 等阻塞操作,否则会长时间下霸 Redis 的____(主线程被占用)导致其他请求无法服务,最终触发 ____(如超时配置)。",
"answer": ["不阻塞", "单线程", "lua-time-limit"],
"answer_rule": "ordered",
"explanation": "Redis 主线程是单线程 event loop,执行 Lua 脚本时主线程被脚本完全占用,期间不处理其他命令。因此 Lua 脚本必须尽量短小且无阻塞,绝不能有 while 死循环、文件 IO、网络 IO、sleep 等操作,否则会让整个 Redis 卡住、其他请求全部排队甚至超时。Redis 对 Lua 脚本执行有 lua-time-limit 保护(默认几秒,如 5000ms),达到后会尝试向脚本抛出错误并中断,防止恶意脚本拖垮在线服务。",
"source": null,
"related": []
}
]
}