少女祈祷中...
Xrq13-Blog

后端接口幂等设计:从请求 ID 到数据库唯一约束的落地方案

什么是接口幂等

接口幂等指同一个业务请求执行一次和执行多次,最终产生的业务结果一致。它不是简单地“防止用户点两次按钮”,而是后端必须面对的可靠性问题:浏览器重试、网关超时重发、消息队列重复投递、移动网络抖动、用户刷新页面,都可能导致同一请求重复到达服务端。

哪些接口需要重点做幂等

  • 创建订单、支付扣款、退款、发放优惠券等资金相关接口。
  • 创建资源、提交表单、报名、预约等只能成功一次的业务。
  • 发送短信、邮件、站内通知等会影响用户体验或产生成本的操作。
  • 消费消息队列时更新业务状态的消费者。

方案一:客户端请求 ID + 服务端唯一约束

最可靠的方式是让客户端或调用方为每次业务操作生成一个唯一请求 ID,例如 requestId。服务端把它和业务主体一起落库,并建立唯一约束。

CREATE TABLE order_request (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL,
  request_id VARCHAR(64) NOT NULL,
  order_id BIGINT,
  status VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT now(),
  updated_at TIMESTAMP NOT NULL DEFAULT now(),
  UNIQUE (user_id, request_id)
);

处理流程如下:

  1. 收到请求后先插入 user_id + request_id
  2. 插入成功,说明是第一次请求,继续执行业务逻辑。
  3. 插入失败,说明是重复请求,查询原请求处理结果并返回。
  4. 业务处理完成后,把结果写回幂等记录。

注意不要只依赖 Redis 锁。Redis 可以降低并发冲突,但数据库唯一约束才是最终兜底,因为业务结果最终也是落在数据库里。

方案二:业务唯一键

有些接口天然有业务唯一键,比如订单号、支付流水号、用户每天签到记录。此时可以直接使用业务唯一键做幂等。

CREATE UNIQUE INDEX uk_daily_checkin
ON user_checkin(user_id, checkin_date);

当用户重复签到时,数据库会拒绝第二次插入。服务端捕获唯一键冲突后,查询当天签到记录并返回“已签到”的结果。这样即使多台应用实例同时处理请求,也不会产生重复数据。

状态机比布尔字段更稳

复杂业务不要只用 success=true。建议使用明确状态,例如:

  • PROCESSING:请求已接收,正在处理。
  • SUCCESS:处理成功,可以返回业务结果。
  • FAILED_RETRYABLE:失败但允许重试。
  • FAILED_FINAL:失败且不可重试。

重复请求到达时,根据状态返回不同结果。如果原请求仍在处理中,可以返回“处理中”或等待短时间后查询结果;如果已经成功,直接返回成功结果;如果失败,则根据失败类型决定是否重新执行。

常见错误

  • 只在前端禁用按钮。前端能改善体验,但无法防止网络重试和恶意请求。
  • 只用查询再插入。高并发下两个请求可能同时查询为空,然后同时插入。
  • 锁粒度过大。用全局锁会影响吞吐,应该按用户、订单或业务键加锁。
  • 没有保存原始响应。重复请求无法返回一致结果,调用方会继续重试。

推荐落地顺序

  1. 先识别所有会“创建、扣减、发放、发送”的接口。
  2. 为每类接口确定幂等键:请求 ID 或业务唯一键。
  3. 在数据库层建立唯一约束。
  4. 增加幂等记录表,保存处理状态和关键返回结果。
  5. 补充压测和并发测试,确认重复请求不会产生重复业务数据。

幂等设计的重点不是写一段防重代码,而是让“重复发生”成为系统设计的一部分。只要接口可能被重试,就应该明确重复到达时的处理策略。

评论