Files
autumn-recruitment/05.架构/idempotency/幂等设计方案_test.md
T

146 lines
6.7 KiB
Markdown
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.
---
tags: [test/review, architecture, arch/idempotency]
create time: 2026-08-09 12:00
---
# 幂等设计方案 — 测试题
## 概述
本测试覆盖六种主流幂等方案(唯一索引防重、Token 预获取、乐观锁版本号校验、状态机校验、网关 Request ID 去重)以及三层防御最佳实践。共 10 道题目(6 选择 + 3 填空 + 1 简答)。
---
## 一、选择题(6道,由浅入深)
> **难度阶梯**: Q1-Q2 基础概念 → Q3-Q4 核心原理 → Q5-Q6 深入应用/边界场景
### Q1(基础)— 考察定义层面
以下哪个描述最准确地表达了"幂等"的概念?
A. 相同的请求只在系统中出现一次
B. 多次执行同一操作与执行一次的效果相同
C. 使用 Redis SETNX 来防止重复执行
D. 数据库的 UNIQUE INDEX 约束
### Q2(基础)→
以下哪种幂等方案**不适合**短延迟批处理场景(如批量导入数据)?
A. 数据库唯一索引防重
B. Token 预获取模式
C. 乐观锁版本号校验
D. 网关层 Request ID 去重
### Q3(进阶)— 核心原理
Lua 脚本 `if redis.call("GET", key) == expected then redis.call("SET", key, "USED") end` 在 Token 幂等模式中的核心作用是什么?
A. 设置 Token 的过期时间
B. 原子性地检查 Token 是否有效并标记为已使用
C. 生成一个新的 Token
D. 删除已经过期的所有 Token
### Q4(进阶)— 比较/辨析
以下关于各幂等方案的对比中,哪一个是错误的?
A. 唯一索引防重的实现复杂度低但会额外占用存储
B. Token 预获取可以防止 CSRF
C. 状态机校验不需要额外的数据结构
D. 网关 Request ID 方案可以保证跨重试窗口外的重复请求也被拦截
### Q5(深入)— 场景推理
某支付系统收到同一笔交易的重复扣款请求(同一个 biz_no)。以下哪种组合方案能提供最可靠的防护?
A. 仅用数据库唯一索引
B. 仅用网关 Request ID
C. 三层防御:网关 Request ID → Token 模式 → 数据库唯一索引兜底
D. 仅用乐观锁版本号
### Q6(深入)— 源码级/边界场景
使用唯一索引做幂等时,以下哪种做法可以正确避免 ToCToU 竞态条件?
A. SELECT count(1) WHERE biz_no='xxx',如果为 0 则 INSERT
B. 直接 INSERT,利用 Duplicate entry 错误判断已被处理过
C. SELECT FOR UPDATE 后手动插入
D. 先在内存 Map 中记录已处理的 biz_no
---
## 二、填空题(3道)
### F1 — 填空1
去重是_____,幂等是_____。去重是手段,幂等是目标——用唯一索引去重是为了达到幂等效果。
> **提示**: 原文中有明确的区分:"XX是强调语义...YY是强调数据..."
### F2 — 填空2
Token 预获取模式中,客户端先调用 _____ 接口获取一次性 Token,服务端存入 Redis 并设置过期时间(如 5 分钟),然后客户端携带 Token 发起实际业务请求。
> **提示**: 回想原文中描述的完整流程步骤。
### F3 — 填空3
Redis SETNX **不适合**单独做业务幂等的原因是:Redis 非持久化宕机可能丢数据;而且 SETNX 只能保证互斥,不能保证"之前是否已经_____过"。
> **提示**: 考虑 SETNX 的能力边界。
---
## 三、简答题(1道)
### S1
某电商下单接口面临用户重复点击导致重复创建订单的问题。请设计一个完整的幂等方案,要求:
1. 从流量入口到数据存储的全链路设计
2. 说明每层的作用和依赖关系
3. 讨论失败降级策略
> **答题框架提示**:
> 1. 第一道防线:什么位置做什么事
> 2. 第二道防线:什么机制保证什么
> 3. 第三道防线:最终兜底
> 4. 异常场景分析
---
## 参考答案与解析
### 选择题答案
| 题号 | 正确答案 | 解析 |
|------|---------|------|
| Q1 | B | 幂等的定义是"多次执行与执行一次效果相同"。A 是去重的定义,C 和 D 是实现幂等的手段而非定义本身。 |
| Q2 | B | Token 模式需要多一次 API 调用(先领 token 再提交),对于批量导入这种高频操作会严重拖慢用户体验。此时唯一索引方案更合适。 |
| Q3 | B | Lua 脚本将 GET 和 SET 打包成原子操作——不存在 ToCToU 竞态。如果分开执行两个步骤,其他线程可能在中间时刻拿到同一个 Token。 |
| Q4 | D | D 是错误的!网关 Request ID 只保护单次请求窗口内的重复(通常设定 TTL 如 300 秒),超出窗口的重复请求无法覆盖。这是其固有限制。 |
| Q5 | C | 三层防御互为补充:网关层拦截大部分重复流量降低后端压力,Token 模式控制高风险操作的不可重入性,数据库唯一索引作为最后防线保证最终一致性。任何一层被突破都有下一层兜住。 |
| Q6 | B | 直接 INSERT 利用 Duplicate entry 错误是最原子性的做法。A 是典型的 ToCToU——检查和插入之间有竞态窗口。C 的 SELECT FOR UPDATE 也可以但不如 D 简洁高效。D 的内存 Map 不解决分布式问题。 |
### 填空题答案
| 题号 | 答案 | 解析 |
|------|------|------|
| F1 | `数据`;`语义` | 去重强调数据层面——相同的请求只出现一次;幂等强调语义层面——多次执行结果相同。去重是手段,幂等是目标。 |
| F2 | `/token/generate` | Token 预获取流程:① 客户端调用 /token/generate 获取一次性 Token;② 服务端存入 Redis,UNUSABLE 状态,5 分钟过期;③ 客户端携带 Token 发起业务请求;④ 服务端 Lua 原子消费 Token。 |
| F3 | `成功执行` | SETNX 只能保证"此刻没人持有这个 key",但不能回答"之前是否已经成功处理过这个请求"。除非你在 Redis 里也存一份状态——那本质上就是把数据库该做的事搬到了 Redis。 |
### 简答题参考答案
S1:**参考答案要点**:
1. **第一道防线(网关层)**:客户端在 HTTP 头中携带 X-Request-ID(UUID),网关层用 Redis SETNX reqid:id 1 EX 300s 做去重。重复请求直接从缓存返回之前的响应。
2. **第二道防线(业务层)**:对支付等高风险接口采用 Token 模式——用户必须先领取一次性 Token 才能提交订单,Token 原子消费确保不可重入。
3. **第三道防线(存储层)**:order_idempotent 表以 biz_no 为唯一索引。INSERT 时 Duplicate entry → 查询已有结果返回。
4. **异常降级**:网关层 Redis 挂了时放行(靠后续两层兜底);Token 服务挂了时切换到 Request ID 模式(用户体验下降但不会中断);数据库层永远生效因为它是物理约束。
**评分标准**:答出任意 2 个要点即可得满分;完全正确需覆盖全部要点。
## 关联笔记
- [[Redis 分布式锁]]
- [[ZooKeeper 分布式锁]]