Seckill秒殺系統高併發優化

        絕大多數秒殺系統都須要實現高併發,這樣就必須在原來的項目基礎上進行優化。簡單的優化頗有可能就會很大地提升系統的併發性能,可是這些優化每每是系統開發人員不多注意的,或者直接被人們忽略。所以要成爲一個出色的開發人員,學會優化技巧與時刻具有系統優化的意識是必須的。java

項目源碼地址:

http://git.oschina.net/COOLFLYCOOL/seckillmysql

本項目秒殺業務核心SQL操做:

        先是UPDATE貨存(貨存減1),再是INSERT購買明細。中間可能會出現重複秒殺,秒殺結束,系統內部錯誤等異常,只要出現異常,事務就會回滾。git

事務行爲分析:

        當一個事務開啓的時候拿到了數據庫表中某一行的行級鎖,另外一個事務進來數據庫時發現鎖住了同一行,若以前的事務不提交或回滾,這個行級鎖不會被釋放,後面進來的那個事務就要等待行級鎖。當第一個事務提交或回滾後,行級鎖被釋放,第二個事務就能得到這個行級鎖進行數據操做,多個事務以此類推,這些過程是一個串行化的操做,也是一個含有大量阻塞的操做。這是MySQL數據庫或是絕大多數關係型數據庫事務實現的方案。web

秒殺系統瓶頸分析:

  1. 如今的事務實現方案是經過Spring的事務對秒殺業務核心進行管理。
  2. 系統目前的秒殺邏輯:java客戶端發送UPDATE語句至MySQL服務端(雖然有網絡延遲,可是各個事務並行),各事務開始競爭行級鎖(阻塞開始),UPDATE執行後將UPDATE結果返回至java客戶端(存在網絡延遲與可能的GC操做),客戶端判斷若是執行成功,則發送INSERT購買明細的SQL語句至MySQL服務端再執行(存在網絡延遲與可能的GC操做),將執行結果返回至java客戶端(存在網絡延遲與可能的GC操做),客戶端再判斷是否執行成功,若是成功,就告知MySQL提交事務(存在網絡延遲)。
  3. 所以,阻塞的時間即從各事務在MySQL服務端競爭行級鎖開始,一直到最後的事務提交,中間有4次的網絡延遲以及java客戶端的各類邏輯判斷。這樣事務的執行週期就會比較長。當排隊的事務比較多的時候,系統性能就會呈指數級降低。

注:Java的GC操做:項目中DAO層各數據庫操做類經過MyBatis實現的生成相應對象注入spring容器中,當使用後再也不被使用時,就會進行垃圾回收。spring

項目優化分析:

經過分析事務的行爲與秒殺系統瓶頸能夠知道,要減小事務等待的時間,削弱阻塞的過程,就要想辦法減小行級鎖持有的時間。sql

  1. 優化思路一:持有行級鎖是在UPDATE上(INSERT不涉及行級鎖),釋放鎖是在Commit(客戶端Spring控制),也就是鎖持有時間是UPDATE和Commit之間。這個過程網絡請求越少,鎖持有時間就越短。
  2. 優化思路二:把客戶端邏輯放在MySQL服務端(使用存儲過程,整個事務在MySQL端完成),避免網絡延遲與GC的影響,也沒有java客戶端的邏輯判斷。

簡單的併發優化(優化思路一):

這裏寫圖片描述

分析:數據庫

參照優化思路一,持有行級鎖在UPDATE上,INSERT不涉及行級鎖(沒INSERT以前根本不存在相應的行,更不可能會有行級鎖)。所以能夠先插入購買明細,這個過程雖然存在網絡延遲,可是各個事務之間是能夠並行的因此不須要等待,這樣就能夠減小各個事務一部分的等待與阻塞。實現減小MySQL row lock的持有時間。(但仍是要把UPDATE庫存的結果返回給客戶端,客戶端再決定是否提交事務,即還有2次網絡延遲)網絡

修改秒殺業務核心代碼順序後:併發

int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone,nowTime);
            //惟一:seckillId,userPhone(聯合主鍵)
            if(insertCount<=0){
                //重複秒殺
                throw new RepeatKillException("seckill repeated");
            }
            else {
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if (updateCount <= 0) {
                    //併發量過高,有可能在等行級鎖的時候庫存沒有了,而且秒殺時間問題在前面已經驗證。
                    throw new SeckillCloseException("seckill is closed");
                }
                else {
                    //秒殺成功
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStateEnums.SUCCESS, successKilled);  //枚舉
                }
            }

深度優化(利用存儲過程實現事務SQL在MySQL端執行):

  1. 參照優化思路二,利用存儲過程將秒殺業務核心事務SQL放在MySQL端執行,這樣就能夠避免事務執行過程當中的網絡延遲與GC影響,事務行級鎖持有時間幾乎就是數據庫數據操做的時間。大大削弱了事務等待的阻塞效應。高併發

