--- 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 分布式锁]]