教你从0到1搭建秒杀系统-订单异步处理
前面幾篇我們從限流角度,緩存角度來優化了用戶下單的速度,減少了服務器和數據庫的壓力。這些處理對于一個秒殺系統都是非常重要的,并且效果立竿見影,那還有什么操作也能有立竿見影的效果呢?答案是下單的異步處理。
前期概述
在秒殺系統用戶進行搶購的過程中,由于在同一時間會有大量請求涌入服務器,如果每個請求都立即訪問數據庫進行扣減庫存和寫入訂單的操作,對數據庫的壓力是巨大的。我們可以將每一條秒殺的請求存入消息隊列(例如RabbitMQ)中,放入消息隊列后,給用戶返回類似“搶購請求發送成功”的結果。而在消息隊列中,我們將收到的下訂單請求一個個的寫入數據庫中,比起多線程同步修改數據庫的操作,大大緩解了數據庫的連接壓力,最主要的好處就表現在數據庫連接的減少。這種實現可以理解為是一中流量削峰,讓數據庫按照他的處理能力,從消息隊列中拿取消息進行處理。接下來我們用代碼來具體來實現一下訂單的異步處理。
訂單異步處理
代碼編寫
在原來代碼的基礎上,OrderController中增加接口createUserOrderWithMq,代碼如下:
/*** 下單接口:異步處理訂單* @param sid* @return*/ @RequestMapping(value = "/createUserOrderWithMq", method = {RequestMethod.GET}) @ResponseBody public String createUserOrderWithMq(@RequestParam(value = "sid") Integer sid,@RequestParam(value = "userId") Integer userId) {try {// 檢查緩存中該用戶是否已經下單過Boolean hasOrder = orderService.checkUserOrderInfoInCache(sid, userId);if (hasOrder != null && hasOrder) {LOGGER.info("該用戶已經搶購過");return "你已經搶購過了,不要太貪心.....";}// 沒有下單過,檢查緩存中商品是否還有庫存LOGGER.info("沒有搶購過,檢查緩存中商品是否還有庫存");Integer count = stockService.getStockCount(sid);if (count == 0) {return "秒殺請求失敗,庫存不足.....";}// 有庫存,則將用戶id和商品id封裝為消息體傳給消息隊列處理// 注意這里的有庫存和已經下單都是緩存中的結論,存在不可靠性,在消息隊列中會查表再次驗證LOGGER.info("有庫存:[{}]", count);JSONObject jsonObject = new JSONObject();jsonObject.put("sid", sid);jsonObject.put("userId", userId);sendToOrderQueue(jsonObject.toJSONString());return "秒殺請求提交成功";} catch (Exception e) {LOGGER.error("下單接口:異步處理訂單異常:", e);return "秒殺請求失敗,服務器正忙.....";} }createUserOrderWithMq接口整體流程如下:
我們新建一個消息隊列,采用之前使用過的RabbitMQ,寫一個RabbitMqConfig:
@Configuration public class RabbitMqConfig {@Beanpublic Queue orderQueue() {return new Queue("orderQueue");} }添加一個消費者:
@Component @RabbitListener(queues = "orderQueue") public class OrderMqReceiver {private static final Logger LOGGER = LoggerFactory.getLogger(OrderMqReceiver.class);@Autowiredprivate StockService stockService;@Autowiredprivate OrderService orderService;@RabbitHandlerpublic void process(String message) {LOGGER.info("OrderMqReceiver收到消息開始用戶下單流程: " + message);JSONObject jsonObject = JSONObject.parseObject(message);try {orderService.createOrderByMq(jsonObject.getInteger("sid"),jsonObject.getInteger("userId"));} catch (Exception e) {LOGGER.error("消息處理異常:", e);}} }真正的下單的操作,在service中完成,我們在orderService中新建createOrderByMq方法:
@Override public void createOrderByMq(Integer sid, Integer userId) throws Exception {Stock stock;//校驗庫存(不要學我在trycatch中做邏輯處理,這樣是不優雅的。這里這樣處理是為了兼容之前的秒殺系統文章)try {stock = checkStock(sid);} catch (Exception e) {LOGGER.info("庫存不足!");return;}//樂觀鎖更新庫存boolean updateStock = saleStockOptimistic(stock);if (!updateStock) {LOGGER.warn("扣減庫存失敗,庫存已經為0");return;}LOGGER.info("扣減庫存成功,剩余庫存:[{}]", stock.getCount() - stock.getSale() - 1);stockService.delStockCountCache(sid);LOGGER.info("刪除庫存緩存");//創建訂單LOGGER.info("寫入訂單至數據庫");createOrderWithUserInfoInDB(stock, userId);LOGGER.info("寫入訂單至緩存供查詢");createOrderWithUserInfoInCache(stock, userId);LOGGER.info("下單完成"); }可以看到我們真正的下單的操作流程為:
我們這里再redis中使用了set集合記錄商品和用戶的關系,
@Overridepublic Boolean checkUserOrderInfoInCache(Integer sid, Integer userId) throws Exception {String key = CacheKey.USER_HAS_ORDER.getKey() + "_" + sid;LOGGER.info("檢查用戶Id:[{}] 是否搶購過商品Id:[{}] 檢查Key:[{}]", userId, sid, key);return stringRedisTemplate.opsForSet().isMember(key, userId.toString());}key是商品id,value是用戶id的集合,這樣有一些不合理的地方:
- 這種結構默認了一個用戶只能搶購一次這個商品
- 使用set集合,在用戶過多后,每次檢查需要遍歷set,用戶過多有性能問題
大家知道需要做這種操作就好,具體如何在生產環境的redis中存儲這種關系,大家可以深入優化下,我這里只是做個示范。整個上述實現只考慮最精簡的流程,不把前幾篇文章的限流,驗證用戶等加入進來,并且默認考慮的是每個用戶搶購一個商品就不再允許搶購,我的想法是保證每篇文章的獨立性和代碼的任務最小化,至于最后的整合我相信小伙伴們自己可以做到。
流程測試
寫完了代碼以后接下來讓我們實際來操作驗證一下。為了對比,這里我們使用非異步與異步下單來進行結果的對比,這樣也更能看出異步下單的好處。這里為了方便,我把用戶購買限制先取消掉,不然還要來模擬多個用戶id,直接把接口中的檢查緩存中該用戶是否已經下單過的檢驗注釋掉即可。
使用常規的非異步下單接口,模擬1000個用戶同時搶購,商品庫存為500個。可以看到,非異步的情況下,吞吐量是142.8個請求/秒:
而異步情況下,吞吐量為200.7個請求/秒:
這里截圖了在500個庫存剛剛好消耗完的時候的日志,可以看到,一旦庫存沒有了,消息隊列就完成不了扣減庫存的操作,就不會將訂單寫入數據庫,也不會向緩存中記錄用戶已經購買了該商品的消息。
那么問題來了,我們實現了上面的異步處理后,用戶那邊得到的結果是怎么樣的呢?用戶點擊了提交訂單,收到了消息:您的訂單已經提交成功。然后用戶啥也沒看見,也沒有訂單號,用戶開始慌了,點到了自己的個人中心——已付款。發現居然沒有訂單!(因為可能還在隊列中處理)這樣的話,用戶可能馬上就要開始投訴了!太不人性化了,我們不能只為了開發方便,舍棄了用戶體驗!所以我們要改進一下,如何改進呢?其實很簡單:
- 讓前端在提交訂單后,顯示一個“排隊中”;
- 同時,前端不斷請求 檢查用戶和商品是否已經有訂單 的接口,如果得到訂單已經處理完成的消息,頁面跳轉搶購成功。
實現起來,我們只要在后端加一個獨立的接口:
/*** 檢查緩存中用戶是否已經生成訂單* @param sid* @return*/ @RequestMapping(value = "/checkOrderByUserIdInCache", method = {RequestMethod.GET}) @ResponseBody public String checkOrderByUserIdInCache(@RequestParam(value = "sid") Integer sid,@RequestParam(value = "userId") Integer userId) {// 檢查緩存中該用戶是否已經下單過try {Boolean hasOrder = orderService.checkUserOrderInfoInCache(sid, userId);if (hasOrder != null && hasOrder) {return "恭喜您,已經搶購成功!";}} catch (Exception e) {LOGGER.error("檢查訂單異常:", e);}return "很抱歉,你的訂單尚未生成,繼續排隊吧您嘞。"; }我們來試驗一下,首先我們用postman請求兩次下單的接口,發現緩存里面已經有數據,但是實際數據庫中沒有數據,即沒有產生實際的訂單:
但是卻給用戶返回的已經秒殺成功,這樣顯然會讓買家產生疑問。
我們加入checkOrderByUserIdInCache接口以后,前端不停調用獲取真實的訂單信息,第一次請求時:
第二次請求時:
再第二次請求時先去調用以下接口,將以下信息返回
一直刷刷刷接口,數據成功插入以后,接口返回”恭喜您,搶購成功“,這個時候將信息返回給前端進行展示,如下圖:
整個流程就走完了。整個秒殺下訂單的主流程我們全部介紹完了。當然里面很多東西都非常基礎,不過之前也說了這只是一個簡單的秒殺系統,供大家入門理解使用,更復雜的業務大家可以在原來的基礎上慢慢增加。
猜你感興趣:
教你從0到1搭建秒殺系統-防超賣
教你從0到1搭建秒殺系統-限流
教你從0到1搭建秒殺系統-搶購接口隱藏與單用戶限制頻率
教你從0到1搭建秒殺系統-緩存與數據庫雙寫一致
教你從0到1搭建秒殺系統-Canal快速入門(番外篇)
教你從0到1搭建秒殺系統-訂單異步處理
更多文章請點擊:更多…
總結
以上是生活随笔為你收集整理的教你从0到1搭建秒杀系统-订单异步处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 教你从0到1搭建秒杀系统-Canal快速
- 下一篇: 谈谈InnoDB下的记录锁,间隙锁,ne