秒殺核心SQL事務存儲過程:

DELIMITER //
CREATE PROCEDURE excuteSeckill(IN fadeSeckillId INT,IN fadeUserPhone VARCHAR (15),IN fadeKillTime TIMESTAMP ,OUT fadeResult INT)
  BEGIN
    DECLARE insertCount INT DEFAULT 0;
    START TRANSACTION ;
    INSERT IGNORE success_killed(seckill_id,user_phone,state,create_time) VALUES(fadeSeckillId,fadeUserPhone,0,fadeKillTime);  --先插入購買明細
    SELECT ROW_COUNT() INTO insertCount;
    IF(insertCount = 0) THEN
      ROLLBACK ;
      SET fadeResult = -1;   --重複秒殺
    ELSEIF(insertCount < 0) THEN
      ROLLBACK ;
      SET fadeResult = -2;   --內部錯誤
    ELSE   --已經插入購買明細,接下來要減小庫存
      UPDATE seckill SET number = number -1 WHERE seckill_id = fadeSeckillId AND start_time < fadeKillTime AND end_time > fadeKillTime AND number > 0;
      SELECT ROW_COUNT() INTO insertCount;
      IF (insertCount = 0)  THEN
        ROLLBACK ;
        SET fadeResult = 0;   --庫存沒有了,表明秒殺已經關閉
      ELSEIF (insertCount < 0) THEN
        ROLLBACK ;
        SET fadeResult = -2;   --內部錯誤
      ELSE
        COMMIT ;    --秒殺成功,事務提交
        SET  fadeResult = 1;   --秒殺成功返回值爲1
      END IF;
    END IF;
  END
//

DELIMITER ;

SET @fadeResult = -3;
CALL excuteSeckill(8,13813813822,NOW(),@fadeResult);
SELECT @fadeResult;

Java客戶端(MyBatis)調用數據庫存儲過程:

首先,在Dao層新建一個接口:void killByProcedure(Map [泛型:String,Object] paramMap); 而後在相應的XML中配置實現(注意:jdbcType沒有INT類型的枚舉,要使用BIGINT;一樣沒有VARCHAR的枚舉,要使用BIGINT代替。):

<!--MyBatis調用存儲過程 -->
    <select id="killByProcedure" statementType="CALLABLE">
        CALL executeSeckill(
          #{ seckillId , jdbcType = BIGINT , mode= IN },
          #{ phone ,jdbcType = BIGINT , mode= IN },
          #{ killTime , jdbcType = TIMESTAMP , mode= IN },
          #{ result , jdbcType = BIGINT , mode= OUT }
        )
    </select>

而後,Service層從新寫入一個方法SeckillExecution executeSeckillProcedure(int seckillId, String userPhone, String md5);(注意:在使用MapUtils時要注入commons-collections 3.2依賴)

public SeckillExecution executeSeckillProcedure(int seckillId, String userPhone, String md5) {

        if( md5==null || !md5.equals(getMD5(seckillId)) ){
            return new SeckillExecution(seckillId,SeckillStateEnums.DATA_REWRITE);
        }

        Timestamp nowTime = new Timestamp(System.currentTimeMillis());
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("seckillId",seckillId);
        map.put("phone",userPhone);
        map.put("killTime",nowTime);
        map.put("result", null);

        try{
            seckillDao.killByProcedure(map);
            int result = MapUtils.getInteger(map,"result",-2);
            if(result == 1){
                SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                return new SeckillExecution(seckillId,SeckillStateEnums.SUCCESS,sk);
            }
            else{
                return new SeckillExecution(seckillId,SeckillStateEnums.stateOf(result));
            }
        }
        catch (Exception e){
            logger.error(e.getMessage(),e);
            return new SeckillExecution(seckillId,SeckillStateEnums.INNER_ERROR);
        }
    }

再者,在web-control層將調用方法改爲executeSeckillProcedure,同時由於executeSeckillProcedure已經將重複秒殺,秒殺結束(無庫存)合併到返回的SeckillExecution中,因此不用再捕獲這兩個異常(本來在service層要拋出這兩個異常,是爲了告訴Spring聲明式事務該程序出錯要進行事務回滾)

try{
      SeckillExecution seckillExecution = seckillService.executeSeckillProcedure(seckillId,phone,md5);
      return new SeckillResult<SeckillExecution>(true,seckillExecution);
}
catch (Exception e){
      logger.error(e.getMessage(),e);
      SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStateEnums.INNER_ERROR);
      return  new SeckillResult<SeckillExecution>(true,seckillExecution);
}

最後,集成測試web層:

這裏寫圖片描述 
這裏寫圖片描述 
這裏寫圖片描述

可見秒殺成功,重複秒殺,秒殺結束都正常進行!

相關文章
相關標籤/搜索