216 lines
12 KiB
JSON
216 lines
12 KiB
JSON
{
|
||
"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": []
|
||
}
|
||
]
|
||
} |