登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 批量接口如何设计幂等键:重复请求的响应语义与唯一约束

来源:17golang原创

时间:2026-08-27 07:56:54 390浏览 收藏

批量创建订单时,客户端超时并不等于服务端没有写入。最麻烦的情况是:第一次请求已经提交,客户端没拿到响应,随后带着同一批数据重试,接口却又插入了一批新订单。可靠的做法是让请求携带稳定的幂等键,由数据库保存请求结果,再用业务唯一约束兜住单条订单的重复。

要点速览
  • 幂等键代表一次批量请求,不能用每次重试都会变化的随机值。
  • 请求记录负责返回同一份结果,订单唯一键负责阻止业务数据重复。
  • 首次请求、处理中、已完成和参数冲突要返回不同的状态语义。
  • 唯一约束冲突必须进入事务内的可判断分支,不能只依赖应用层先查再插。

先把“重复请求”与“重复订单”分开

幂等设计经常只加一个 idempotency_key 字段就结束了,但它实际解决的是请求重复,不一定能阻止订单重复。比如同一个请求里有 20 个商品行,客户端重试时生成了新的请求键,接口层看不到重复;如果订单表没有业务唯一约束,数据库仍然会接受第二批数据。

本文用 POST /api/order-batches 举例。调用方为一次用户点击生成 idem_20260827_7f3a,重试时原样携带它;服务端把这个键与用户、请求摘要一起保存。订单行则使用“用户 + 外部订单号”作为另一层唯一身份。

请求表和订单表各自守住一条边界

请求表可以这样建,重点不是字段数量,而是唯一键的作用域必须明确:

CREATE TABLE order_request_record (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  idempotency_key VARCHAR(80) NOT NULL,
  request_digest CHAR(64) NOT NULL,
  status TINYINT NOT NULL COMMENT '1 processing, 2 succeeded, 3 failed',
  response_body JSON NULL,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL,
  UNIQUE KEY uk_user_idem (user_id, idempotency_key)
);

CREATE TABLE order_item (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  external_order_no VARCHAR(64) NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  created_at DATETIME NOT NULL,
  UNIQUE KEY uk_user_order (user_id, external_order_no)
);

uk_user_idem 防止同一用户把同一请求登记两次,uk_user_order 防止业务订单号重复。不要把幂等键做成全局唯一,除非调用方能保证不同租户之间也绝不会复用它;多租户接口更适合把租户或用户放进唯一键。

MySQL 批量接口中相同幂等键从首次登记到重复响应的请求流转示意图

参数语义决定重试能不能安全复用

同一个幂等键再次到达时,服务端不能只看键是否存在,还要比较 request_digest。请求内容相同,说明它是网络重试;请求内容不同,说明调用方错误地复用了键,这时应返回参数冲突,而不是悄悄覆盖第一次请求。

状态判断建议响应
不存在插入请求记录并进入处理201 或处理中结果
processing摘要相同,原请求仍未结束409,提示稍后查询
succeeded摘要相同且已有结果复用第一次响应
任意状态摘要不同409,幂等键参数冲突

这里的响应体要稳定。第一次成功返回订单号列表,重试也返回同一列表;不要因为第二次走了“查询旧记录”分支,就改成另一套字段名。

事务里先登记请求,再写入订单

实际实现可以把“登记请求—写订单—保存结果”放进一个事务。登记时遇到 uk_user_idem 冲突,就读取旧记录并比较摘要;只有确认是同一请求,才进入复用或处理中分支。应用层的先查询再插入不是充分保护,因为两个并发事务可能同时查到不存在。

START TRANSACTION;

-- 伪代码:先按 user_id + idempotency_key 锁定请求记录
-- 新记录:写入 processing
-- 旧记录:比较 request_digest

INSERT INTO order_item (user_id, external_order_no, amount, created_at)
VALUES (42, 'B20260827-001', 19.90, NOW());

-- 批量行全部成功后,更新请求记录和 response_body
COMMIT;

订单行的唯一键冲突也不能直接吞掉。它可能表示重试,也可能表示外部订单号被另一笔业务占用。对冲突行查询已有记录并确认归属、金额等关键字段;信息不一致时回滚整批,避免返回一个看似成功的混合结果。

MySQL 订单表用用户与外部订单号唯一约束阻止重复写入的前后对照图

错误模型要让调用方知道下一步做什么

批量接口最怕所有异常都返回 500。建议至少区分三类:幂等键参数冲突是调用方修正请求的问题;请求仍在处理中是等待或查询的问题;业务唯一键冲突则要告诉调用方哪些订单行已存在、哪些行需要检查。

  • 409 IDEMPOTENCY_KEY_REUSED:键相同但请求摘要不同。
  • 409 REQUEST_IN_PROGRESS:同一请求正在处理,附带查询地址或重试建议。
  • 409 ORDER_ALREADY_EXISTS:外部订单号已归属于不同业务数据。

错误码只是协议的一部分,日志里还应记录用户、幂等键的脱敏值、摘要、事务结果和冲突行号。不要把完整请求体或可能包含个人信息的订单内容直接写进日志。

上线前用并发测试核对四个结果

验收不要只发一次成功请求。至少准备相同键相同参数、相同键不同参数、不同键相同业务订单号和两个并发请求四组样例,检查订单表行数、请求表状态和两次响应体。

  1. 相同键、相同摘要重试:只能有一批订单,响应中的订单号一致。
  2. 相同键、不同摘要:没有新订单,返回参数冲突。
  3. 不同键、相同外部订单号:唯一约束生效,冲突归因清楚。
  4. 并发提交同一键:一个事务创建记录,另一个只能复用或得到处理中状态。

测试结束后用 SELECT user_id, idempotency_key, COUNT(*) ... GROUP BY ... HAVING COUNT(*) > 1 检查请求记录,再用业务唯一键对应的查询确认没有重复订单。这个结果比单看 HTTP 200 更有价值。

常见问题

幂等键应该由前端生成还是后端生成?

最好由发起一次业务动作的调用方生成并在重试时复用,后端负责校验格式、长度和作用域。后端每次都重新生成会让重试失去关联。

处理中的请求一定要返回 409 吗?

不一定,但必须有明确语义。同步接口可以返回 409 并提供查询方式,异步接口也可以返回任务状态;关键是不要把尚未完成伪装成成功。

有了幂等表还需要订单唯一键吗?

需要。幂等表保护请求边界,订单唯一键保护业务事实;导入、补偿或其他入口绕过请求表时,业务约束仍然能挡住重复数据。

小结

批量接口的幂等不是一个字段的装饰,而是请求记录、参数摘要、事务边界和业务唯一约束共同形成的协议。先定义重复请求应该得到什么响应,再让 MySQL 唯一键承担最后一道事实校验,重试和并发才会变成可验证的分支,而不是靠运气。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>