介绍
✅ 什么是等幂(Idempotency)?
等幂
无论这个操作被执行多少次,结果都是一样的,不会因为多次执行而产生副作用。
通俗一点说:“点一次和点一百次,效果是一样的。”
✅ 在接口中,什么是等幂操作?
在 Web / API 开发中,一个 等幂操作的接口,意味着客户端(用户、服务、浏览器)多次请求同一个接口,结果不变,也不会影响系统的状态或数据重复修改。
操作 | 等幂性 | 原因 |
---|---|---|
GET /user/123 | ✅ 是 | 多次获取用户信息不改变任何东西 |
DELETE /user/123 | ✅ 是 | 删除一次和删除多次效果一样,用户都不存在 |
PUT /user/123 {name: “Tom”} | ✅ 是 | 每次更新为相同数据,结果一样 |
POST /user | ❌ 否 | 每次都会新建一个用户,重复多次会创建多个资源 |
• PUT 是等幂的,因为它是“更新为某个状态”
• POST 不是等幂的,因为它每次都是“创建新的资源”
✅ 为什么等幂很重要?
• 防止重复扣款、重复下单、重复删除等问题;
• 支持客户端/中间代理的自动重试;
• 提高系统容错能力。
✅ 如何实现接口等幂?
常见做法有:
幂等键(Idempotency Key):客户端每次请求都带上一个唯一 key,服务端缓存这个 key,避免重复处理。例如支付场景就常用这个机制。
根据业务设计逻辑保证幂等:比如数据库 INSERT 改为 UPSERT(存在则更新,不存在则插入)。
幂等性中间件 / 请求锁定机制:防止重复请求在短时间内被处理多次。
处理PUT的等幂
多次调用会修改 update_time、产生日志、触发 webhook、更新缓存等等副作用。
理论上:
• PUT /resource/123 的语义是:把这个资源更新成某个固定状态。
• 所以连续多次执行 PUT(用相同数据),最终资源状态是一致的 —— 这是“等幂”。
实际上:
• 即使数据一样,每次 PUT 可能都会执行:
• 自动更新时间戳(update_time)
• 写数据库变更日志
• 写操作审计表
• 发送消息到 MQ
• 清理或更新缓存
➤ 这些副作用就
破坏了等幂性
👉 方式一:判断数据是否变更
if new_data != old_data:
do_update()
• 如果数据一样,直接跳过写入、跳过更新时间戳等。
• 这种方式最简单,适合“频繁重复 PUT”的场景。
👉 方式二:允许更新,但保持副作用幂等
• 比如:
• update_time 只在数据真正变更时更新;
• 日志、MQ 消息仅在内容变更时才触发;
• 或使用幂等锁 + 缓存处理。
👉 方式三:使用幂等 key(更适合 POST)
比如前端在 10s 内重试,带上幂等 key,服务端只处理一次。
✅ 针对 update_time 的建议做法:
def update_user(user_id, new_data):
old_data = db.get_user(user_id)
if new_data == old_data:
return # 数据没变,不做更新
new_data["update_time"] = now()
db.update_user(user_id, new_data)
幂等锁+缓存处理
这是用于更复杂、可能存在并发请求或前端重复请求场景的策略。
📌 场景:
假设你的接口会被短时间内 多次调用(并发 or 重试):
PUT /order/123/status {"status": "paid"}
应该避免:
• 用户一不小心连点了 2 次;
• 前端接口设置了“自动重试机制”;
• 网关/中间层产生了重复调用。
✅ 解决方式:使用「幂等锁」
给每次请求生成一个幂等 key(比如前端传递一个唯一 idempotent-key);
使用缓存(Redis)记录这个 key 的处理状态;
判断是否已经处理过,如果是,就跳过执行逻辑。
# 接收到请求
key = f"idempotent:{user_id}:{operation_id}"
if redis.exists(key):
return "已处理,直接返回"
# 设置锁,有效期60秒
redis.set(key, "processing", ex=60)
try:
# 执行更新操作
db.update_order_status("paid")
mq.send("order.paid")
write_log("用户付款成功")
finally:
redis.delete(key)
等幂锁流程
✅ 幂等锁的完整流程设计(前后端协作)
🔸 适用场景:
• 用户发起重要操作,如:下单、支付、扣积分、修改状态
• 你希望避免:
• 用户手抖点两下
• 前端接口自动重试
• 网关中转多次
• 并发执行相同逻辑导致“重复创建 / 重复扣款”
✅ 正确的幂等锁逻辑应该是:
✅ 「令牌模式」幂等方案
后端生成 幂等 key,前端持有这个 key,之后带着这个 key 去执行幂等请求。
模式 | 说明 | 适合场景 |
---|---|---|
令牌模式 | 前端先拿一个幂等 key(令牌),再带着 key 去调接口 | 用户主动操作型,如“提交订单” |
前端生成UUID | 前端自己生成 UUID,当做幂等 key 发请求 | 自动重试 / 服务间调用 场景 |
令牌方式:
• 幂等 key 生命周期完全由后端掌控 ✅;
• 安全性高,不依赖前端生成 uuid 的正确性 ✅;
• 能配合业务类型(如创建订单、支付、退款)做细粒度控制 ✅;
• 可以直接缓存执行结果,实现「重复请求 → 直接返回结果」 ✅;
维度 | 你说的方式(后端生成) | 之前的方式(前端生成) |
---|---|---|
控制权 | 在后端 ✅ | 在前端 ❌ |
安全性 | 更高 ✅ | 依赖前端或外部系统 |
结果缓存 | 容易实现 ✅ | 实现麻烦 ❌ |
实现复杂度 | 多一步(key申请) | 略简单 |
应用场景 | 提交表单、支付类接口 | 微服务调用、幂等补偿逻辑 |
幂等令牌机制”,非常适合处理「用户主动操作 + 严格控制重复」的接口,强烈推荐在 支付、下单、扣款等业务中使用。
注意:
✅ 前端生成 UUID 并不是简单「纯随机」,它要遵循 可识别性 或 可复用性 的规则,才能让后端识别同一个操作。
✅ 正确的前端 UUID 幂等 key 设计方案
🔸 前提场景适用:
• 后端不参与幂等 key 生成(例如微服务架构中,一些服务没有共享 Redis)
• 前端或调用方能控制 key 生成(如 App / 网关 / API 调用方)
• 通常用于:接口重试、任务去重、上传文件避免重复入库等
需要生成的 UUID 实际是“结构化唯一 key”,并不是完全随机,比如:
idempotent:<业务类型>:<用户ID>:<业务ID或时间戳>
业务场景 | 幂等 key 示例 |
---|---|
下单 | idemp:order:uid123:order456 |
修改用户信息 | idemp:user:update:uid123 |
提交问卷 | idemp:survey:uid123:survey_20240330 |
提交任务(时间窗口内) | idemp:task:uid123:20240330T10 |
Key 设计 | 是否推荐 | 原因 |
---|---|---|
完全随机 UUID | ❌ 不推荐 | 后端无法判断是否是重复操作 |
带业务结构的 key | ✅ 推荐 | 可以判断“是不是相同操作” |
后端统一分发 key | ✅ 更推荐 | 更安全、更集中控制 |
✨
与其在前端搞业务 ID / 时间戳构造 key,不如直接用令牌机制:后端统一生成并下发幂等 key,前端拿着用。
对比点 | 时间戳方案(前端方案) | 令牌机制(后端) |
---|---|---|
幂等 key 来源 | 前端生成时间戳 | 后端统一生成 |
是否真正幂等 | ❌ 多次执行都不同 | ✅ 同一个 key 保证只执行一次 |
是否能缓存结果 | ❌ 难以复用 | ✅ 可直接返回上次执行结果 |
实现复杂度 | 中(要设计 key 格式) | 高一点(需要额外接口) |
安全性 & 可控性 | ❌ 前端失控 | ✅ 后端控制 |