常见场景下的幂等性及解决方案

幂等性定义

幂等性是指:调用方对同一个接口发起一次请求和多次相同请求,最终产生的业务结果是一致的。

需要注意的是,幂等并不代表每次请求返回的内容都完全相同,而是指多次调用不会产生额外的业务副作用。

例如:

  • 一次支付请求和重复提交多次支付请求,最终只能扣款一次。
  • 一次创建订单请求和重复提交多次创建订单请求,最终只能生成一笔订单。
  • 同一个消息被 MQ 重复投递多次,最终只能执行业务一次。

常见场景

  • 支付场景:支付系统必须保证同一笔支付请求不会被重复扣款,这是支付平台对用户资金安全最基本的保障。

  • 订单场景:避免用户因重复点击、网络重试或消息重复投递而重复下单。在秒杀、抢购等场景中,还需要防止同一用户重复创建订单并多次扣减库存。

  • AI 对话场景:在流式输出过程中,需要避免同一段文本或同一个 Token 被重复推送,否则可能导致前端内容重复,甚至引起语义错误。

常见解决方案

幂等方案的核心思想基本一致:

为一次业务操作生成唯一标识,并通过唯一性约束判断该操作是否已经执行过。

这个唯一标识通常被称为幂等 Key。

幂等 Key 可以根据具体业务生成,例如:

text 复制代码
业务名称 + 业务主键

例如:

text 复制代码
payment_10001
order_20001
message_30001_10

其中,幂等 Key 必须能够唯一标识一次业务操作,而不能只标识某个接口或某类业务。

1. 基于业务状态机实现幂等

对于具有明确状态流转的业务,可以利用业务 ID 和当前状态进行条件更新。

假设业务状态只能按照以下顺序流转:

text 复制代码
A → B → C

当请求将状态从 A 更新为 B 时,可以执行类似下面的 SQL:

sql 复制代码
UPDATE table_name
SET status = 'B'
WHERE id = ? AND status = 'A';

第一次请求执行时,数据状态为 A,因此更新成功。

重复请求再次执行时,数据状态已经变成 B,不再满足 status = 'A' 的条件,因此更新失败。

程序可以通过 SQL 影响行数判断是否成功:

  • 影响行数为 1:本次状态变更成功;

  • 影响行数为 0:状态已经发生变化,本次请求可能属于重复请求或非法状态流转。

这种方式本质上是利用数据库的条件更新实现乐观锁,从而保证状态只能成功流转一次。

2. 基于唯一键去重

可以为每一次业务操作生成唯一 Key,并依靠数据库唯一索引、Redis SET NX 或前端本地状态进行去重。

例如,在 AI 对话的流式输出场景中,可以使用:

text 复制代码
请求 ID + 内容偏移量

作为每一段流式数据的唯一标识:

text 复制代码
requestId_offset

例如:

text 复制代码
chat_10001_0
chat_10001_10
chat_10001_20

前端接收到数据后,可以记录已经处理过的偏移量。如果再次收到相同的 请求 ID + 偏移量,则直接丢弃,避免同一段内容被重复渲染。

这种方案相当于让前端承担流式数据的去重职责。

如果是服务端写入数据库的场景,也可以为该唯一标识建立唯一索引:

sql 复制代码
UNIQUE(request_id, offset)

当重复数据再次写入时,数据库会因为唯一键冲突而拒绝插入。

3. 基于 MQ、Redis 和消费状态实现幂等

MQ 通常只能保证消息至少被投递一次,因此在消费者执行超时、网络异常或消费确认失败时,同一条消息可能会被重复投递。

消费者需要根据消息中的业务唯一标识生成幂等 Key,例如:

text 复制代码
业务名称 + 消息业务 ID

消息消费流程如下:

text 复制代码
收到消息
    ↓
生成幂等 Key
    ↓
通过 Redis SET NX 写入 CONSUMING
    ↓
判断是否成功获得消费资格

第一次消费

消费者通过 Redis 原子命令写入:

text 复制代码
Key:幂等 Key
Value:CONSUMING

对应的 Redis 命令类似于:

redis 复制代码
SET idempotent_key CONSUMING NX EX 600

如果写入成功,说明当前消息尚未被其他消费者处理,本次消费者获得业务执行资格。

随后执行业务逻辑:

  • 业务执行成功:将 Redis 状态修改为 CONSUMED

  • 业务执行失败:删除 Redis Key,或者等待 Key 超时,使 MQ 后续能够重新消费。

完整状态变化如下:

text 复制代码
不存在
   ↓
CONSUMING
   ↓
CONSUMED

重复消费

如果 Redis SET NX 执行失败,说明对应的幂等 Key 已经存在。

此时需要读取 Redis 中的消费状态。

状态为 CONSUMING

表示该消息当前正在被其他消费者处理。

此时不能直接将消息视为消费成功,因为原消费者可能最终执行失败。当前消费者应该抛出异常,让 MQ 在一段时间后重新投递消息。

状态为 CONSUMED

表示该消息已经成功处理过。

当前消费者不再执行业务逻辑,直接向 MQ 返回消费成功即可。MQ 在收到消费成功的确认后,不会继续重试该消息。

这里并不是由消费者主动删除 MQ 中的消息,而是消费者返回消费成功,由 Broker 根据消息消费进度处理后续逻辑。

4. 数据库唯一约束作为最终兜底

Redis 状态并不是绝对可靠的。

例如,可能出现以下情况:

text 复制代码
业务数据库写入成功
    ↓
Redis 更新为 CONSUMED 失败
    ↓
MQ 再次投递消息

此时 Redis 中可能仍然是 CONSUMING,甚至 Key 已经过期。如果完全依赖 Redis,消息可能再次执行业务。

因此,对于支付、订单、库存等关键业务,通常还需要结合数据库唯一索引或业务状态机进行最终兜底。

例如,为支付流水号建立唯一索引:

sql 复制代码
UNIQUE(payment_no)

即使 Redis 幂等判断失效,重复插入相同支付流水时,数据库也会拒绝第二次写入。

因此,更可靠的幂等方案通常是:

text 复制代码
Redis 快速拦截重复请求
        +
数据库唯一索引或状态机最终兜底

Redis 主要用于提高性能和减少重复请求进入业务系统,数据库约束则负责保证最终的数据一致性。

相关推荐
小陈工8 小时前
第7篇:Django框架核心原理与实战深度解析(下)
后端·python·面试
她的男孩8 小时前
低代码只能做单表 CRUD?我们一行代码没写,搭了个完整进销存
java·后端·架构
Conan在掘金8 小时前
鸿蒙 ArkUI 进阶:@Provide 和 @Consume,跨层传递的「直通车」,告别 props 层层透
后端
用户298698530149 小时前
Python 数据处理:XML 与 Excel 互转的实用指南
后端·python·excel
用户9931441579849 小时前
Java打包操作编译报错
后端
SimonKing9 小时前
Agnes AI出桌面版了,可图可视频,免费用
java·后端·程序员
CodeSheep9 小时前
有这4个迹象,你就该离职了!
前端·后端·程序员
程序员爱钓鱼9 小时前
Rust 元组 Tuple 详解:组合不同类型的数据
前端·后端·rust
IT_陈寒9 小时前
React的useEffect为什么经常执行两次?
前端·人工智能·后端