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

简介: 如何防止接口重复提交是一个常见的系统设计问题,主要目的是确保关键操作的原子性和一致性。以下是简化的摘要:这些方法可以单独或组合使用,取决于系统规模和业务需求。例如,对于低流量系统,简单的请求唯一ID和数据库唯一索引可能足够;而对于高并发场景,可能需要结合前端禁用和后端分布式锁来提高可靠性。幂等性设计是确保接口安全的一种通用策略,适用于各种场景。

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

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

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

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

01

前端防重处理

1. 禁用按钮:

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

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

禁用按钮代码示例

image.gif

2. 显示加载指示:

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

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

加载loading代码示例

image.gif

3. 限制提交频率:

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

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

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

image.gif 02

后端防重处理

1. 唯一ID机制:

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

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

  1. 当用户进入订单提交界面的时候,调用后端获取请求唯一ID,并将唯一ID值埋点在页面里面;
  2. 当用户点击提交按钮时,后端检查这个唯一ID是否用过,如果没有用过,继续后续逻辑;如果用过,就提示重复提交
  3. 最关键的一步操作,就是把这个唯一ID 存入业务表中,同时设置这个字段为唯一索引类型,从数据库层面做防止重复提交 image.gif 防止重复提交的大体思路如上,实践代码如下!

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;

image.gif

2 编写获取请求唯一ID的接口

@RestController
@RequestMapping("api")
public class CommonController {
    /**
     * 获取getRequestId
     * @return
     */
    @RequestMapping("getRequestId")
    public ResResult getRequestId(){
        String uuid = UUID.randomUUID().toString();
        return ResResult.getSuccess(uuid);
    }
}

image.gif

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();
    }
}

image.gif

对于下单流量不算高的系统,可以采用这种请求唯一ID+数据表增加唯一索引约束的方式,来防止接口重复提交!虽然简单粗暴,但是十分有效!

image.gif

2. 分布式锁+全局唯一的ID=Redis+Token:

分布式锁实现解决JVM锁实现单机锁局限问题

具体流程步骤:

  1. 客户端先请求服务端,会拿到一个能代表这次请求业务的唯一字段
  2. 将该字段以 SETNX 的方式存入 redis 中,并根据业务设置相应的超时时间
  3. 如果设置成功,证明这是第一次请求,则执行后续的业务逻辑
  4. 如果设置失败,则代表已经执行过当前请求,直接返回 image.gif

Token实现:生成唯一ID

image.gif

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;
    }

image.gif

调用代码如下:

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 "执行成功!";
    }
}

image.gif

LRUMap在 Apache 提供的 commons-collections 框架中,可以保存指定数量的固定的数据,并且它会按照 LRU 算法,帮你清除最不常用的数据。

LRUMap 的本质是持有头结点的环回双链表结构,当使用元素时,就将该元素放在双链表 header 的前一个位置,在新增元素时,如果容量满了就会移除 header 的后一个元素

继续完善代码,可以通过自定义注解,将业务代码写到注解中,需要调用的方法只需要写一行注解就可以防止重复提交了。

03

总结

防止重复提交是确保Web应用或API的健壮性和用户体验的重要措施。为了防止绕过前端限制通过工具重复请求接口,在防重处理时需要前后端配合。

除了我们介绍的几种方式外还有其它方式和封装好的工具,如:react中可以用swr、ahook,vue中用VueRequest。每种方法都有其适用场景,通常需要根据具体的业务需求和系统架构来选择最合适的策略。在设计系统时,应该综合考虑多种方法,以实现最佳的效果。

— end —

相关文章
|
3月前
|
缓存 前端开发 中间件
[go 面试] 前端请求到后端API的中间件流程解析
[go 面试] 前端请求到后端API的中间件流程解析
|
21天前
|
前端开发 JavaScript
回顾前端页面发送ajax请求方式
回顾前端页面发送ajax请求方式
36 18
|
11天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
44 1
|
23天前
|
前端开发 JavaScript API
前端Get请求能在body上传参吗
【10月更文挑战第11天】 在浏览器环境中,GET请求的body参数会被忽略,这是因为浏览器中的XHR和fetch实现限制了这一行为。而在Node.js服务端环境中,GET请求可以在body中传递参数,因为服务端请求库没有这样的限制。实际上,GET请求不带body是HTTP标准的一部分,但在某些场景下,为了遵循RESTful规范,可以考虑通过服务端转发或BFF模式来实现复杂的参数传递。
|
28天前
|
存储 缓存 监控
|
28天前
|
移动开发 前端开发 HTML5
SharedWorker 优化前端轮询请求
【10月更文挑战第6天】
18 1
|
3月前
|
JavaScript 前端开发 Java
面试官:假如有几十个请求,如何去控制并发?
面试官:假如有几十个请求,如何去控制并发?
|
3月前
|
存储 前端开发 Serverless
中后台前端开发问题之Django项目中接收和处理用户的抽奖请求如何解决
中后台前端开发问题之Django项目中接收和处理用户的抽奖请求如何解决
18 0
|
3月前
|
前端开发 应用服务中间件 API
"揭秘!面试官必问:你是如何巧妙绕过跨域难题的?前端代理VS服务器端CORS,哪个才是你的秘密武器?"
【8月更文挑战第21天】在软件开发中,尤其前后端分离架构下,跨域资源共享(CORS)是常见的挑战。主要解决方案有两种:一是服务器端配置CORS策略,通过设置响应头控制跨域访问权限,无需改动前端代码,增强安全性;二是前端代理转发,如使用Nginx或Webpack DevServer在开发环境中转发请求绕过同源策略,简化开发流程但不适用于生产环境。生产环境下应采用服务器端CORS策略以确保安全稳定。
47 0
|
4月前
|
存储 前端开发 安全
前端轮询问题之在setTimeout版轮询中,如何避免旧请求的响应继续触发定时
前端轮询问题之在setTimeout版轮询中,如何避免旧请求的响应继续触发定时