今天無意中看見了這位兄弟的文章 通過請求隊列的方式來緩解高並發搶購(初探) 但文章最后說並發超過500 就會出現超發,看了下代碼,的確有這個問題
抽空簡單完善了下,經壓力測試后發現暫無超發現象, 下面為我的代碼,有不足之處請指正交流:
1.請求參數封裝,有個隨機的用戶ID 用來區分不同用戶的請求:
import java.util.Random; public class OrderRequest { private int goodId = new Random().nextInt(100000);// 商品id private int userId = new Random().nextInt(100000);// 用戶ID private int status;// 0:未處理;1:正常;2:異常 public int getGoodId() { return goodId; } public void setGoodId(int goodId) { this.goodId = goodId; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } }
2.controller 入口:
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class QueueController { private static ConcurrentLinkedQueue<OrderRequest> orderQueue = new ConcurrentLinkedQueue<OrderRequest>(); private static AtomicInteger totalprodocut; private static AtomicInteger totalQueSize; private ExecutorService excutorService = Executors.newCachedThreadPool(); public static ReentrantLock queueLock = new ReentrantLock(true); static { totalprodocut = new AtomicInteger(15); totalQueSize = new AtomicInteger(15); } @RequestMapping("/queque") public String queque(OrderRequest orderRequest) { try { // queueLock.lock(); if (totalprodocut.get() < 1) { return "error"; } else { if (orderQueue.size() < totalQueSize.get()) { // System.out.println(orderRequest.getGood_id()+" 增加到待處理隊列成功:" + orderQueue.size()); orderQueue.add(orderRequest); } else { return "error"; } } if (!OrderDealThread.dealLock.isLocked()) { OrderDealThread dealQueue = new OrderDealThread(orderQueue); excutorService.execute(dealQueue); } } catch (Exception e) { e.printStackTrace(); return "not done"; } finally { // queueLock.unlock(); } return "ok"; } }
說明: 如果這里放開lock,可以保證只有允許的請求進入到請求隊列中去,但是效率會降低很多,畢竟每個請求都要去上鎖開鎖
如果這里不要鎖,進入請求隊列的請求會超過我們設定的個數,但不會多太多;
其實這里應該不用鎖,應該快速的響應大多數不能進入請求隊列用戶的請求,已經進入請求隊列的請求在后續處理的時候還會進行業務判斷的
3.業務處理線程
import java.util.Iterator; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; public class OrderDealThread implements Runnable { ConcurrentLinkedQueue<OrderRequest> orderQueue; private static AtomicInteger totalprodocut; public static ReentrantLock dealLock = new ReentrantLock(true); static { totalprodocut = new AtomicInteger(15); } public OrderDealThread() { } public OrderDealThread(ConcurrentLinkedQueue<OrderRequest> queque) { this.orderQueue = queque; } @Override public void run() { while (!orderQueue.isEmpty()) { try { dealLock.lock(); Iterator<OrderRequest> it = orderQueue.iterator(); while (it.hasNext()) { dealQueque(it.next()); } } catch (Exception e) { e.printStackTrace(); } finally { dealLock.unlock(); } } } void dealQueque(OrderRequest orderRequest) { if (orderRequest.getStatus() == 0) { int status = 2; if (totalprodocut.get() > 0) {//需再次判斷是否還有商品,加鎖范圍內 //TODO 下單處理其他邏輯 totalprodocut.decrementAndGet();// 減庫存 status =1; } if (status == 2) { // System.out.println(orderRequest.getUserId() + " deal er:" + Thread.currentThread().getName()); orderRequest.setStatus(2); } else { System.out.println(orderRequest.getUserId() + " deal ok:" + Thread.currentThread().getName()); orderRequest.setStatus(1); } } } }
說明:在真正處理業務的時候還要判斷是否有庫存等邏輯
上述代碼中,部分內容,比如產品數目等應該在活動開始前同步到redis等能快速獲取的中間件中去
下面是我的測試結果:有個疑問? 我設置的線程組是1秒內啟動6000個,為啥這里顯示6萬個? 疑問解除,原來我線程組里面循環了10次 沒注意到那個參數
經過多輪測試,暫未發現多發現象, OK項目始終只有15個
歡迎指正
由於是在windows下測試,並發高了就報錯 java.net.BindException: Address already in use 這個初看上去很像端口被占用,其實是因為已經完成請求的 socket 端口 還處於 TIME_WAIT狀態,把端口耗盡了
處理辦法 參考 https://blog.csdn.net/ywb201314/article/details/51258777
=============增加=====================
有同學給我發郵件提了一個問題:
問:你代碼里面請求進入了請求隊列就返回了成功,但是你寫了請求隊列中請求數會超過預期值,那我怎么做下一步的操作呢?比如進行付款?
答:很好的問題! 感謝你的提問
說下處理邏輯:1.進入了請求隊列,就有可能被請求到,而且這里是異步,就是說請求收到ok了,但后台邏輯完全可能還沒處理
所以,在接收到OK后,前端應該發起一個類似倒計時頁面,如下:
提示系統正常處理中,同時隔一定時間去后台確認是否處理完成以及狀態
當獲取到的狀態為完成且成功時,跳轉到下一步如付款操作界面,現在很多秒殺系統都是這么處理的
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=bunl3uhohufn