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

216 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": "single_choice",
"schema_version": "1.0.0",
"generated": "2026-09-04T00:00:00+08:00",
"questions": [
{
"id": "sc-001",
"type": "single_choice",
"difficulty": 2,
"tags": [
"削峰",
"分层",
"CDN",
"消息队列"
],
"question": "秒杀场景的特点是:极大瞬时流量与极小售卖数量并存(几秒内几十万请求,但能卖出的只有十几件)。为保护后端数据库,系统设计应采用的核心思想是?",
"options": {
"A": "把所有请求无差别直接打到数据库,依靠 MySQL 性能硬扛",
"B": "分层削峰,逐层把海量请求挡在外层,只让极少数真买家穿透到后端",
"C": "把秒杀商品全部预加载进内存,让每个请求都同步写库",
"D": "关闭静态资源缓存,保证所有流量都触发后端实时校验"
},
"answer": "B",
"explanation": "秒杀的核心矛盾是极小窗口 vs 极大瞬时流量。正确做法是分层削峰:链路通常为 CDN/Nginx(静态资源) → 网关(限流/限购/风控) → 秒杀服务 → Redis(原子扣库存) → 消息队列 → 数据库(异步落库)。这样数据库只接待削峰后的极小写入,绝不让高并发直接压 MySQL。A 把高档并发直接打向数据库是错误的;C 所有请求都同步写库同样压垮数据库;D 关闭缓存反而放大流量。",
"source": null,
"related": []
},
{
"id": "sc-002",
"type": "single_choice",
"difficulty": 2,
"tags": [
"限流",
"429"
],
"question": "HTTP 状态码 429 以及它必须携带的响应头,正确含义是什么?",
"options": {
"A": "429 表示库存已售罄,携带 Retry-After 头告知客户商品下架时间",
"B": "429 表示服务过载故障,让客户端随机重试",
"C": "429 表示被限流(还有机会),必须携带 Retry-After 头让客户端平滑重试",
"D": "429 表示请求参数非法,应携带 X-RateLimit-Error 描述错误原因"
},
"answer": "C",
"explanation": "429 Too Many Requests 语义是被限流而非售罄或故障,表示暂时还有机会,稍后重试可能成功。被限流、售罄、故障应用不同状态码区分:429=被限流,404或业务码=售罄,503=服务过载。429 响应必须带 Retry-After 头(如 Retry-After: 5)让客户端在指定时间后平滑重试,从而避免大量客户端同时重放造成重试风暴二次打穿系统。前端拿到 429 应展示'已自动排队'文案而非'系统繁忙'。",
"source": null,
"related": []
},
{
"id": "sc-003",
"type": "single_choice",
"difficulty": 3,
"tags": [
"超卖",
"Redis原子性"
],
"question": "用纯 Redis 两条普通命令(先 GET 读库存再 DECR 扣减)实现扣库存,高并发下为何会超卖?",
"options": {
"A": "因为 Redis 默认关闭持久化,重启后库存丢失导致超卖",
"B": "因为并发多个请求都先读到'还有库存',然后各自 DECR,总扣减量超出库存",
"C": "因为 DECR 返回负数时会致 Redis 卡死,引发雪崩",
"D": "因为 GET 与 DECR 之间网络延迟过大导致事务或回漏"
},
"answer": "B",
"explanation": "两条普通命令不是原子的:并发请求都先执行 GET 读到库存仍有剩余,随后各自执行 DECR,总卖出数量超过库存上限造成超卖。正确做法是把'读库存→判断够→减库存'封装进 Lua 脚本由 Redis 单线程整体不可切分执行。A 是持久化问题与超卖无关;C 中 DECR 返回负数不会导致 Redis 崩溃;D 表示事务,错误在于本来就无原子性保障。",
"source": null,
"related": []
},
{
"id": "sc-004",
"type": "single_choice",
"difficulty": 4,
"tags": [
"Redis原子性",
"Lua",
"乐观锁"
],
"question": "为什么秒杀中'读→判→扣'必须用 Lua 而不能用 MULTI/EXEC 事务?",
"options": {
"A": "因为 MULTI/EXEC 不支持管道复用,网络往返比 Lua 慢很多",
"B": "因为 MULTI/EXEC 只是把命令连续打包执行,无法在中间写 if 判断逻辑,而 Lua 可以写条件",
"C": "因为 MULTI/EXEC 不是原子的,Redis 会穿插其他请求打断它",
"D": "因为 MULTI/EXEC 执行期间会阻塞主线程导致超时,而 Lua 严格不阻塞"
},
"answer": "B",
"explanation": "MULTI/EXEC 只是把一组命令连续打包执行(要么全成要么全不),但无法插入判断逻辑。秒杀'库存够才扣'这种带条件分支必须用 Lua:Lua 在 Redis 单线程中作为一个整体执行,期间主线程不接管其他命令,因此既原子又带 if 判断。A 网络往返不是关键原因;C 错误,MULTI/EXEC 本身也是原子的连续执行;D 不实,两者都会占用主线程。",
"source": null,
"related": []
},
{
"id": "sc-005",
"type": "single_choice",
"difficulty": 3,
"tags": [
"乐观锁",
"超卖"
],
"question": "在 Reddis 之后,数据库层仍推荐用乐观锁做超卖兜底。下面哪条 SQL 写法符合乐观锁思路?",
"options": {
"A": "INSERT INTO stock(goods_id, stock) VALUES(?, ?) 用先查再插入",
"B": "UPDATE stock SET stock=stock-1 WHERE goods_id=? 不加任何条件",
"C": "UPDATE stock SET stock=stock-1, version=version+1 WHERE goods_id=? AND stock>0 AND version=?,RowsAffected()==0 则视为售罄",
"D": "SELECT stock ...; 应用层判断后直接 UPDATE,不依赖版本号"
},
"answer": "C",
"explanation": "乐观锁通过 WHERE 中的 version 和 stock 校验作版本与库存的双重条件:UPDATE stock SET stock=stock-1, version=version+1 WHERE goods_id=? AND stock>0 AND version=?; 命中影响行数 RowsAffected()==0 表示库存不足或版本冲突,返回售罄。A 无明显扣减;B 无任何条件,并发会互相覆盖致超卖;D 是典型的读后改,无法防止并发改写导致超卖。",
"source": null,
"related": []
},
{
"id": "sc-006",
"type": "single_choice",
"difficulty": 4,
"tags": [
"限购",
"Redis原子性",
"超卖",
"Lua"
],
"question": "一人限购一件中,为什么必须把'判已购 + 扣库存'放在同一个 Lua 脚本一次性完成?",
"options": {
"A": "为了缩减脚本字节数,降低网络下载开销",
"B": "为了避开事故,拆成两条独立命令可增加审计日志",
"C": "否则会出现'扣了库却没标记巳购'(可重复买)或'标了已购却没扣库存'(库存错乱)的不一致",
"D": "为了绕过数据库 UNIQUE 约束避免插入冲突"
},
"answer": "C",
"explanation": "若'判已购'与'扣库存'由两独立命令执行,高并发会交叉:先扣库存但未标记已购,其他请求再次扣减实现重复购买;或先标已购但没扣库存,库存错乱。合并进一个 Lua 整体执行,Redis 单线程不插入其他命令,两者严格一致。A/B 与正确性无关;D 反向——数据库用 UNIQUE KEY (goods_id, user_id) 做兜底。",
"source": null,
"related": []
},
{
"id": "sc-007",
"type": "single_choice",
"difficulty": 3,
"tags": [
"异步",
"消息队列",
"削峰"
],
"question": "秒杀扣库存成功后,为什么推荐进消息队列由消费者批量异步写库,而不是直接同步写库?",
"options": {
"A": "异步可把海量请求阻塞在缓存层,让缓存读取更快",
"B": "直接读 MQ 消息即可,数据库可以彻底移除",
"C": "把数据库即时压力转化为可队列的可控吞吐,消费者攒批批量 INSERT,降低 DB 交互次数",
"D": "同步写库会死锁,异步写库则数据库完全无锁"
},
"answer": "C",
"explanation": "库只负责接收削峰后的极小写入。扣库存成功后入消息队列,消费者把消息攒成一泡池(如 100 条)再批量 INSERT,大幅减少与数据库的交互次数,把瞬间峰值压力转化为可控吞吐,保护数据库不被高并发打穿。A 与 B 概念错误,数据库仍是最终落账的权威存;D 过于绝对,异步不减锁开销。",
"source": null,
"related": []
},
{
"id": "sc-008",
"type": "single_choice",
"difficulty": 4,
"tags": [
"hash tag",
"Redis原子性",
"Lua"
],
"question": "Redis Cluster 中一个 Lua 脚本作多个 key 时可能因跨槽位报错,常用的解法是?",
"options": {
"A": "给脚本手动加 LOCK key 做全局锁",
"B": "把脚本拆成多条分别提交分步执行",
"C": "使用 hash tag,使用{}包裹相同内容使 key 落到同一槽位",
"D": "把所有 key 随机平均分到不同槽位以分散负载"
},
"answer": "C",
"explanation": "Redis Cluster 中 key 通过 CRC16 计算槽位,不同 key 常落不同节点,跨节点多 key 的 Lua 无法原子执行报错。解法 hash tag:大括号 `{...}` 内的内容参与哈希计算,因此 `{seckill:1001}:stock` 与 `{seckill:1001}:user:01` 哈希一致,定位同一槽位节点,可原子操作。A 不通用;B 拆开生丢原子;D '分散'正与需求相反。单实例 Redis(秒杀常用)无此限制。",
"source": null,
"related": []
},
{
"id": "sc-009",
"type": "single_choice",
"difficulty": 2,
"tags": [
"Redis Key设计",
"分层"
],
"question": "关于 Redis key 的分层/冒号设计,下列说法正确的是?",
"options": {
"A": "Redis 的 key 有真正文件夹层级,冒号会被解析成目录,依赖它做权限",
"B": "Redis 的 key 底层是平坦字典,没有文件夹;冒号只是开发者命名约定,对 Redis 只是普通字符串如 seckill:stock:1001",
"C": "冒号 key 会被自动渲染成 bucket,导致内存膨胀,应全部改为数字命名",
"D": "Redis 的 key 必须使用多个节点分片,映射到对应磁盘桶"
},
"answer": "B",
"explanation": "Redis 的 key 是全局扁平字典,无层级结构。`seckill:stock:1001` 中的冒号是开发者用于可读性和业务分层的命名约定,对 Redis 而言只是一个普通字符串。规范一般是'业务:对象:标识'。A 将其当作真实目录、C/D 引入不存在的层级/桶概念,都是对 Redis 存储模型的误解。",
"source": null,
"related": []
},
{
"id": "sc-010",
"type": "single_choice",
"difficulty": 4,
"tags": [
"单线程",
"限流",
"Redis原子性"
],
"question": "使用 INCR + EXPIRE 实现每 5 秒限一次(如 seckill:limit:userID),下述逻辑写法正确的是?",
"options": {
"A": "必须先 EXPIRE 再 INCR,否则 key 永不过期累计失效",
"B": "count := INCR(key);若 count==1 单独 EXPIRE(key, 5s);返回 count>1 即 429",
"C": "用 SET key value EX 5 后再 GETSET 判断,与 INCR 效果相同,无需区分首次",
"D": "使用 INCRNX 命令并无需判断是否第一次"
},
"answer": "B",
"explanation": "经典写法:先 INCR 拿到当前窗口内访问次数,若返回 1 说明该 key 是首次创建,此时为其设置 EXPIRE 5s;窗口结束 key 过期,新窗口重新计。count>1 表示该窗口内频率超限返回 429。A 顺序错误,EXPIRE 应在 INCR 首次返回后;C 用 SET+GETSET 无法自然实现滑动窗口计数;D INCRNX 并不存在这种方式来进行窗口限流(窗口内需多次递增)。",
"source": null,
"related": []
}
]
}