1. 程式人生 > >Java高併發秒殺系統(二)

Java高併發秒殺系統(二)

本文主要對秒殺系統在大併發的場景下效能瓶頸的做一個分析,以及秒殺系統的優化實現。秒殺系統的業務分析和系統實現,可以參考上一篇文章 Java高併發秒殺系統(一)

1.秒殺系優化分析

下圖列出秒殺的系統流程,其中紅色部分是可能發生高併發的地方,綠色則表示沒有影響。
這裡寫圖片描述

1.1 詳情頁面

  1. 詳情頁面放在CDN
    秒殺還未開始,大量的使用者不斷的點選重新整理頁面。我們將詳情頁靜態化放到CDN,這樣訪問detail頁面就不會落到我們的業務系統,可以減輕系統的壓力。
    這裡寫圖片描述

  2. CDN是什麼?
    CDN是Content Delivery Network(內容分發網路)的縮寫,是可以加快使用者獲取資料的系統。一般將一些不經常變化的資源放到CDN,比如:靜態資源(圖片、HTML、CSS、JavaScript)、視訊檔案等,使用者訪問到CDN上的資源後就不用請求應用系統,不但減輕了系統的壓力還可以提升使用者的訪問速度。
    這裡寫圖片描述

1.2系統時間

  1. 為什麼單獨提供獲取系統時間介面?

    系統部署的時候會將秒殺詳情頁面放到CDN,使用者訪問頁面的時候不用訪問我們的系統,所以也就拿不到系統時間,因此提供一個獲取伺服器系統時間的介面。
    
  2. 獲取系統時間介面不需要優化

    獲取系統時間介面不需要優化,是因為這個介面操作是通過訪問記憶體實現的,訪問一次記憶體大約花費10ns。記憶體操作速度很快,介面只有一個 new Date() ,基本上可以不用考慮GC,1s可以執行10億次操作。
    

1.3地址暴露介面

  1. 能否放到CDN快取?

    秒殺地址介面無法使用CDN服務,因為CDN適用於請求資源不會變化的。而秒殺地址介面返回的資料會發生變化的,因此不適合放在CDN快取。
    
  2. 秒殺地址介面優化思路

    可以考慮將秒殺暴露介面返回的結果放到redis中,設定過期時間保證資料的一致性,或者可以在資料發生變化時通知redis將相應的資料進行更新。
    

    這裡寫圖片描述

1.4執行秒殺操作

  1. 秒殺其他方案分析

    1)通過原子計數器記錄商品的庫存,一般採用redis或者其他NoSQL來保證庫存數的原子性。
    
    2)記錄成功後,將購買記錄的行為訊息傳送到分散式訊息佇列中。
    
    3)後端系統從訊息佇列中訊息訊息,將相應資料修改落地到MySQL
    
    4)優點:方便擴充套件、伸縮性好,能夠抗住非常高的併發。
    

    大型的網際網路公司基本都採取這種方案。
    這裡寫圖片描述

    使用這套方案的成本非常高,不管是運維成本還是開發成本,需要技術人員對這些技術有比較深入的瞭解。 
    

    這裡寫圖片描述

  2. 如何判斷秒殺操作成功?
    這裡寫圖片描述

  3. 秒殺操作瓶頸分析

    秒殺對應資料庫中就是減庫存操作和插入購買記錄這兩步操作,資料庫使用行級鎖來保證操作的原子性,這也就導致秒殺變成了序列操作。
    

    這裡寫圖片描述

    由於應用系統和MySQL資料庫經常不是部署在同一臺機器上,所以資料庫操作的都要經過網路傳輸,這就產生了網路延遲問題,通過還伴隨著GC操作。
    

    這裡寫圖片描述

    1)網路延遲分析
    下面分別對同城機房和異地機房進行分析:
    這裡寫圖片描述

    這裡寫圖片描述

    2)減少鎖的持有時間
    這裡寫圖片描述

  4. 秒殺優化方案
    1)簡單的優化:將insert語句和update語句調換位置,先執行insert語句,可以去除一些重複秒殺減掉一半的網路延遲和GC,目的是降低MySQL的行rowLock時間。
    這裡寫圖片描述

    2)深度優化:將事務操作放到MySQL端執行(儲存過程),減少網路延遲和GC的成本,實現方案有兩種
    這裡寫圖片描述

2.秒殺系統優化實現

2.1 秒殺地址介面優化

  • service層增加redis快取
    因為秒殺商品物件在秒殺活動期間一般不會發生變化的,所以可以在這裡做一層快取。先從快取中獲取秒殺商品物件,如果沒有,則訪問資料庫拿到商品物件之後再放入redis中。如果有,則直接將秒殺地址返回。
public Exposer exportSeckillUrl(long seckillId) {
        //優化點1:快取優化:超時的基礎上維護一致性
        //1.訪問redis
        SecKill secKill = redisDao.getSeckill(seckillId);
        if(secKill == null){
            //2.訪問資料庫
            secKill = secKillDao.queryById(seckillId);
            if(secKill == null){
                return new Exposer(false,seckillId);
            }else{
                //3.放入redis
                redisDao.putSeckill(secKill);
            }
        }

        Date startTime = secKill.getStartTime();
        Date endTime = secKill.getEndTime();
        //系統當前時間
        Date noewTime = new Date();
        if(noewTime.getTime() < startTime.getTime() || noewTime.getTime() > endTime.getTime()){
            return new Exposer(false,seckillId,noewTime.getTime(),startTime.getTime(),endTime.getTime());
        }

        //轉化特定字串的過程,不可逆
        String md5 = getMD5(seckillId);
        return new Exposer(true,md5,seckillId);
    }
  • 將資料庫操作放到MySQL端執行(儲存過程)
    建立儲存過程,service層通過呼叫儲存過程獲取秒殺結果。儲存過程定義如下,通過判斷返回結果result值來判斷,秒殺結果。
-- 秒殺執行儲存過程
DELIMITER $$ -- console ; 轉換為 $$

-- 定義儲存過程
-- 引數:IN 輸入引數; OUT 輸出引數
-- row_count(): 返回上一條sql(delete,insert,update)的影響行數
-- row_count:0:未修改資料; >0: 表示修改的行數; <0: sql錯誤/未執行修改sql
CREATE PROCEDURE `seckill`.`execute_seckill`
  (IN v_seckill_id BIGINT, IN v_phone BIGINT,
    IN v_kill_time TIMESTAMP,OUT r_result INT)
  BEGIN
    DECLARE insert_count INT DEFAULT 0;
    START TRANSACTION ;
    INSERT IGNORE INTO success_killed(seckill_id, user_phone, state, create_time)
      VALUES (v_seckill_id,v_phone,0,v_kill_time);

    SELECT row_count() INTO insert_count;
    IF (insert_count = 0 ) THEN
      ROLLBACK ;
      SET r_result = -1;
    ELSEIF (insert_count < 0) THEN
      ROLLBACK ;
      SET r_result = -2;
    ELSE
      UPDATE seckill SET number = number - 1
      WHERE seckill_id = v_seckill_id AND end_time > v_kill_time AND start_time < v_kill_time AND number > 0 ;

      SELECT row_count() INTO insert_count;
      IF (insert_count = 0) THEN
        ROLLBACK ;
        SET r_result = 0;
      ELSEIF (insert_count < 0) THEN
        ROLLBACK ;
        SET r_result = -2;
      ELSE
        COMMIT ;
        SET r_result = 1;
      END IF;
     END IF ;
  END;
$$
-- 儲存過程定義結束

DELIMITER ;
--
SET @r_result = -3;
-- 執行儲存過程
CALL execute_seckill(1003,18270919398,now(),@r_result);
-- 獲取結果
SELECT @r_result;

-- 儲存過程
-- 1.儲存過程優化:事務行級鎖持有時間
-- 2.不要過度依賴儲存過程
-- 3.簡單的邏輯可以應用儲存過程
-- 4.QPS:一個秒殺單 6000/QPS

-- 檢視儲存過程定義:show create procedure execute_seckill\G

3.秒殺系統的部署

3.1 系統用到的服務

1. CDN:內容分發網路,加速使用者獲取資料,降低伺服器請求量
2. WebServer:Nginx做http伺服器以及Jetty伺服器的反向代理
3. redis:快取熱點資料
4. MySQL:通過事務保證秒殺的一致性和完整性 

這裡寫圖片描述

3.2請求處理的流程:

1. 邏輯叢集是我們開發的部分

這裡寫圖片描述

4.秒殺系統優化總結

  • 優化點

    1.靜態頁面使用CDN快取,實現動靜態資料分離。
    2.後端不經常變化的資料放入redis中進行快取
    3.將事務操作移到MySQL端:MySQL本地執行主鍵SQL可以達到4w QPS ,Java執行也很快,瓶頸主要存在於網路延遲以及GC的停頓操作。這裡可以用儲存過程,解決網路延遲以及GC停頓操作帶來的問題。
    

    這裡寫圖片描述

  • 併發優化
    這裡寫圖片描述

5.資源地址