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

简介: 用户重复点两次提交按钮,后台收到几十条重复线索?根因十有八九是前端没做防重+后端没做幂等。本文从前端按钮状态、网关限流、后端幂等校验三层,完整复盘一次活动报名重复提交事故的排查与修复,附可复用的幂等代码和5条踩坑经验。

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

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

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

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

数据可信度声明

本文整理的是一次真实线上事故的排查过程,所有数据来自我们的生产环境日志。涉及的数字:

  • 活动上线当天报名总量:1247条
  • 重复提交记录:203条(占16.3%)
  • 排查用时:2小时
  • 修复后重复率:降到0

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

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

-- 统计重复的手机号数量
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. 用户刷新了页面,或者用手机切后台又切回来,表单数据还在,就又提交了一次

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

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

// 方案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');
  }
}

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

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

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

修复分两步:

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

// 用表单内容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;
}

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

-- 给活动+手机号加唯一索引,重复提交直接报错
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分钟就能定位到方向。

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

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

相关文章
|
19小时前
|
NoSQL 小程序 测试技术
门店预约高峰期约满?容量预估与排队机制的一次压测实践
门店预约高峰期约满、同一个时段被约两次?根因十有八九是容量预估不准+并发预约没加锁。本文从预约记录分布、并发请求日志、数据库行锁三层,完整复盘一次门店预约超卖事故的压测与修复,附可复用的行锁代码和5条踩坑经验。
|
4月前
|
人工智能 新制造 图形学
西门子 UG NX 2506 智能三维设计与智能制造新标杆下载安装教程 附安装包
UG NX 2506是西门子推出的最新一代CAD/CAE/CAM一体化工业设计平台,集成智能三维建模、同步建模、拓扑优化、多学科仿真(结构/流体/机电)、数控编程与数字孪生能力,广泛应用于航空航天、汽车、模具及医疗器械等领域。
797 0
|
2天前
|
缓存 JSON API
DeepSeek‑V4完整技术解析:Flash与Pro双版性能对比、计费缓存机制与API实战调用全教程
DeepSeek‑V4通过Flash、Pro双版本差异化产品策略,把百万Token超长上下文能力普及到不同预算层级的开发者。Flash版本主打低单价、低延迟、高并发,适合绝大多数轻量化高频业务;Pro版本凭借超大MoE激活参数,承接复杂推理、多步骤Agent、深度长文档分析等专业场景。统一的分布式KV缓存机制,极大降低长文本业务的输入调用开销。
88 0
|
1天前
|
弹性计算 人工智能 数据库
企业上云算力避坑指南:阿里云 ECS、GPU 云服务器新版配置价格与 AI 场景匹配教程
在数字化业务、大模型推理训练、企业业务系统搭建的场景之下,ECS弹性计算实例是底层算力底座,区别于轻量化套餐化的轻量应用服务器,ECS具备完整的弹性计算能力,支持自定义网络、弹性网卡、多块云盘挂载、灵活升降配,同时衍生出GPU加速实例,专门面向AI推理、模型微调、图像渲染、高性能计算场景。新版ECS实例基于CIPU架构迭代,细分出通用型、内存增强型、存储优化型、GPU虚拟化、GPU物理计算型多个规格族,覆盖个人开发、中小企业业务、大型AI项目等不同层级需求。
32 0
|
1天前
|
存储 人工智能 安全
阿里云服务器经济型e实例:99元/年起续费同价,企业级SLA保障,从技术特性到最优选型全解析
本文聚焦预算有限的个人开发者、学生及小微企业的上云痛点,全面解析阿里云经济型e实例这款入门级共享云服务器。内容覆盖其搭载英特尔可扩展处理器的技术架构、多档规格参数,拆解建站、开发测试等适配场景,详解99元/年起且续费同价的特惠政策,同时给出针对性选型、成本优化的实用选购策略,是入门用户低成本上云的实用参考指南。
|
2月前
|
存储 人工智能 Shell
用Hook让Agent感知时间
本文采用 Hook 机制为 Agent 注入时间感知能力,实现方式为 UserPromptSubmit 事件 Hook + 全局规则声明,Shell 脚本共 23 行。Agent 的时间感知问题指的是:模型权重是静态的,训练数据有截止日期,推理时没有内置时钟,它的时间停在上一次预训练那天。一周前的会话今天接着聊,对用户来说过了一周,对 AI 来说时间没有流动。目前不少 Agent 框架通过系统提示词注入时间,本文介绍一种基于 Hook 的通用实现,详细介绍见全文
446 0
用Hook让Agent感知时间
|
安全 数据安全/隐私保护
|
机器学习/深度学习 并行计算 数据可视化
深度学习经典网络模型汇总——LeNet、AlexNet、ZFNet、VGGNet、GoogleNet、ResNet【对卷积池化后特征图变化有详细讲解】
深度学习经典网络模型汇总——LeNet、AlexNet、ZFNet、VGGNet、GoogleNet、ResNet【对卷积池化后特征图变化有详细讲解】
1546 0
深度学习经典网络模型汇总——LeNet、AlexNet、ZFNet、VGGNet、GoogleNet、ResNet【对卷积池化后特征图变化有详细讲解】
|
芯片
独家esxi7镜像_集成网卡m.2等驱动
独家esxi7镜像_集成网卡m.2等驱动
1554 0
Qt无边框窗口拖拽和阴影
无边框窗口的实现
746 0
Qt无边框窗口拖拽和阴影