1. 程式人生 > >前端事務性任務多次請求的問題

前端事務性任務多次請求的問題

專案上線的時候,遇到一個詭異的問題:預上線環境不能復現,生產環境必現。而預上線環境和生產環境最大的區別就在於:預上線是單點應用服務,生產環境則是叢集部署。

問題描述

專案概況:後臺採用Spring Cloud生態搭建的微服務體系,前端為Vue框架,做了前後端分離。
日常情況下,都是單節點部署,不過也使用Eureka實現了服務註冊和發現的機制,可以通過serviceId訪問相關服務。前端請求都通過閘道器進行鑑權和轉發,閘道器使用Zuul進行路徑對映,同時由Ribbon進行負載均衡控制。
上線比較匆忙,除錯為叢集方式後就部署了。
前端有一個需求,是上傳一些資料到後臺服務,然後由後臺服務生成Excel檔案後供前端下載。之所以要上傳到後端,是因為前臺資料不足以或者不方便做這一部分複雜的Excel操作,同時要利用一下後臺的相關資料和模板。
單節點情況下可以正常下載,叢集部署後就開始出現404錯誤,頁面刷白的情況。

前端程式碼

request().excel(data, command, function(success) {
  vueObj.progressBarPercent = 100;
  vueObj.progressBarVisible = false;
  window.clearInterval(id);
  request().download(process.env.API_ROOT + "project/downloadexcel/" + success);
  vueObj.$message({
    message: '匯出成功!',
    type: 'success',
    iconClass:'el-icon-circle-check1'
}); });

可以看出,前端採用了兩次請求的方式處理這個需求:
第一個請求為request().excel,該請求讓後臺生成Excel檔案;
第二個請求為request().download,這個請求從後臺下載Excel檔案。
這個Excel檔案是個一次性檔案,無需儲存留檔。

後端程式碼

StringBuffer tempPath = new StringBuffer();
        tempPath.append(System.getProperty(Consts.JavaTempDir)).append(UUID.randomUUID().toString().replace
("-", "")).append("/").append(catalog).append(suffix); File file = new File(tempPath.toString()); Files.createParentDirs(file); // 設定輸出流 FileOutputStream fOut = new FileOutputStream(file); // 將模板的內容寫到輸出檔案上 templatewb.write(fOut); fOut.flush(); // 操作結束,關閉檔案 fOut.close();

暫且不討論上面未考慮windows與linux的系統相容(路徑斜槓),可以看到處理邏輯是在tmp目錄下生成兩個一個臨時目錄下的檔案。在download之後會清理掉這個臨時檔案。
表面上看起來應該沒什麼問題:檔案生成,路徑下發給前端,前端請求,獲得檔案後寫入輸出流。
然後,叢集環境下穩定復現的問題還是讓我們不得不深究一下存在的漏洞。

原因分析

後端在生成excel的時候,生成在了當前系統的臨時資料夾下。
在單節點的情況下,當然沒有問題,因為生成檔案的請求和下載檔案的請求肯定都會發送到該服務節點。
然後,在叢集環境下,閘道器做了負載均衡,就意味著生成檔案的請求和下載檔案的請求可能被分發到不同的服務節點上。而這兩個節點如果不在同一臺虛擬機器上的話,下載請求必定找不到對應的檔案。
我們在閘道器的地方通過Ribbon執行了負載均衡策略,預設使用輪詢策略,而且沒有類似Nginx那樣的IP對映策略。所以這個Bug穩定復現也就不足為怪了。

由於之前的專案都比較小,單節點基本能夠滿足需求,所以大家在寫程式碼的時候就忘了考慮叢集的情況。甚至以客戶端軟體的思維來寫程式碼,對於檔案操作時特別容易出現問題。

解決方案

解決方案其實有兩個辦法:
改造前端:在同一個請求中完成所有的事情,不要把檔案生成和檔案下載分開。
改造後端:不能利用本地檔案系統,要利用檔案伺服器處理。
和負責前端的同事討論後,說是前端改造可能有問題,會下載不到。因為目前前端的下載還是通過一個form表單的方式來處理的。
那麼我們考慮之後選擇了檔案服務的方式來處理,正好我們也有一個檔案服務。讓檔案生成到檔案伺服器上(我們的檔案服務是採用Mongo搭建的,可以處理小檔案),讓檔案下載也去請求檔案伺服器。雖然檔案服務也是叢集部署的,但是MongoDB資料庫目前是單節點部署的(僅做了實時備份),所以就不會再出現這個問題了。

經驗教訓

涉及共享型的操作,一定要摒棄單機思維。