天天看点

软件开发中防止数据重复提交的几种方式

作者:风起晓城

在日常开发过程中,通常会遇到前端或接口的重复提交问题。本文介绍了什么是重复提交问题,并提供了几种防止重复提交的方式。

什么是重复提交问题?重复提交问题是指当用户在短时间内多次提交同一个请求时,服务器可能会处理多次请求,导致结果出现异常。例如用户在提交表单之后,突然网络卡顿,然后迅速重试提交,此时服务器可能会认为该请求是多次请求,导致数据出现问题。为了避免这种情况的发生,需要对请求进行防重复提交处理。以下是几种解决方案:

1. 禁用按钮

这种方法相对而言比较简单,只需要在表单提交后禁用提交按钮即可,这样就可以避免用户多次点击提交按钮。

let button = document.getElementById('submitButton');
button.addEventListener('click', function() {
  // 禁用提交按钮,防止重复提交
  button.disabled = true;
  // 发送表单请求
  fetch('/submitForm', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      // request data
    })
  }).then(response => {
    if (response.ok) {
      // 成功提交表单,可以进行其他操作,如页面跳转
    } else {
      // 提交表单失败,可以进行其他操作,如提示错误信息
    }
    // 启用提交按钮
    button.disabled = false;
  }).catch(error => {
    console.error('请求错误:', error);
    // 启用提交按钮
    button.disabled = false;
  });
});           

在上面的代码中,当用户点击提交按钮时,按钮将被禁用,防止重复提交表单。当表单请求完成后,按钮将被重新启用。如果请求成功,则可以进行其他操作,如页面跳转等;如果请求失败,则可以提示错误信息等。

2. 前端错开请求时间

这种方法的思路是在每次请求之间添加一定的时间间隔,可以使用 JavaScript 的 setTimeout() 方法实现:

var isSubmitting = false;
 function submitForm() {
  if (!isSubmitting) {
    isSubmitting = true;
    setTimeout(function() {
      // 提交表单
      isSubmitting = false;
    }, 1000);
  }
}           

也可以使用以下方式:

<form onsubmit="return checkForm(this);">
  <input type="hidden" name="timestamp" value="<%=System.currentTimeMillis()%>">
  ...
  <button type="submit">提交</button>
</form>
 <script>
function checkForm(form) {
  var timestamp = form.timestamp.value;
  var current = new Date().getTime();
  if (current - timestamp < 5000) { // 限制提交时间为 5 秒
    alert("请勿重复提交");
    return false;
  }
  return true;
}
</script>           

3. 限制请求次数

这种方法的思路是限制每个用户在一定时间内可以提交的次数,可以在后端记录用户每次请求的时间,并在较短的时间内拒绝过多的请求。

private static Map<String, List<Long>> requestHistory = new HashMap<>();
private static final long REQUEST_INTERVAL = 1000; // 1s 内仅能请求一次
private static final int REQUEST_LIMIT = 10; // 10 秒内最多请求 10 次

@PostMapping("/submitForm")
public String submitForm(@RequestParam String name, HttpSession session) {
	String sessionId = session.getId();
  List<Long> history = requestHistory.getOrDefault(sessionId, new ArrayList<>());
  long currentTimeMillis = System.currentTimeMillis();
  int recentRequestsCount =
      (int) history.stream().filter(time -> currentTimeMillis - time < REQUEST_INTERVAL).count();
  if (recentRequestsCount >= REQUEST_LIMIT) {
    // 达到请求限制
    return "rate_limit";
  } else {
    // 处理请求
    history.add(currentTimeMillis);
    requestHistory.put(sessionId, history);
    return "success";
  }
}           

4. 接口幂等性

接口幂等性是指对于一个接口,无论调用多少次,其结果都是一样的,不会因为调用的次数的不同而产生不同的结果。

举个例子,比如说银行转账接口,如果你调用一次将100元转账到某个账户,那么第二次调用就会返回错误,因为转账已经完成了,再次调用就没有任何意义了。这就是幂等性的应用,保证接口在调用时的结果是一致的。

下面是一个使用 HTTP POST 请求创建资源并实现接口幂等性的代码示例。

import requests
 def create_user(name: str, email: str) -> None:
    data = {
        "name": name,
        "email": email,
    }
    headers = {
        "Content-Type": "application/json",
        "Idempotency-Key": "unique-id-for-each-request",
    }
    url = "http://example.com/users/"
    response = requests.post(url, headers=headers, json=data)
    if response.status_code in (200, 201):
        print("User created successfully.")
    else:
        print("Failed to create user.")           

在上面的示例中, Idempotency-Key 是一个唯一标识符,用于做接口幂等性的实现。客户端发送请求时,需要在请求头中加入这个标识符。服务端会将这个标识符与接口的请求参数一起记录,如果客户端重复发送相同的请求,服务端会判断 Idempotency-Key 是否已经存在,如果已经存在,则直接返回之前的结果,否则执行请求并将结果记录下来,保证接口的结果始终是一致的,不会产生重复数据或错误的结果。

5.通过缓存和唯一标识

类似于token校验机制。示例如下:

@PostMapping("/submitForm")
public Result submitForm(@RequestBody Form form, HttpServletRequest request) {
  // 获取请求的唯一标识符
  String uuid = request.getHeader("uuid");
  // 判断是否已经处理过此请求
  if (redisTemplate.opsForValue().setIfAbsent(uuid, "lock")) {
    // 设置此请求的过期时间为5秒
    redisTemplate.expire(uuid, 5, TimeUnit.SECONDS);
    // 处理表单请求
    return formService.submit(form);
  } else {
    // 请求已经处理过,返回重复提交结果
    return Result.error("您已经提交过该请求,请不要重复提交!");
  }
}           

在上面的代码中,当客户端发送表单请求时,首先从请求头中获取唯一标识符 uuid。如果当前请求的 uuid 在缓存中不存在,则将这个 uuid 存储到缓存中,并设置过期时间为 5 秒。然后处理表单请求并返回处理结果。如果当前请求的 uuid 在缓存中已经存在,则说明此请求已经被处理过,返回重复提交结果。

以上几种方法都可以防止数据重复提交,需要根据具体情况选择合适的方法。可能写的有点乱,大家了解一下就好。

除此之外,还可以使用定时器、拦截器等技术来解决前端重复提交的问题。

继续阅读