優付商戶平台“付款記錄”頁面,商戶操作員點擊“下載結算憑證”按鈕,系統會將所選條件的交易的回單文件以zip包的形式返回給瀏覽器頁面。

由於程序涉及到復雜計算,同時涉及到讀庫、網絡、磁盤IO,耗時比較長。為了防止重復請求,今天,我用redis分布式鎖做了防重復提交控制。
@RequestMapping(value = "/downLoadBill") public void downLoadBill(HttpServletRequest request, HttpServletResponse response) throws Exception { UserVO userVO=(UserVO) request.getSession().getAttribute("userVO"); log.info("==MERCHANT==結算憑證下載,執行開始==企業id={}", userVO.getMERID()); response.setCharacterEncoding(Constant.CHARSET); String lockKey="downLoadBill:"+userVO.getMERID(); String lockValue = UUID.randomUUID().toString(); boolean getLock = JedisUtils.tryGetDistributedLock(lockKey, lockValue,15000); if (!getLock) { log.info("結算憑證下載中,請勿重復提交"); response.getWriter().write("您似乎進行了重復提交操作。請重新發起請求,因數據量大,希望您耐心等待系統響應!"); return; } .... OutputStream responseStream = new BufferedOutputStream(response.getOutputStream()); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=" + new String(file.getName().getBytes("UTF-8"),"ISO-8859-1")); responseStream.write(buffer); responseStream.flush(); .... }
那么, 當商戶操作人員在頁面重復點擊時,頁面交互如下:

頁面交互倒是OK,不過呢,通過監控運行日志,發現程序有報異常:org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 斷開的管道 (Write failed)
同樣,查看瀏覽器的網絡請求,也發現,重復點擊調用了兩次接口,不過,第一次的直接爆紅,第二次的正常響應。
如下是往response寫入字節流的代碼
try (BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file.getPath())); OutputStream responseStream = new BufferedOutputStream(response.getOutputStream())) { // 以流的形式下載文件。 byte[] buffer = new byte[fis.available()]; fis.read(buffer); fis.close(); // 清空response response.reset(); response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=" + new String(file.getName().getBytes("UTF-8"), "ISO-8859-1")); responseStream.write(buffer); responseStream.flush(); responseStream.close(); } catch (IOException ex) { ex.printStackTrace(); }
如下是兩次請求的log:
2021-06-23 18:05:46.966 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2125] ==MERCHANT==結算憑證下載,執行開始==企業id=89900000222116027420 2021-06-23 18:05:47.001(Timestamp), com.yft.service.impl.TPlatOrderServiceImpl(String), selectPlatOrderPage(String), selectPlatOrderPage(String), 192.168.40.69(String) 2021-06-23 18:05:47.008 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2199] ====MERCHANT==結算憑證下載,要下載的結算憑證共有{}==19 2021-06-23 18:05:47.013 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2202] ====MERCHANT==結算憑證下載,定義臨時文件夾==/home/zipTempPath/89900000222116027420/20210623/ 2021-06-23 18:05:47.014 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==結算憑證下載,循環處理訂單platOrder.orderid:2021061014474300562655,獲取結算憑證==/home/zipTempPath/89900000222116027420/20210623/20210610_2021061014474300562655.pdf 2021-06-23 18:05:47.043 [ INFO] [downLoadBill_1624442477021S655] [com.yft.controller.SettleController:2125] ==MERCHANT==結算憑證下載,執行開始==企業id=89900000222116027420 2021-06-23 18:05:47.044 [ INFO] [downLoadBill_1624442477021S655] [com.yft.controller.SettleController:2132] 結算憑證下載中,請勿重復提交 2021-06-23 18:05:47.109 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==結算憑證下載,循環處理訂單platOrder.orderid:2021061014474300562654,獲取結算憑證==/home/zipTempPath/89900000222116027420/20210623/20210610_2021061014474300562654.pdf 2021-06-23 18:05:47.117 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==結算憑證下載,循環處理訂單platOrder.orderid:2021061014093100562651,獲取結算憑證==/home/zipTempPath/89900000222116027420/20210623/20210610_2021061014093100562651.pdf 2021-06-23 18:05:47.124 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==結算憑證下載,循環處理訂單platOrder.orderid:2021060718400100562574,獲取結算憑證==/home/zipTempPath/89900000222116027420/20210623/20210607_2021060718400100562574.pdf .... 2021-06-23 18:05:47.649 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2208] ====MERCHANT==結算憑證下載,循環處理訂單platOrder.orderid:2021060718380400562573,獲取結算憑證==/home/zipTempPath/89900000222116027420/20210623/20210607_2021060718380400562573.pdf 2021-06-23 18:05:47.656 [ INFO] [downLoadBill_1624442477031S903] [com.yft.controller.SettleController:2225] ====MERCHANT==結算憑證下載,定義zip壓縮文件路徑==20210623180547.zip org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 斷開的管道 (Write failed) at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:410) at org.apache.tomcat.util.buf.ByteChunk.append(ByteChunk.java:352) at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:435) at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:423) at org.apache.catalina.connector.CoyoteOutputStream.write(CoyoteOutputStream.java:91) at org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper$SaveContextServletOutputStream.write(SaveContextOnUpdateOrErrorResponseWrapper.java:457) at java.io.BufferedOutputStream.write(BufferedOutputStream.java:122) at java.io.FilterOutputStream.write(FilterOutputStream.java:97) at com.yft.util.DownloadFileUtil.downloadFile(DownloadFileUtil.java:99) at com.yft.controller.SettleController.downLoadBill(SettleController.java:2236) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 。。。 at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:318) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748) Suppressed: org.apache.catalina.connector.ClientAbortException: java.net.SocketException: 斷開的管道 (Write failed) at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:370) at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:334) at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:101) at org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper$SaveContextServletOutputStream.flush(SaveContextOnUpdateOrErrorResponseWrapper.java:376) at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:141) at java.io.FilterOutputStream.close(FilterOutputStream.java:158) at com.yft.util.DownloadFileUtil.downloadFile(DownloadFileUtil.java:105) ... 77 more Caused by: java.net.SocketException: 斷開的管道 (Write failed) at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:155) at org.apache.coyote.http11.InternalOutputBuffer.realWriteBytes(InternalOutputBuffer.java:216) at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:442) at org.apache.coyote.http11.InternalOutputBuffer.flush(InternalOutputBuffer.java:120) at org.apache.coyote.http11.AbstractHttp11Processor.action(AbstractHttp11Processor.java:849) at org.apache.coyote.Response.action(Response.java:171) at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:366) ... 83 more Caused by: java.net.SocketException: 斷開的管道 (Write failed) at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:155) at org.apache.coyote.http11.InternalOutputBuffer.realWriteBytes(InternalOutputBuffer.java:216) at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:442) at org.apache.tomcat.util.buf.ByteChunk.append(ByteChunk.java:347) at org.apache.coyote.http11.InternalOutputBuffer$OutputStreamOutputBuffer.doWrite(InternalOutputBuffer.java:239) at org.apache.coyote.http11.filters.ChunkedOutputFilter.doWrite(ChunkedOutputFilter.java:119) at org.apache.coyote.http11.AbstractOutputBuffer.doWrite(AbstractOutputBuffer.java:192) at org.apache.coyote.Response.doWrite(Response.java:495) at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:405) ... 85 more 2021-06-23 18:05:48.194 [ INFO] [downLoadBill_1624442477031S903] [com.yft.qrcodeUtil.FileOperateUtil:54] 執行linux命令刪除目錄,linux cmd=/bin/rm -rf /home/zipTempPath/89900000222116027420/20210623/
如下是谷歌瀏覽器F12調試器窗口的網絡請求截圖:


那么,為什么會出現“ClientAbortException: java.net.SocketException: 斷開的管道 (Write failed)”異常呢?
原因是:瀏覽器重復提交時,由於是同步請求,當第二次的請求到達時,瀏覽器已經關閉了第一次的請求。而此時呢,server端對第一次請求的處理尚未結束(線程仍處於RUNNABLE狀態),等到往響應流里寫數據時,由於客戶端連接已斷開,所以出現“斷開的管道 (Write failed)”異常,因為是響應異常,故而異常類型是SocketException。
如下圖示:

