什么是幂等 (Idempotent)

IT 技术 4 阅读 更新于 2026-10-24 07:00

幂等(Idempotent)

一句话:同一个操作,执行一次 和 重复执行 N 次,结果完全一样,不会产生副作用,这个操作就是幂等的。

数学来源:
函数满足 \(f(f(x)) = f(x)\),调用多少次,最终结果不变。

举例子

✅ 幂等操作

  1. 查询:select 余额,查1次、查10次,余额不变
  2. 更新固定值:把用户状态改成 已支付,执行一次是已支付,再执行还是已支付
  3. 删除:删除id=123订单,删一次成功,再删返回“不存在”,最终效果一致

❌ 非幂等操作(重复执行会出问题)

  1. 扣款:余额 = 余额 - 100,点两次扣两次钱
  2. 累加:积分 += 10,重复提交就多次加积分

后端开发里为什么要幂等?

解决重复请求问题:

  • 网络卡顿,用户多次点提交按钮
  • 超时重试、MQ消息重复投递
  • 前端重复调用接口

如果接口不做幂等,就会:重复下单、重复扣款、重复发奖。

常见幂等实现方案

  1. 唯一业务单号(幂等号):前端生成全局唯一id,第一次处理,后续直接返回上次结果
  2. 数据库唯一索引:订单号加唯一约束,重复插入直接报错拦截
  3. 状态机控制:订单状态流转,已支付不再走支付逻辑
  4. 分布式锁:防止并发重复处理
  5. Token机制:提交前拿一次性token,用过即作废

简单口诀

一次和多次,效果相同 = 幂等;多次会变,就不是幂等。

要不要我给你写一段极简的幂等接口伪代码?

方案:幂等号(idempotentKey)方案,最常用极简版

逻辑:

  1. 前端每次发起业务请求,带上全局唯一 idempotentKey(由前端生成,一次业务一个key)
  2. 服务端先查这个key是否已经处理过
  3. 已处理:直接返回上次结果,不执行业务
  4. 未处理:执行业务,成功后把 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}"}

⚠️ 这个方案的坑(重点)

  1. Redis宕机/数据丢失风险
    Redis是内存缓存,一旦丢数据,幂等防护失效。生产最佳实践:Redis做前置拦截,最终用数据库幂等记录/唯一索引兜底。
  2. 不要用 get + set 分开写!会有并发竞争,必须用 SET ... NX EX 原子命令。
  3. 过期时间设置:大于业务最大执行时长,防止业务还没跑完key过期,重复进入。
  4. 区分两种状态: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 可直接跑的版本。

← 返回IT 技术 yicool 百科 · 什么是幂等 (Idempotent)

评论 0