banner
NEWS LETTER

后端接口幂等设计:重复提交、重试、订单和消息消费

Scroll down

一、什么是幂等

幂等指同一个操作执行一次和执行多次,最终产生的业务结果一致。

例如查询接口天然是幂等的:

1
GET /api/orders/1001

多查几次不会改变订单状态。

但创建订单、支付回调、库存扣减、消息消费就不一定幂等:

1
2
3
POST /api/orders
POST /api/pay/callback
POST /api/coupon/receive

如果用户连点两次、客户端超时重试、网关重复转发、消息队列重复投递,就可能产生重复订单、重复扣款或重复发券。

二、哪些场景必须考虑幂等

常见高风险场景:

  • 创建订单。
  • 支付回调。
  • 退款申请。
  • 优惠券领取。
  • 库存扣减。
  • 表单重复提交。
  • 消息队列消费。
  • 定时任务补偿。
  • 第三方接口回调。

判断标准很简单:如果重复执行会改变钱、库存、权益、状态,就必须设计幂等。

三、唯一请求号

最常见的方式是客户端或服务端生成唯一请求号,也叫 requestIdidempotencyKey 或业务流水号。

请求示例:

1
2
POST /api/orders
Idempotency-Key: 7f7d6f9c-9b2a-4c43-a61f-7e6d2f7d1001

服务端处理流程:

  1. 校验幂等 key 是否存在。
  2. 不存在则创建处理记录。
  3. 执行业务操作。
  4. 保存业务结果。
  5. 重复请求直接返回第一次处理结果。

表结构示例:

1
2
3
4
5
6
7
8
9
10
create table api_idempotency (
id bigint primary key auto_increment,
idem_key varchar(128) not null,
biz_type varchar(64) not null,
status varchar(32) not null,
response_body text,
created_at datetime not null,
updated_at datetime not null,
unique key uk_idem_key_biz_type (idem_key, biz_type)
);

关键点是唯一索引。没有唯一约束,只靠先查再插,在并发下仍然可能重复。

四、利用数据库唯一约束

有些业务可以直接用业务唯一键保证幂等。

例如用户领取优惠券,一个用户对同一张券只能领取一次:

1
2
3
4
5
6
7
create table user_coupon (
id bigint primary key auto_increment,
user_id bigint not null,
coupon_id bigint not null,
created_at datetime not null,
unique key uk_user_coupon (user_id, coupon_id)
);

接口逻辑:

1
2
3
4
5
6
7
public void receiveCoupon(Long userId, Long couponId) {
try {
userCouponMapper.insert(userId, couponId);
} catch (DuplicateKeyException e) {
// 已领取,按成功处理
}
}

这种方式简单可靠,但只适合能明确找到业务唯一键的场景。

五、状态机防重

订单、退款、工单这类业务通常有状态流转。状态机本身也可以防止重复执行。

例如订单支付:

1
2
3
4
5
update orders
set status = 'PAID',
paid_at = now()
where order_no = #{orderNo}
and status = 'WAIT_PAY';

只有待支付状态才能更新为已支付。重复回调时,订单已经是 PAID,更新行数为 0,业务可以查询当前订单后直接返回成功。

这种写法比“先查状态再更新”更抗并发,因为判断和修改在同一条 SQL 中完成。

六、Redis 锁的作用和边界

Redis 分布式锁常用于拦截短时间内的重复请求:

1
SET lock:order:create:{requestId} 1 NX EX 30

拿到锁才执行业务,拿不到锁说明同一个请求正在处理。

但要注意:

  • 锁只能减少并发进入,不能替代数据库唯一约束。
  • 锁过期时间太短,业务没执行完就可能被第二个请求拿到锁。
  • 服务异常退出时,要有最终一致的兜底机制。

可靠做法通常是“Redis 锁 + 数据库唯一约束 + 状态机”组合使用。

七、消息消费幂等

消息队列通常只保证至少投递一次,消费者必须能处理重复消息。

可以建立消费记录表:

1
2
3
4
5
6
7
8
create table mq_consume_record (
id bigint primary key auto_increment,
message_id varchar(128) not null,
consumer_group varchar(64) not null,
status varchar(32) not null,
created_at datetime not null,
unique key uk_msg_group (message_id, consumer_group)
);

消费流程:

  1. 插入消费记录。
  2. 插入成功,执行业务。
  3. 业务成功,更新记录为成功。
  4. 插入失败,说明已经消费过或正在消费。

如果消息本身没有全局唯一 ID,可以使用业务单号、事件类型和版本号组合出唯一键。

八、接口返回策略

幂等接口遇到重复请求时,不建议直接返回“重复提交”错误。更好的体验是返回第一次请求的业务结果。

例如创建订单:

  • 第一次请求创建订单成功,返回订单号。
  • 第二次使用同一个幂等 key 请求,仍然返回同一个订单号。

这样客户端即使因为超时重试,也能拿到确定结果。

如果第一次请求还在处理中,可以返回明确状态:

1
2
3
4
{
"code": "PROCESSING",
"message": "请求处理中,请稍后查询"
}

九、小结

幂等设计不是单个工具能解决的问题,而是一组约束:

  • 请求层用幂等 key 识别重复请求。
  • 数据层用唯一索引兜底。
  • 状态流转用条件更新防止重复变更。
  • 消息消费用消费记录防重。
  • 返回结果尽量保持可重试。

涉及资金、库存、权益和外部回调的接口,都应该在设计阶段把幂等方案写清楚。

其他文章
目录导航 置顶
  1. 1. 一、什么是幂等
  2. 2. 二、哪些场景必须考虑幂等
  3. 3. 三、唯一请求号
  4. 4. 四、利用数据库唯一约束
  5. 5. 五、状态机防重
  6. 6. 六、Redis 锁的作用和边界
  7. 7. 七、消息消费幂等
  8. 8. 八、接口返回策略
  9. 9. 九、小结
请输入关键词进行搜索