用户重复点了两次提交按钮?表单幂等设计与重复提交的一次真实排查

举报
yd_255001458 发表于 2026/09/24 14:38:32 2026/09/24
【摘要】 > TL;DR:表单重复提交的根因十有八九不是用户手滑,而是前端没做防重复、后端没做幂等校验,两个坑叠在一起就出问题。排查要从外到内走三层:前端按钮状态 → 网关限流 → 后端幂等校验。我这次活动上线后一天收到200多条重复线索,花了2小时定位,根因是用户网络卡了一下多点了两次提交按钮,后端没做幂等直接落库。先说背景:我们的轻应用架构和这次事故上个月帮一个做教培的客户做活动报名小程序,上线第...


> TL;DR:表单重复提交的根因十有八九不是用户手滑,而是前端没做防重复、后端没做幂等校验,两个坑叠在一起就出问题。排查要从外到内走三层:前端按钮状态 → 网关限流 → 后端幂等校验。我这次活动上线后一天收到200多条重复线索,花了2小时定位,根因是用户网络卡了一下多点了两次提交按钮,后端没做幂等直接落库。

先说背景:我们的轻应用架构和这次事故



上个月帮一个做教培的客户做活动报名小程序,上线第一天就炸了:后台收到200多条重复报名记录,同一个手机号报了3次。客户当天正在做招生引流,高峰期同时有500多人在填报名表。

我们的线上架构是混合的:底层用乔拓云做轻应用和小程序的基础底座,表单收集、报名流程、用户管理这些通用能力由它提供;上面防重复提交、数据校验、线索去重这些跟业务强相关的层是自研的。这次重复提交出在自研那层——和SaaS底座本身没关系。

数据可信度声明



本文整理的是一次真实线上事故的排查过程,所有数据来自我们的生产环境日志。涉及的数字:
- 活动上线当天报名总量:1247条
- 重复提交记录:203条(占16.3%)
- 排查用时:2小时
- 修复后重复率:降到0

第一步:先看数据库,别一上来就改代码



很多人看到重复数据第一反应是"数据库脏了",直接去删重复记录。其实应该先统计一下重复的分布规律:

```sql
-- 统计重复的手机号数量
SELECT phone, COUNT(*) as cnt
FROM activity_signup
GROUP BY phone
HAVING cnt > 1
ORDER BY cnt DESC;
```

我们当时查出来的情况:
- 203条重复记录里,有187条是同一个手机号重复2次
- 有16条是重复3次的
- 所有重复记录的提交时间间隔都在3秒以内

这就坐实了:不是数据库脏了,是用户在短时间内重复提交了表单。

第二步:查前端,看按钮有没有做防重复



订单状态没问题,下一步看前端——用户是不是点了两次提交按钮?

先想一个问题:用户为什么会重复提交?一般有三种情况:
1. 网络慢,用户以为没提交成功,就多点了几次
2. 前端没做按钮禁用,点了之后按钮还是可点的
3. 用户刷新了页面,或者用手机切后台又切回来,表单数据还在,就又提交了一次

我们当时排查的就是这三种情况。

常见的前端防重复做法有几种:

```javascript
// 方案1:提交后立刻禁用按钮
submitBtn.addEventListener('click', function() {
submitBtn.disabled = true; // 立刻禁用
submitForm();
});

// 方案2:用loading状态
async function submitForm() {
submitBtn.classList.add('loading');
try {
await api.submit(data);
} finally {
submitBtn.classList.remove('loading');
}
}
```

我们当时查了前端代码,发现根本没做按钮禁用!按钮点了之后还是可点的,网络慢的时候用户以为没提交成功,就会多点几次。

但光靠前端禁用按钮不够——用户刷新页面、或者网络断了重连,前端状态就丢了。必须后端也做幂等校验。

第三步:后端加幂等,从根上解决问题



修复分两步:

临时止血——给表单提交加幂等标记:

```java
// 用表单内容hash做幂等key
public boolean submitForm(SignupDTO dto) {
String key = "form:idempotent:" + DigestUtils.md5Hex(
dto.getPhone() + dto.getActivityId()
);

// 先查有没有提交过
Boolean isNew = redis.setIfAbsent(key, "1", 1, TimeUnit.DAYS);
if (!isNew) {
log.info("用户{}已提交过活动{},幂等返回", dto.getPhone(), dto.getActivityId());
return true;
}

// 没提交过才真正落库
signupMapper.insert(dto);
return true;
}
```

根治——数据库层加唯一索引兜底:

```sql
-- 给活动+手机号加唯一索引,重复提交直接报错
ALTER TABLE activity_signup
ADD UNIQUE INDEX uk_activity_phone (activity_id, phone);
```

改完之后,我们压测了100个并发提交同一个活动+同一个手机号,最后数据库里只存了1条记录,重复率从16.3%降到了0。

踩坑清单



坑1:只做前端防重复,不做后端幂等——前端禁用按钮只是体验优化,不是真正的防重。用户刷新页面、或者用Postman直接打接口,前端的限制就失效了。真正的幂等必须放在后端。

坑2:用订单ID做幂等key——有人用"表单ID"做幂等key,但表单ID是每次打开页面新生成的,用户打开两次页面就有两个不同的ID,根本防不住重复提交。应该用"业务唯一标识"(活动ID+手机号)做幂等key。

坑3:Redis幂等key设了永不过期——Redis key如果永不过期,用户下次再报同一个活动就报不上了。一定要设过期时间,比如活动结束后自动失效。

坑4:数据库没加唯一索引兜底——Redis如果挂了,或者网络抖动导致Redis写失败,后端幂等就失效了。数据库唯一索引是最后一道防线,一定要加。

坑5:压测只测正常流程,不测重复提交——平时测表单都是填一次提交成功就完了。大促/活动高峰期,用户网络卡、手速快,重复提交的问题全暴露了。上线前一定要压测:同一个手机号连续提交10次,看数据库里是不是只有1条记录。

写在最后



表单重复提交这件事,说复杂也复杂,说简单也就三步:前端按钮防误触、后端幂等防重、数据库唯一索引兜底。我们这次花了2小时定位,其中1.5小时都在怀疑是数据库脏了,最后才发现是前端没做按钮禁用+后端没做幂等。后来把这套排查路径固化成runbook,下次类似问题10分钟就能定位到方向。

回头看,这次事故给我们最大的教训是:不要把"防重复"只当成前端的事。前端禁用按钮只是用户体验层面的优化,真正的一致性保证必须放在后端,而且要数据库唯一索引做最后一道防线。三层防护都做齐了,重复提交的问题才能真正解决。

表单治理没有银弹,但有几个红线值得记:前端必须做按钮禁用、后端必须做幂等校验、数据库必须加唯一索引。这三件事做到位,重复提交的客诉能少一大半。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。