【Java面试】如何保证接口的幂等性?
使用 Request ID(请求唯一标识) 是最简单且常用的接口幂等性方案,但在实际应用中需结合存储和校验机制才能完整生效。以下是具体实现方案及注意事项:
🔑 一、Request ID 方案核心步骤
- 生成唯一ID
客户端在请求时生成全局唯一ID(如UUID
、雪花算法ID
),通过请求头(如X-Request-ID
)或参数传递给服务端。 - 服务端校验与存储
服务端通过缓存(如 Redis)或数据库存储 Request ID,校验逻辑如下:// 伪代码示例:基于Redis的校验 String requestId = "unique-request-123"; String key = "idempotent:" + requestId; // 原子操作:若ID不存在则设置并返回true,否则返回false boolean isNewRequest = redis.setIfAbsent(key, "1", 10, TimeUnit.MINUTES); if (!isNewRequest) {return "重复请求,直接返回上次结果"; // 幂等响应 } // 首次请求,执行业务逻辑
- 业务处理完成后保留结果
若需返回相同结果给重复请求,可将处理结果缓存(如用 Request ID 作为Key存储结果)。
⚠️ 二、单独使用 Request ID 的局限性
尽管 Request ID 是基础方案,但需注意以下问题:
- 原子性漏洞
若未保证“校验 ID 存在性”和“标记 ID 已处理”的原子性,高并发时可能重复执行业务。
✅ 解决:用 Redis 的SETNX
命令或 Lua 脚本 - 存储依赖风险
- Redis 宕机可能导致 ID 丢失(需持久化或降级方案)。
- 数据库防重表可替代,但性能较低。
- 状态型操作不适用
例如订单支付,若第一次请求已扣款,重复请求应返回“已支付”而非“支付成功”。此时需结合 状态机:UPDATE orders SET status = 'paid' WHERE id = 123 AND status = 'unpaid'; -- 仅当状态为未支付时更新
🛠️ 三、不同场景的优化方案
场景 | 推荐方案 | 结合 Request ID 的用法 |
---|---|---|
表单提交/订单创建 | Request ID + Redis 原子校验 | 用 ID 拦截重复请求 |
支付/状态变更 | Request ID + 状态机 | ID 确保请求唯一,状态机保证业务逻辑正确 |
高并发更新(如库存扣减) | Request ID + 乐观锁 | ID 防重提交,乐观锁避免超卖 |
分布式系统调用 | Request ID + 分布式锁(Redisson) | ID 标记请求,锁保证全局唯一执行 |
💡 四、生产环境注意事项
- ID 生成策略
- 分布式场景用
雪花算法
或UUID
,避免单点ID重复。
- 分布式场景用
- 过期时间设置
- 缓存中 Request ID 的 TTL 需大于业务最大处理时间(如支付系统设为30分钟)。
- 降级方案
- Redis 不可用时,可降级为数据库唯一索引或本地缓存(需考虑集群一致性)。
- 客户端配合
- 前端在提交后禁用按钮,减少重复请求。
💎 总结
Request ID 是幂等性的基础方案,适用于大多数简单场景,但需搭配原子存储(Redis/数据库)才能生效。
- ✅ 适用:数据插入、无状态请求(如短信发送)。
- ⚠️ 需增强:状态变更、高并发更新需结合状态机/锁。
- 一句话回答面试官:
“Request ID 是通用方案,通过唯一标识拦截重复请求,但需原子操作保证校验可靠性,并结合业务场景选择存储介质(Redis/数据库)和过期策略。”