面试官问:如何防止重复提交请求,99%的前端能说出来!

如何有效地防止接口请求重复提交。在开发中,我们常常会遇到这样的问题。在面试场合,也是面试官经常问的问题。但很多新手却经常忽略。因为防重处理需要对应具体业务操作。

下面说的防重操作,相信很多同学都遇到过。如支付功能订单提交业务、表单提交、手机验证码功能。

订单提交为什么需要防重呢?想像一下你在商城购物,你选中商品点击提交订单,如果这时网络延迟没有返回成功提示,你又多点了几次。每点一次都会发送提交订单请求,若是没做防重处理,会出现生成多个订单情况。同样商城系统商品添加功能,用户不小心点了多次表单提交,若防重处理没做,将会添加多个商品。

如何实现防止重复提次请求操作,确保Web应用或API的健壮性和用户体验呢?我们分别从前后端操作上来说一说:

01

前端防重处理

1. 禁用按钮:

在用户点击提交按钮后立即禁用它,直到服务器响应完成。

禁用按钮在注册获取手机验证码场景经常用到。点击获取验证码按钮后,按钮立即变为灰色显示禁用状态,读秒结束后恢复正常。

禁用按钮代码示例

2. 显示加载指示:

提交过程中显示加载动画或提示,防止用户再次点击。

在登录界面,用户点击登录按钮,请求登录接口后,出现加载动画,防止用户重复点击。

加载loading代码示例

3. 限制提交频率:

使用防抖(debounce)和节流(throttle)技术来限制事件触发的频率。

在用户点击提交按钮后,使用防抖或节流的技术延迟发送请求,确保只发送一次请求。防抖和节流是一种常见的前端性能优化技术,可以控制函数的执行频率。

在搜索框输入内容时,可能需要在用户停止输入一段时间后才发送请求,以减少请求的次数。在用户调整浏览器窗口大小时,可能需要在用户停止调整后计算布局或重新渲染页面。在用户连续按键时,例如在输入密码时,可以限制按键事件的处理频率。

02

后端防重处理

1. 唯一ID机制:

生成一个唯一的ID,并在客户端提交时一并发送,服务器验证ID后进行处理。

以订单业务为例,实现的逻辑流程如下:

  1. 当用户进入订单提交界面的时候,调用后端获取请求唯一ID,并将唯一ID值埋点在页面里面;

  2. 当用户点击提交按钮时,后端检查这个唯一ID是否用过,如果没有用过,继续后续逻辑;如果用过,就提示重复提交

  3. 最关键的一步操作,就是把这个唯一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锁实现单机锁局限问题

具体流程步骤:

  1. 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段

  2. 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间

  3.  如果设置成功,证明这是第一次请求,则执行后续的业务逻辑

  4. 如果设置失败,则代表已经执行过当前请求,直接返回

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 —

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

耀南.

你的鼓励将是我最最最最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值