天天看點

軟體開發中防止資料重複送出的幾種方式

作者:風起曉城

在日常開發過程中,通常會遇到前端或接口的重複送出問題。本文介紹了什麼是重複送出問題,并提供了幾種防止重複送出的方式。

什麼是重複送出問題?重複送出問題是指當使用者在短時間内多次送出同一個請求時,伺服器可能會處理多次請求,導緻結果出現異常。例如使用者在送出表單之後,突然網絡卡頓,然後迅速重試送出,此時伺服器可能會認為該請求是多次請求,導緻資料出現問題。為了避免這種情況的發生,需要對請求進行防重複送出處理。以下是幾種解決方案:

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 在緩存中已經存在,則說明此請求已經被處理過,傳回重複送出結果。

以上幾種方法都可以防止資料重複送出,需要根據具體情況選擇合适的方法。可能寫的有點亂,大家了解一下就好。

除此之外,還可以使用定時器、攔截器等技術來解決前端重複送出的問題。

繼續閱讀