幂等(Idempotent)
一句话:同一个操作,执行一次 和 重复执行 N 次,结果完全一样,不会产生副作用,这个操作就是幂等的。
数学来源:
函数满足 \(f(f(x)) = f(x)\),调用多少次,最终结果不变。
举例子
✅ 幂等操作
- 查询:
select 余额,查1次、查10次,余额不变 - 更新固定值:
把用户状态改成 已支付,执行一次是已支付,再执行还是已支付 - 删除:
删除id=123订单,删一次成功,再删返回“不存在”,最终效果一致
❌ 非幂等操作(重复执行会出问题)
- 扣款:
余额 = 余额 - 100,点两次扣两次钱 - 累加:
积分 += 10,重复提交就多次加积分
后端开发里为什么要幂等?
解决重复请求问题:
- 网络卡顿,用户多次点提交按钮
- 超时重试、MQ消息重复投递
- 前端重复调用接口
如果接口不做幂等,就会:重复下单、重复扣款、重复发奖。
常见幂等实现方案
- 唯一业务单号(幂等号):前端生成全局唯一id,第一次处理,后续直接返回上次结果
- 数据库唯一索引:订单号加唯一约束,重复插入直接报错拦截
- 状态机控制:订单状态流转,已支付不再走支付逻辑
- 分布式锁:防止并发重复处理
- Token机制:提交前拿一次性token,用过即作废
简单口诀
一次和多次,效果相同 = 幂等;多次会变,就不是幂等。
要不要我给你写一段极简的幂等接口伪代码?
方案:幂等号(idempotentKey)方案,最常用极简版
逻辑:
- 前端每次发起业务请求,带上全局唯一
idempotentKey(由前端生成,一次业务一个key) - 服务端先查这个key是否已经处理过
- 已处理:直接返回上次结果,不执行业务
- 未处理:执行业务,成功后把 key + 结果存入幂等记录表
# 伪代码(Python风格)
def pay_order(idempotentKey, orderId, amount):
# 1. 查询幂等记录
record = idempotent_table.get(idempotentKey)
if record is not None:
# 已经处理过,直接返回上次结果
return {
"code": 0,
"msg": "已处理",
"data": record.result
}
# 2. 执行业务(扣款,非幂等操作)
try:
db.begin_transaction()
# 扣余额
user_account.minus_balance(amount)
# 更新订单为已支付
order.set_paid(orderId)
# 插入幂等记录,标记成功
idempotent_table.insert(idempotentKey, result={"status": "success"})
db.commit()
except Exception as e:
db.rollback()
return {"code": -1, "msg": "支付失败"}
return {"code": 0, "msg": "支付成功"}
⚠️ 关键点:幂等记录插入 和 业务操作 放在同一个事务,防止业务成功、幂等记录没写,导致重复执行。
简化版【数据库唯一索引方案】(适合新增场景)
-- 幂等表,idempotent_key 加唯一索引
CREATE TABLE order_main (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) UNIQUE, -- 业务唯一号,天然幂等
amount DECIMAL(10,2),
status TINYINT
);
不需要单独查幂等表,依靠唯一索引拦截重复插入:
-- 幂等表,idempotent_key 加唯一索引
CREATE TABLE order_main (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) UNIQUE, -- 业务唯一号,天然幂等
amount DECIMAL(10,2),
status TINYINT
);
def create_order(orderNo, amount):
try:
# 重复orderNo会触发唯一索引异常,直接拦截
db.execute("INSERT INTO order_main(order_no, amount, status) VALUES (?, ?, 0)", orderNo, amount)
return {"code":0, "msg":"下单成功"}
except UniqueKeyError:
return {"code":0, "msg":"订单已存在"}
补充:状态机幂等(订单状态)
def pay(orderId):
order = order_table.get(orderId)
if order.status == "PAID":
return {"msg":"订单已支付"}
if order.status != "WAIT_PAY":
return {"msg":"订单状态非法"}
# 只有待支付才执行扣款
do_pay_logic(orderId)
order.status = "PAID"
order.save()
return {"msg":"支付成功"}
如果你想要,我可以再给你一个 Redis 版幂等伪代码(适合高并发)。
核心思路:Redis 存幂等标记 + SETNX(或 SET key value NX EX),原子操作,避免并发击穿;注意:SETNX 只锁住“能不能进入业务”,业务成功后最好再落库兜底,防止 Redis 丢数据
NX:key不存在才设置;EX:过期时间(防止key无限堆积)
# Redis版高并发幂等伪代码
import redis
rd = redis.Redis()
def pay_order(idempotent_key, order_id, amount):
# 1. 原子抢占幂等标记:NX=不存在才写入,设置过期时间(比如5分钟,根据业务超时调整)
# value=processing 代表正在处理中
ok = rd.set(idempotent_key, "processing", nx=True, ex=300)
if not ok:
# 获取key的值,判断是处理中还是已完成
val = rd.get(idempotent_key)
if val == b"success":
return {"code":0, "msg":"该请求已处理成功"}
else:
return {"code":-1, "msg":"请求正在处理中,请勿重复提交"}
# ========== 抢到幂等锁,开始执行业务 ==========
try:
db.begin_transaction()
# 非幂等业务操作:扣款、改订单状态
account = db.query("select * from user_account where id=? for update", uid)
if account.balance < amount:
raise Exception("余额不足")
account.balance -= amount
account.update()
db.execute("update t_order set status='PAID' where id=?", order_id)
db.commit()
# ✅ 业务成功:更新redis标记为success,永久有效可以配合数据库兜底
rd.set(idempotent_key, "success")
return {"code":0, "msg":"支付成功"}
except Exception as e:
db.rollback()
# ❌ 业务失败:删除幂等key,允许用户重试
rd.delete(idempotent_key)
return {"code":-1, "msg":f"支付失败:{e}"}
⚠️ 这个方案的坑(重点)
- Redis宕机/数据丢失风险
Redis是内存缓存,一旦丢数据,幂等防护失效。生产最佳实践:Redis做前置拦截,最终用数据库幂等记录/唯一索引兜底。 - 不要用
get + set分开写!会有并发竞争,必须用SET ... NX EX原子命令。 - 过期时间设置:大于业务最大执行时长,防止业务还没跑完key过期,重复进入。
- 区分两种状态:
processing(处理中)、success(处理完成)- 如果只存一个布尔标记,业务耗时很长时,重复请求会直接放行,失去幂等。
极简变体(只做防重复提交,轻量接口)
def submit(idempotent_key):
# 30秒防重
if not rd.set(idempotent_key, "1", nx=True, ex=30):
return {"code":-1, "msg":"请勿重复提交"}
# 执行业务
do_biz()
return {"code":0, "msg":"成功"}
进阶:Redis+数据库兜底组合伪代码(生产推荐)
def pay_order_safe(idempotent_key, order_id, amount):
# 先Redis拦截
ok = rd.set(idempotent_key, "processing", nx=True, ex=300)
if not ok:
val = rd.get(idempotent_key)
if val == b"success":
return {"code":0, "msg":"已处理"}
return {"code":-1, "msg":"处理中"}
try:
db.begin_transaction()
# 数据库幂等表兜底
exist = db.query("select 1 from t_idempotent where idempotent_key=?", idempotent_key)
if exist:
db.commit()
rd.set(idempotent_key, "success")
return {"code":0, "msg":"已处理"}
# 业务逻辑
do_pay(order_id, amount)
# 写入数据库幂等记录(持久化兜底)
db.execute("insert into t_idempotent(idempotent_key,result) values(?,?)", idempotent_key, "success")
db.commit()
rd.set(idempotent_key, "success")
return {"code":0, "msg":"支付成功"}
except Exception as e:
db.rollback()
rd.delete(idempotent_key)
return {"code":-1, "msg":"失败"}
如果你想要,我可以给一段 Java SpringBoot + RedisTemplate 可直接跑的版本。