真正起作用的是后端幂等识别,前端禁用按钮仅为辅助;需前后端协同:前端生成唯一requestid随表单提交,后端用redis原子操作校验并标记已处理,配合状态机或防重表确保同一请求多次执行结果一致。

表单提交后如何防止重复点击触发多次请求
按钮禁用只是前端障眼法,真正起作用的是后端对请求的幂等识别。用户双击、F5刷新、浏览器后退重提,这些行为产生的请求必须被拦截或合并处理,否则数据库会多出几条订单。
关键不是“不让点”,而是“点了也白点”。常见错误是只在前端加 disabled 属性,但用户绕过 JS(比如直接 POST 请求)或页面重载后状态重置,照样能发重复请求。
- 前端需配合生成一次性
requestId,随表单一起提交(如隐藏域或请求头) - 后端收到后立即校验该
requestId是否已存在处理记录,存在则直接返回成功响应(不执行业务逻辑) - 校验和标记写入必须原子化——推荐用 Redis 的
SETNX+ 过期时间,或数据库防重表的INSERT ... ON CONFLICT DO NOTHING - 不要用 session 存 token:分布式部署下 session 不共享,且超时清理不可控
为什么状态机比乐观锁更适合表单提交场景
状态机不是炫技,它解决的是“同一操作多次触发但只生效一次”的本质问题。比如订单创建后,状态从 INIT → CREATED,后续再收到相同请求,只要当前状态已是 CREATED 或更高,就拒绝更新。
对比乐观锁:UPDATE order SET status = 'CREATED' WHERE id = 123 AND version = 1 虽然能防并发覆盖,但无法阻止“状态已为 CREATED 后还反复执行 UPDATE”——它只管版本,不管语义。
- 状态机要求 SQL 带状态条件:
UPDATE order SET status = 'CREATED', updated_at = NOW() WHERE id = 123 AND status = 'INIT' - 执行后检查
affected_rows:为 0 表示状态已变更,本次请求无副作用,可安全返回成功 - 状态流转必须单向,禁止
CREATED → INIT或PAYED → CREATED等回滚路径 - 数据库字段建议用枚举类型(如 PostgreSQL 的
ENUM),避免字符串拼写错误导致状态跳变
防重表设计中容易忽略的三个细节
防重表(idempotency table)是落地最稳的方案之一,但很多人建完表就以为万事大吉,结果上线后还是出现重复数据。
核心问题不在“有没有表”,而在“怎么查、怎么插、怎么清”。INSERT INTO idempotent_log (request_id, biz_type, created_at) VALUES ('abc123', 'create_order', NOW()) 这条语句看似没问题,但并发下仍可能失败。
- 必须给
request_id加唯一索引,否则高并发时两条 INSERT 都通过,造成脏数据 - 插入失败不能直接抛异常——要捕获
duplicate key violation错误,并主动查询该request_id对应的业务结果是否已存在 - 防重记录不能永久保留:按业务生命周期设置 TTL(如订单类保留 90 天),否则表膨胀拖慢查询
异步提交下如何让前端感知真实状态而非“假成功”
很多项目用 AJAX 提交后直接弹“提交成功”,但后端其实还在走 MQ 异步落库、调第三方支付接口。这时用户刷新页面,发现订单没出来,信任就崩了。
真正的幂等不只是防重复,还要让整个链路状态可追溯。前端不该只听后端一句“OK”,而应拿到一个可轮询的 traceId 或状态 URL。
- 后端返回结构必须含
status(如PENDING/SUCCESS/FAILED)和result_url(如/api/order/status?request_id=abc123) - 前端轮询间隔要递增(如 1s→2s→4s),避免压垮接口;超过阈值后降级为手动刷新提示
- 如果业务允许,优先用 Server-Sent Events(SSE)替代轮询,服务端状态变更时主动推送
- 注意:SSE 在 iOS Safari 旧版本有兼容问题,需 fallback 到轮询
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











