天天看點

防止重複送出政策

前言

業務開發中,常常面臨防止重複送出問題,當該情況發生往往會帶來驗證後果。前端操作抖動、快速操作、網絡延遲以及背景處理慢等等都會增加後端重複處理的機率;

方案

  1. 前端送出之後,屏蔽送出按鈕。該方案雖然可以啟動一定作用,對于模拟接口請求就沒有用。
  2. 送出表單跳轉其他頁面。該方案在極緻情況下也是不安全的。
  3. 利用Session防止表單重複送出。用戶端請求一個頁面,服務端生成一個token(令牌)存在session中,并且把token放在頁面中一起發給用戶端,用戶端送出請求時,服務端先驗證token合法性,如果通過從session删除該值,繼續處理業務;如果驗證不通過傳回錯誤,理論上接口并發請求還是可能會出現重複送出。
  4. 利用資料庫字段設定唯一索引。可以有效避免表單重複送出,但增大伺服器和資料庫開銷。
  5. 利用Redis加鎖和删除鎖。Redis中設定一個單據鎖,利用set操作的原子性,隻有擷取該鎖(未送出表單)才能送出表單,未擷取(已送出表單)則直接傳回用戶端;這裡單據鎖的值過期時間一定要大于中間邏輯執行時間,不然就會出現重複送出。
    <?php
    // 加載redis元件
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    // 防止重複變量名稱
    $redis_name = 'lock';
    
    // 模拟使用者ID
    $uid = 1;
    $ok = $redis->rawCommand('set', $redis_name . '_' . $uid, $uid, 'EX', 5, 'NX');
    if (!$ok) {
        // 未擷取到鎖,表示已送出
        // 其他處理邏輯...
        write_log('test.log', ['status' => 0, 'msg' => '訂單已送出,請勿重複送出', 'data' => ['uid' => $uid, 'ok' => $ok]]);
        return false;
    }
    // 擷取到鎖,處理訂單邏輯...
    sleep(3);
    // 其他邏輯處理完成,删除鎖
    $redis->del($redis_name . '_' . $uid);
    
    write_log('test.log', ['status' => 1, 'msg' => '訂單處理完成', 'data' => ['uid' => $uid, 'ok' => $ok]]);
    $redis->close();
    
    function write_log($filepath, $data) {
        $dir_name = dirname($filepath);
        if (!file_exists($dir_name)) { // 目錄不存在就建立
            // iconv防止中文名亂碼
            mkdir(iconv("UTF-8", "GBK", $dir_name), 0777, true);
        }
        $fp = fopen($filepath, "a+");// 打開檔案資源通道 不存在則自動建立
        fwrite($fp, date("Y-m-d H:i:s") . ":" . json_encode($data, JSON_UNESCAPED_UNICODE) . "\r\n");//寫入檔案
        fclose($fp);// 關閉資源通道
    }
    
    ---------------------------------------------------------------------------------------
    模拟并發送出
    /usr/local/wrk/wrk -t4 -c100 -d1s http://127.0.0.1:1010/queue_redis/avoid_duplicate.php
    Running 1s test @ http://queue.babytime.vip/queue_redis/avoid_duplicate.php
      4 threads and 100 connections
      Thread Stats   Avg      Stdev     Max   +/- Stdev
        Latency   131.30ms  136.14ms 663.84ms   89.58%
        Req/Sec   168.42    146.36   500.00     84.62%
      534 requests in 1.03s, 99.60KB read
    Requests/sec:    518.12
    Transfer/sec:     96.64KB
    
    檢視日志記錄
    [[email protected] queue_redis]# tail -f test.log 
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:19:{"status":0,"msg":"訂單已送出,請勿重複送出","data":{"uid":1,"ok":false}}
    2020-01-07 19:12:21:{"status":1,"msg":"訂單處理完成","data":{"uid":1,"ok":true}}
    
    
    
               

總結 

以上方案都有其優缺點,根據自己項目實際情況調整和搭配使用。