如何有效地防止接口请求重复提交。在开发中,我们常常会遇到这样的问题。在面试场合,也是面试官经常问的问题。但很多新手却经常忽略。因为防重处理需要对应具体业务操作。
下面说的防重操作,相信很多同学都遇到过。如支付功能订单提交业务、表单提交、手机验证码功能。
订单提交为什么需要防重呢?想像一下你在商城购物,你选中商品点击提交订单,如果这时网络延迟没有返回成功提示,你又多点了几次。每点一次都会发送提交订单请求,若是没做防重处理,会出现生成多个订单情况。同样商城系统商品添加功能,用户不小心点了多次表单提交,若防重处理没做,将会添加多个商品。
如何实现防止重复提次请求操作,确保Web应用或API的健壮性和用户体验呢?我们分别从前后端操作上来说一说:
01
前端防重处理
1. 禁用按钮:
在用户点击提交按钮后立即禁用它,直到服务器响应完成。
禁用按钮在注册获取手机验证码场景经常用到。点击获取验证码按钮后,按钮立即变为灰色显示禁用状态,读秒结束后恢复正常。
禁用按钮代码示例
2. 显示加载指示:
提交过程中显示加载动画或提示,防止用户再次点击。
在登录界面,用户点击登录按钮,请求登录接口后,出现加载动画,防止用户重复点击。
加载loading代码示例
3. 限制提交频率:
使用防抖(debounce)和节流(throttle)技术来限制事件触发的频率。
在用户点击提交按钮后,使用防抖或节流的技术延迟发送请求,确保只发送一次请求。防抖和节流是一种常见的前端性能优化技术,可以控制函数的执行频率。
在搜索框输入内容时,可能需要在用户停止输入一段时间后才发送请求,以减少请求的次数。在用户调整浏览器窗口大小时,可能需要在用户停止调整后计算布局或重新渲染页面。在用户连续按键时,例如在输入密码时,可以限制按键事件的处理频率。
02
后端防重处理
1. 唯一ID机制:
生成一个唯一的ID,并在客户端提交时一并发送,服务器验证ID后进行处理。
以订单业务为例,实现的逻辑流程如下:
- 当用户进入订单提交界面的时候,调用后端获取请求唯一ID,并将唯一ID值埋点在页面里面;
- 当用户点击提交按钮时,后端检查这个唯一ID是否用过,如果没有用过,继续后续逻辑;如果用过,就提示重复提交
- 最关键的一步操作,就是把这个唯一ID 存入业务表中,同时设置这个字段为唯一索引类型,从数据库层面做防止重复提交 防止重复提交的大体思路如上,实践代码如下!
1. 给数据库表增加唯一键约束
CREATE TABLE tb_order ( id bigint(20) unsigned NOT NULL, order_no varchar(100) NOT NULL, .... request_id varchar(36) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uniq_request_id (request_id) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2 编写获取请求唯一ID的接口
@RestController @RequestMapping("api") public class CommonController { /** * 获取getRequestId * @return */ @RequestMapping("getRequestId") public ResResult getRequestId(){ String uuid = UUID.randomUUID().toString(); return ResResult.getSuccess(uuid); } }
3 业务提交的时候,检查唯一ID
@RestController @RequestMapping("order") public class OrderController { @Autowired private OrderService orderService; /** * 下单 * @param request * @return */ @PostMapping(value = "order/confirm") public ResResult confirm(@RequestBody OrderConfirmRequest request){ //调用订单下单相关逻辑 if(StringUtils.isEmpty(request.getRequestId())){ return ResResult.getSysError("请求ID不能为空!"); } if(request.getRequestId().length() != 36){ return ResResult.getSysError("请求ID格式错误!"); } //检查当前请求唯一ID,是否已经存在,如果存在,再提交就是重复下单 Order source = orderService.queryByRequestId(request.getRequestId()); if(Objects.nonNull(source)){ return ResResult.getSysError("当前订单已经提交成功,请勿重复提交"); } orderService.confirm(request); return ResResult.getSuccess(); } }
对于下单流量不算高的系统,可以采用这种请求唯一ID+数据表增加唯一索引约束的方式,来防止接口重复提交!虽然简单粗暴,但是十分有效!
2. 分布式锁+全局唯一的ID=Redis+Token:
分布式锁实现解决JVM锁实现单机锁局限问题
具体流程步骤:
- 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段
- 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间
- 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑
- 如果设置失败,则代表已经执行过当前请求,直接返回
Token实现:生成唯一ID
3. 幂等性设计:
幂等设计,即多次执行同一操作的结果与执行一次相同。这通常通过在接口设计时考虑实现。
封装一个公共的方法,以供所有类使用:
import org.apache.commons.collections4.map.LRUMap; /** * 幂等性判断 */ public class IdempotentUtils { // 根据 LRU(Least Recently Used,最近最少使用)算法淘汰数据的 Map 集合,最大容量 100 个 private static LRUMap<String, Integer> reqCache = new LRUMap<>(100); /** * 幂等性判断 * @return */ public static boolean judge(String id, Object lockClass) { synchronized (lockClass) { // 重复请求判断 if (reqCache.containsKey(id)) { // 重复请求 System.out.println("请勿重复提交!!!" + id); return false; } // 非重复请求,存储请求 ID reqCache.put(id, 1); } return true; }
调用代码如下:
import com.example.idempote.util.IdempotentUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/user") @RestController public class UserController4 { @RequestMapping("/add") public String addUser(String id) { // 非空判断(忽略)... // -------------- 幂等性调用(开始) -------------- if (!IdempotentUtils.judge(id, this.getClass())) { return "执行失败"; } // -------------- 幂等性调用(结束) -------------- // 业务代码... System.out.println("添加用户ID:" + id); return "执行成功!"; } }
LRUMap在 Apache 提供的 commons-collections 框架中,可以保存指定数量的固定的数据,并且它会按照 LRU 算法,帮你清除最不常用的数据。
LRUMap
的本质是持有头结点的环回双链表结构,当使用元素时,就将该元素放在双链表 header
的前一个位置,在新增元素时,如果容量满了就会移除 header
的后一个元素。
继续完善代码,可以通过自定义注解,将业务代码写到注解中,需要调用的方法只需要写一行注解就可以防止重复提交了。
03
总结
防止重复提交是确保Web应用或API的健壮性和用户体验的重要措施。为了防止绕过前端限制通过工具重复请求接口,在防重处理时需要前后端配合。
除了我们介绍的几种方式外还有其它方式和封装好的工具,如:react中可以用swr、ahook,vue中用VueRequest。每种方法都有其适用场景,通常需要根据具体的业务需求和系统架构来选择最合适的策略。在设计系统时,应该综合考虑多种方法,以实现最佳的效果。
— end —