277 lines
23 KiB
JSON
277 lines
23 KiB
JSON
|
|
{
|
|||
|
|
"topic": "seckill-design",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"schema_version": "1.0.0",
|
|||
|
|
"generated": "2026-09-04T12:00:24+08:00",
|
|||
|
|
"questions": [
|
|||
|
|
{
|
|||
|
|
"id": "sa-001",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 2,
|
|||
|
|
"tags": [
|
|||
|
|
"分层",
|
|||
|
|
"削峰",
|
|||
|
|
"CDN",
|
|||
|
|
"Gateway",
|
|||
|
|
"消息队列",
|
|||
|
|
"异步落库"
|
|||
|
|
],
|
|||
|
|
"question": "请画出并说明高并发秒杀系统的分层削峰架构(从请求入口到数据库),并解释为什么数据库层只接待极少量写入。",
|
|||
|
|
"answer": "分层削峰架构由外到内依次是:\nCDN/Nginx(静态资源、简单防护)→ 网关(限流/限购/风控)→ 秒杀服务 → Redis(Lua 原子扣库存)→ 消息队列 → 数据库(异步落库)。\n\n各层职责:\n1. CDN/Nginx 层:承载静态页面、商品图、按钮状态等,命中缓存即可返回,挡掉海量重复请求。\n2. 网关层:基于用户/IP 做限流、令牌桶、风控、黑名单,把极大部分非法与重复流量拦截在系统入口。\n3. 秒杀服务:处理业务规则与前置校验。\n4. Redis 层:用 Lua 脚本原子地“读库存→判断够不够→扣减→标记已购”,是真正的防超卖防线。\n5. 消息队列层:秒杀成功后不直接写库,把成交事件发到 MQ 异步削峰。\n6. 数据库层:消费者批量异步写入,数据库只接待削峰后极少量的真实成交写入。\n\n这样设计的原因:秒杀窗口极小(几秒到几十秒)、瞬时 QPS 极高(数万到百万),但最终能成交的只有十几件。若把海量请求直接打到数据库,MySQL 必然被打穿。通过分层削峰,让海量请求在外层就被拦截丢弃,数据库只处理极少数“真买家”的写入,从而保护后端。",
|
|||
|
|
"keywords": [
|
|||
|
|
"分层",
|
|||
|
|
"削峰",
|
|||
|
|
"CDN/Nginx",
|
|||
|
|
"网关限流风控",
|
|||
|
|
"Redis原子扣库存",
|
|||
|
|
"消息队列",
|
|||
|
|
"异步落库",
|
|||
|
|
"数据库保护"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 4 分。①画出/说明完整链路并按正确顺序(CDN→网关→服务→Redis→MQ→DB)得 1 分;②指出每层主要职责(尤其 Redis 原子扣减、MQ 异步削峰)得 1 分;③说明数据库采用异步落库避免直接压库得 1 分;④点出“极小成交量 vs 极大瞬时流量”矛盾与逐层削峰思路得 1 分。",
|
|||
|
|
"explanation": "本题考查对秒杀整体架构的把控。秒杀系统设计核心就是分层削峰。回答须体现从外到内的每一层职责,并强调最终只有极少量数据写入数据库、MySQL 不直接承受高并发。缺任一层或说不出各层职责都会扣分。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-002",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 3,
|
|||
|
|
"tags": [
|
|||
|
|
"429",
|
|||
|
|
"售罄",
|
|||
|
|
"Retry-After",
|
|||
|
|
"限流",
|
|||
|
|
"重试风暴",
|
|||
|
|
"状态码"
|
|||
|
|
],
|
|||
|
|
"question": "秒杀系统中被限流与售罄为什么要用不同状态码区分?分别用哪个状态码,以及 429 响应必须携带 Retry-After 头的原因。",
|
|||
|
|
"answer": "被限流与售罄代表完全不同的业务状态:“被限流”表示系统还有机会,稍后重试可能成功;而“售罄”意味着库存已经没了,无论怎么重试都不会有货。若混用,客户端或用户会误判——把“还有机会”当成“没货”而放弃,或把“没货”当成“被限流”反复请求加重后端负担。\n\n具体约定:\n- 429 Too Many Requests=被限流(还有成交机会);\n- 404 或自定义业务码=售罄(已无库存);\n- 503=服务过载/后端故障。\n\n429 必须带 Retry-After 的原因:\n1. Retry-After 告诉客户端“多久之后再来”,让客户端平滑、有节奏地重试,而不是立刻再来。\n2. 若没有 Retry-After,客户端会立刻并发重试,形成重试风暴(把请求量放大数倍到数十倍),把已经过载的服务二次打穿。\n3. 前端拿到 429 时展示“已自动排队”等友好文案,避免诱导用户手动刷新(手动刷新同样放大风暴)。\n\n示例:响应头 Retry-After: 5,意为 5 秒后重试;可配合 X-RateLimit-Remaining、X-RateLimit-Reset 提供剩余额度信息。",
|
|||
|
|
"keywords": [
|
|||
|
|
"429",
|
|||
|
|
"Retry-After",
|
|||
|
|
"限流",
|
|||
|
|
"售罄",
|
|||
|
|
"重试风暴",
|
|||
|
|
"503"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 5 分。①说明被限流与售罄是不同业务状态(有机会/没机会)得 1 分;②答对 429=限流、售罄用 404/业务码、503=过载得 2 分;③答对 Retry-After 让客户端平滑重试、避免重试风暴得 2 分。",
|
|||
|
|
"explanation": "本题考查状态码语义与重试风暴防护。重点:429 与售罄的区分是用户体验与后端稳定性双重需要;Retry-After 是防止重试风暴的关键手段,属于秒杀设计常考点。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-003",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 4,
|
|||
|
|
"tags": [
|
|||
|
|
"Redis原子性",
|
|||
|
|
"Lua",
|
|||
|
|
"超卖",
|
|||
|
|
"单线程",
|
|||
|
|
"事件循环"
|
|||
|
|
],
|
|||
|
|
"question": "为什么把“读库存→判断够不够→扣减”封装成 Lua 脚本执行就能保证不会超卖?请从 Redis 单线程模型的角度解释其原子性原理。",
|
|||
|
|
"answer": "保证不超卖的本质是让“判断+扣减”这个不可分割的临界区变成原子操作,避免并发下的竞态条件。\n\n原子性原理:\n1. 若把“先 GET 读库存→再 DECR 扣减”分开执行,不同并发请求会先各自 GET 到“还有库存”,然后各自 DECR,累加卖出就会超过库存——这就是超卖。\n2. Redis 服务器主线程是一个单线程的 event loop,同一时刻只执行一个命令/一个脚本,普通命令之间不并发。\n3. Lua 脚本被 Redis 当作一个整体执行:脚本执行期间,Redis 主线程被该脚本独占,不会在处理过程中插入任何其他命令。\n4. 因此把“读库存→判断 stock 是否足够→DECRBY 扣减”写进同一个 Lua 脚本,脚本要么整体执行、要么不执行,中间不会被切分,天然原子。\n5. 脚本执行期间即便并发请求到达,也只能在队列中等待该脚本跑完,从根本上杜绝了“都读到有货、然后都扣”造成的超卖。\n\n补充:为保证 Redis 主线程不被打挂,Lua 脚本必须短小、无阻塞(不写死循环、不做文件/网络 IO),否则会阻塞单线程并触发 lua-time-limit(默认 5000ms)。",
|
|||
|
|
"keywords": [
|
|||
|
|
"Lua",
|
|||
|
|
"原子性",
|
|||
|
|
"单线程",
|
|||
|
|
"event loop",
|
|||
|
|
"超卖",
|
|||
|
|
"临界区",
|
|||
|
|
"DECRBY"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 5 分。①指出“先读后扣”分开执行会因并发竞态而超卖得 1 分;②说出 Redis 底层是单线程 event loop 得 1 分;③解释 Lua 独占主线程期间不插入其他命令、整体原子得 2 分;④点明并发请求排队等脚本执行、杜绝重复扣减得 1 分。",
|
|||
|
|
"explanation": "本题考查 Lua 原子性这一核心考点。关键在于“判断+扣减”写进单个脚本后在单线程 Redis 里整体执行、不可被切分,这与“先读后扣拆分两次命令”的竞态形成对比。答题时强调单线程事件循环与命令不插入。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-004",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 3,
|
|||
|
|
"tags": [
|
|||
|
|
"Lua",
|
|||
|
|
"MULTI/EXEC",
|
|||
|
|
"事务",
|
|||
|
|
"原子性",
|
|||
|
|
"if判断"
|
|||
|
|
],
|
|||
|
|
"question": "为什么秒杀“库存够才扣”的场景必须用 Lua 脚本而非 Redis 的 MULTI/EXEC?请对比两者差异并说明理由。",
|
|||
|
|
"answer": "1. Redis 的 MULTI/EXEC 事务是把一批命令“从头到尾连续执行”,保证这批命令之间不被其他命令插入;但它不能在命令之间写“基于值的判断逻辑”(没有 if 分支/条件选择),也做不到根据判断结果中途放弃某一整批操作。\n2. 秒杀要求“有库存才扣”,需要先读库存、判断数值是否大于等于购买数量,满足才 DECR、不满足返回售罄——这是一个带 if 分支的逻辑。\n3. MULTI/EXEC 的局限:不支持条件判断,一旦把 DECR 放进事务就会无条件执行(即使库存已为 0 也会照扣,导致超卖);而 Lua 是完整脚本语言,可在脚本内写 if 分支,在条件不满足时直接 return 0(不执行扣减)。\n4. 结论:要实现“库存够才扣”这种“带判断的原子操作”,必须用 Lua 脚本;MULTI/EXEC 只能做“无条件的批量原子执行”,无法满足秒杀防超卖需求。\n\n补充:Lua 的 if 判断配合 Redis 单线程原子执行,比“乐观锁配合多次重试”更简单高效,也更常用。",
|
|||
|
|
"keywords": [
|
|||
|
|
"Lua",
|
|||
|
|
"MULTI/EXEC",
|
|||
|
|
"if条件",
|
|||
|
|
"原子性",
|
|||
|
|
"防超卖",
|
|||
|
|
"事务"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 5 分。①正确概括 MULTI/EXEC 为“连续执行、不支持中间条件判断”得 2 分;②说明 Lua 可在脚本内写 if 分支完成“够才扣”得 2 分;③结论“此题需用 Lua 而非 MULTI/EXEC”得 1 分。",
|
|||
|
|
"explanation": "本题考察 Redis 两种原子手段的适用差异。重点:二者都原子,但 MULTI/EXEC 无 if 分支、无法依据读到的值做选择;秒杀“够才扣”需要条件分支故必须 Lua。答出“都是原子、差异在能否条件判断”易得高分。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-005",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 4,
|
|||
|
|
"tags": [
|
|||
|
|
"乐观锁",
|
|||
|
|
"唯一约束",
|
|||
|
|
"兜底",
|
|||
|
|
"防超卖",
|
|||
|
|
"限购"
|
|||
|
|
],
|
|||
|
|
"question": "Redis Lua 原子扣减之外,为什么还要在数据库层用乐观锁 + 唯一约束做双保险兜底?请分别说明乐观锁和唯一约束各自守护什么。",
|
|||
|
|
"answer": "兜底的总体原因:\n1. Redis 是秒杀的主要防线,但并非绝对可靠:若 Redis 异常、被绕过(例如业务直接调用 DB 接口),或日后流量路径改变直接打到数据库,单靠 Redis 无法全局兜住。因此在数据库层再加一层防线。\n\n2. 乐观锁守护“总库存不超卖”:\n UPDATE stock SET stock=stock-1, version=version+1 WHERE goods_id=? AND stock>0 AND version=?;\n - WHERE 中限定“库存仍 >0 且 version=当前值”。\n - 其中“版本号 + 库存上限判断”共同保证只有符合条件的 update 生效。\n - 若 RowsAffected()==0,说明库存不足或版本号已变化(被别的事务先改过),直接返回售罄,不回滚,不夸售。\n\n3. 唯一约束守护“一人一件限购”:\n 建数据库唯一约束 UNIQUE KEY(goods_id, user_id),当同一用户对同一商品再次插入下单记录时发生冲突(Duplicate entry),数据库在该物理层面拒绝重复,从而锁死一人只能谋购一次。\n\n4. 两者“防线不同”构成双保险:乐观锁防“总量超卖”,唯一约束防“单人多买”,与 Redis 叠加形成多层防线,把超卖与重复购买都做了底层兜底。",
|
|||
|
|
"keywords": [
|
|||
|
|
"乐观锁",
|
|||
|
|
"version",
|
|||
|
|
"唯一约束",
|
|||
|
|
"Duplicate entry",
|
|||
|
|
"兜底",
|
|||
|
|
"双保险",
|
|||
|
|
"防超卖"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 6 分。①说明单靠 Redis 不保险、需要 DB 兜底得 1 分;②说出乐观锁利用 WHERE stock>0 AND version=? 判断、RowsAffected()==0 则返回售罄得 2 分;③说出唯一约束 (goods_id,user_id) 挡重复插入、物理上拒绝一人多买得 2 分;④用一句话总结“乐观锁防超卖,唯一约束防限量购”得 1 分。",
|
|||
|
|
"explanation": "本题既考察“Redis 之外还要 DB 兜底”的动机,也考察两道防线各自的差异。乐观锁(version 比较 + stock>0 判断 + RowsAffected)守超卖,唯一约束守一人一件。写出 SQL 主张变更尤其得分高。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-006",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 5,
|
|||
|
|
"tags": [
|
|||
|
|
"同一Lua",
|
|||
|
|
"扣库存",
|
|||
|
|
"限购",
|
|||
|
|
"原子性",
|
|||
|
|
"防双写"
|
|||
|
|
],
|
|||
|
|
"question": "秒杀中为什么“扣减库存”和“标记一人已购”必须放在同一个 Lua 脚本内一次性完成?如果分开实现会出什么问题?返回约定 -1/0/1 各代表什么?",
|
|||
|
|
"answer": "必须放在同一个 Lua 的原因:\n“扣库存”和“标记已购”本质上是同一笔交易的两个状态变更,需要原子、连贯地发生,中途不被打断。\n\n如果分开两次执行(不同时刻发出命令)会出两类问题:\n1. “扣了库但没标记已购”:库存被扣掉,但用户没有被正确记录,用户重试时仍可能再次买入,导致一人多件、账实不一致;\n2. “标记了但没扣库”:用户被标记已购,但库存没有减,库存虚高,更坏的是会把真实成交数据搞乱。\n\n这两个状态变更必须“要么同时成功、要么同时不做”,写进同一个 Lua 脚本、由单线程一次性完成,中间不插入其他命令,从根上避免“扣而没标记一可重复买”或“标记没扣一库存错乱”。\n\n典型返回约定:\n- -1:已购过(该用户已被限购,不能重复购买);\n- 0:售罄/库存不足,无法扣减;\n- 1:扣减成功且同时完成限购标记。\n\n伪流程:if exists(已购key) then return -1; 若库存<1 return 0; DECRBY 库存; SET 已购key; EXPIRE 已购key; return 1。\n\n另外,在数据库层再建 UNIQUE(goods_id,user_id) 唯一约束作为物理兜底,把“一人一件”钉死。",
|
|||
|
|
"keywords": [
|
|||
|
|
"同一Lua",
|
|||
|
|
"原子",
|
|||
|
|
"扣库存",
|
|||
|
|
"限购标记",
|
|||
|
|
"-1已购",
|
|||
|
|
"0售罄",
|
|||
|
|
"1成功",
|
|||
|
|
"防双写"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 6 分。①说出“扣库+已购标记”需原子连贯得 1 分;②分别指出分开实现的两类问题(先扣后没标记→可重复买 / 先标后没扣→库存错乱)各得 1 分、共 2 分;③说明写成单个 Lua 的整体原子性得 1 分;④正确说明 -1→已购 /0→售罄 /1→成功 得 2 分。",
|
|||
|
|
"explanation": "超级库存与限购标记塞进同一个 Lua 是防重复购买、防库存错乱的核心点,难度最高。答题须说明分开后会产生的两类问题,以及 -1/0/1 三个返回语义。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-007",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 2,
|
|||
|
|
"tags": [
|
|||
|
|
"Redis key",
|
|||
|
|
"无层级",
|
|||
|
|
"冒号命名",
|
|||
|
|
"扁平字典"
|
|||
|
|
],
|
|||
|
|
"question": "描述 Redis 的 key 为什么叫“无层级”。用冒号分级的 key 名(如 seckill:stock:1001)在 Redis 眼里到底是什么?命名应遵循什么规范?",
|
|||
|
|
"answer": "1. Redis 底层用一个平坦的字典(hash 表)保存所有 key,key 本质上就是一个普通字符串,不存在文件系统里的“文件夹/目录/层级”概念。\n2. 你看到的冒号【:】并不是 Redis 给它做了目录分级;Redis 只把它当作普通字符串的一部分。\n3. 因此 `seckill:stock:1001` 在 Redis 中只是一个平铺的字符串键(key 名为“seckill:stock:1001”),是扁平的、元树状结构。\n4. 冒号只是开发者约定俗成的命名分隔方式,用来表达逻辑层次、方便前缀扫描(如 SCAN 按前缀续航)和人工可读性。\n\n推荐命名规范:`业务:对象:标识`,例如:\n- seckill:stock:1001 商品 1001 的库存\n- seckill:bought:1001:user123 商品 1001 的某用户已购标记\n- seckill:limit:user123 对用户 user123 限流计数\n\n因此 key 命名有“逻辑分层”,但底层存储并无层级,性能也并不会因冒号分层而不同。",
|
|||
|
|
"keywords": [
|
|||
|
|
"Redis",
|
|||
|
|
"key",
|
|||
|
|
"字典",
|
|||
|
|
"冒号",
|
|||
|
|
"无层级",
|
|||
|
|
"命名规范"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 4 分。①说 Redis 底层为平坦 dict、key 只是普通字符串得 2 分;②点出冒号只是命名约定而非真实目录层级得 1 分;③给出“业务:对象:标识”命名规范得 1 分。",
|
|||
|
|
"explanation": "exam 考点为“Redis key 无层级”这一易混淆点。重点是澄清:并无目录树,冒号只是字符串约定。回答时用底层是 flat dict 更准确。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-008",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 5,
|
|||
|
|
"tags": [
|
|||
|
|
"hash tag",
|
|||
|
|
"Redis Cluster",
|
|||
|
|
"槽位",
|
|||
|
|
"多键",
|
|||
|
|
"Lua"
|
|||
|
|
],
|
|||
|
|
"question": "秒杀 Lua 脚本同时操作“库存 key”和“用户已购 key”两个 key 时单实例可正常执行;为什么在 Redis Cluster 下会报警?如何用 hash tag 解问题?",
|
|||
|
|
"answer": "说明 Cluster 的跨节点问题:\n1. Redis Cluster 会对 key 做 CRC16 哈希,并对 16384 取模得到槽位(slot),再把不同槽位的数据分布到不同节点上。\n2. 若同一个 Lua 脚本操作多个 key 时,这些 key 被算到不同槽位/不同节点,Cluster 无法在单个节点内原子地跨 key 执行该脚本,此时会直接报错(最典型为 CROSSSLOT Keys in request don't hash to same slot)。\n\n解决方案——hash tag `{...}`:\n1. 在多个 key 的同一节用一对花括号包住共同照段,如都写成 {seckill:1001} 前缀,Redis 只对花括号内的内容计算 CRC16 槽位,因此这些 key 会落到同一个槽位/同一个节点。\n2. 例:\n stock key:{seckill:1001}:stock\n user key :{seckill:1001}:user:1001\n 由于每两个 key 的花括号部分都是“seckill:1001”,被分配在同一 slot 同一节点,Lua 即可在该节点原子处理。\n\n补充:\n- 秒杀单实例 Redis 常见,不存在跨槽限制,无需 hash tag。\n- Cluster 且需一次操作多个 key 时,才必须用 hash tag 强制同槽;同时 hash tag 可能把相关 key 集中到同一节点,造成热点集中,需权衡。",
|
|||
|
|
"keywords": [
|
|||
|
|
"hash tag",
|
|||
|
|
"Redis Cluster",
|
|||
|
|
"CRC16",
|
|||
|
|
"槽位",
|
|||
|
|
"CROSSSLOT",
|
|||
|
|
"同一节点"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 5 分。①正确说明 Cluster 按 CRC16 把 key 分布到不同槽/节点,跨槽无法原子执行(报 CROSSSLOT 错误)得 2 分;②说明用同名花括号 `{...}` 强制同 hash 得 2 分;③给出 hash tag 具体例子({seckill:商品ID} 前缀)得 1 分。",
|
|||
|
|
"explanation": "hash tag 是 Cluster 下解决“多键在单个 Lua 中原子执行”的关键。答题要点是结合 CRC16/槽位:不同槽位无法在同一脚本原子写多个 key,用 `{}` 强制让若干 key 落到同一槽同一节点即可。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-009",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 3,
|
|||
|
|
"tags": [
|
|||
|
|
"异步",
|
|||
|
|
"消息队列",
|
|||
|
|
"削峰",
|
|||
|
|
"批量落库",
|
|||
|
|
"MQ"
|
|||
|
|
],
|
|||
|
|
"question": "秒杀扣库存成功后为什么不应“同步写库”?请说明用消息队列做异步削峰、批量落库的流程与好处。",
|
|||
|
|
"answer": "不同步写库的核心原因:\n1. 秒杀瞬间可能有极高并发产生大量成功扣库存请求,若每个都同步写库,数据库会在瞬间被大批同步连接与事务命中,直接被打穿,数据库成为瓶颈。\n2. 而最终要写入数据库其实是“成功登录成交”的少量结果,时延可以被接受,没必要同步写。\n\n异步削峰 + 批量落库流程:\n1. 扣库存成功后不下库,而是把成交事件丢到消息队列(MQ);\n2. 秒杀服务先快速给前端返回成功,把 DB 时延从主链路“剥离”出来;\n3. 消费者从 MQ 消费,是“攒一批”(例如每攒 100 条)再批量 INSERT/批量 upsert 写入数据库。\n\n好处:\n1. 削峰:瞬时高峰的 DB 压力被“摊平”到消费者的稳定消费时段,流量被缓冲而不是一次性打穿;\n2. 大幅减少交互次数:把 N 条单条写合并为 N/批 次批量写,连接、事务与 IO 开销都显著降低;\n3. 缓冲与解耦:即使 DB 短暂波动,MQ 可积压、秒杀核心不阻塞不丢数据;\n4. 提升数据库吞吐:数据库仅面对稳定批量的写。",
|
|||
|
|
"keywords": [
|
|||
|
|
"异步",
|
|||
|
|
"消息队列",
|
|||
|
|
"削峰",
|
|||
|
|
"批量",
|
|||
|
|
"解耦",
|
|||
|
|
"DB保护"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 5 分。①说明同步大量写会打爆 DB 得 1 分;②描述扣后入 MQ、消费者异步消费得 1 分;③说明攒批批量入库(降低交互至少数倍)得 2 分;④点出“削峰/缓冲/解耦”好处得 1 分。",
|
|||
|
|
"explanation": "async 已是秒杀批量落库的常考点。核心避免 N 次单条写同步 DB,改“攒一批再批量写”,在削峰的同时大幅降 DB 压力。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "sa-010",
|
|||
|
|
"type": "short_answer",
|
|||
|
|
"difficulty": 4,
|
|||
|
|
"tags": [
|
|||
|
|
"INCR",
|
|||
|
|
"EXPIRE",
|
|||
|
|
"限流",
|
|||
|
|
"原子性",
|
|||
|
|
"Redis",
|
|||
|
|
"429"
|
|||
|
|
],
|
|||
|
|
"question": "如何用 Redis 的 INCR + EXPIRE 实现“每 5 秒针对单用户限流 1 次”请求?请说明正确流程,并解释为什么必须用 INCR(原子)而不能用 GET + SET。",
|
|||
|
|
"answer": "算法流程:\n1. 以用户维度建 key(如 `seckill:limit:<userID>`),值作为计数。\n2. 对该 key 做 INCR 自增并获取返回值。\n3. 若这是首次访问(返回值为 1),说明是新窗口的开始,设置 EXPIRE 5 秒,从而每个 5 秒形成一个计数窗口。\n4. 判断 count 是否超过阈值(比如限制 5 秒 1 次则阈值为 1);超过即返回 429(被限流)。\n\nGo 核心代码:\nkey := \"seckill:limit:\" + userID\ncount,_ := rdb.Incr(ctx,key).Result()\nif count == 1 { _ = rdb.Expire(ctx,key, 5*time.Second).Err() }\nif count > 1 { return 429 }\n\n为何用 INCR 而不用 GET + SET:\n- 若先 GET 后 SET(或 GET 后根据值决定SET),会出现 TOCTOU 竞态:多个并发请求同时读到相同的旧值,分别 +1 再写回,计数会丢失/失真——请求次数根本没被记准,限流可能被绕过或误判。\n- 而 INCR 是 Redis 原子操作:底层单条命令完成“自增并返回”,并发请求会排队,各次都能拿到正确且唯一的值。\n- 加上“首次访问设置 EXPIRE”,可实现窗口到点自动复位、不再需要手动清理。\n\n注意:只有 count==1(新窗口首个请求)时才设置 EXPIRE,否则会把同一窗口的过期时间不断延长,破坏限流。",
|
|||
|
|
"keywords": [
|
|||
|
|
"INCR",
|
|||
|
|
"原子性",
|
|||
|
|
"EXPIRE",
|
|||
|
|
"窗口",
|
|||
|
|
"429",
|
|||
|
|
"竞态",
|
|||
|
|
"限流"
|
|||
|
|
],
|
|||
|
|
"scoring_rubric": "共 5 分。①描述以用户维度建 key 并用 INCR 自增得 1 分;②正确给出首次访问(count==1)时设置 EXPIRE 的窗口逻辑得 2 分;③说明阈值判断超限返回 429 得 1 分;④解释为什么用 INCR(原子)可比 GET+SET 避免 TOCTOU 竞态失真得 1 分。",
|
|||
|
|
"explanation": "这是限流计数的实现。核心用 INCR 的原子性避免并发计数竞态,并配合首次访问设 EXPIRE 控制窗口过期复位。能写出 Go 或 Lua 伪代码更容易得满分。",
|
|||
|
|
"source": null,
|
|||
|
|
"related": []
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|