“避免重复提交”,总结下大致有以下两个方向:
- 真正做到不重复提交
- 即使重复提交,也能够使最终的结果和单次提交的结果保持一致。也就是所谓的“幂等性”。
幂等性:
所谓幂等性,就是一个接口,多次发起同一个请求,该接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多统计 1,这就是幂等性。
产生原因(基于浏览器分析):
- 多次点击页面提交按钮
- 点击刷新页面
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单
- 使用浏览器历史记录重复提交表单
- 浏览器重发http请求
- nginx重发...
场景举例:
例如,某分布式交易服务提供的付款接口,由于前端未知操作(或者由于网络超时导致的超时重试)导致对一笔订单发起了两次支付请求,且分别请求到不同服务器上,结果造成一笔订单扣款两回。
问题解析 → 保证幂等性:
- 对于每个交易请求都对应一个唯一标识符。比如:订单业务中的“唯一订单号”(对订单号建立mysql的唯一索引),每个订单号最多只能支付一次。
- 每次处理完交易请求后,都有一个记录标识这个请求状态为“已处理”。常见方案:数据库中插入一条订单记录,或者增加“交易状态”字段来表明是否交易成功。
- 每次收到交易请求,都先查询并判断之前是否已支付过。如果已支付,那么新插入的订单记录就会因为“mysql唯一键约束”而导致插入订单失败。若插入失败则表示可能先前存在记录,也就不会再重复扣款了。
如何避免重复提交:
- 利用MySQL的唯一索引,若重复insert订单数据会报错。update订单时,使用乐观锁(version比较法)
- 使用悲观锁(如mysql行锁 for update),对资源即订单记录上锁。但此方式并发性能较差,仅适用于请求量不大的单体应用。
- 通过redis设置标识位,用户在支付一个订单前,先插入一条交易记录到mysql。若插入成功,那么系统可以同时写一个交易成功标识的key到redis中(set order_id payed)。倘若下次来了重复请求,先尝试在缓存中查询key,若key已存在说明支付过了,这样就可以避免重复支付。
前端侧:
- 前端js提交禁止按钮:可以用一些js组件
- 重定向页面:当用户提交表单后,转到提交成功信息的页面,以避免用户F5导致的重复提交
- ...
总之,先查询订单记录是否存在,然后再判断是否执行 insert 操作!