絕大多數秒殺系統都須要實現高併發,這樣就必須在原來的項目基礎上進行優化。簡單的優化頗有可能就會很大地提升系統的併發性能,可是這些優化每每是系統開發人員不多注意的,或者直接被人們忽略。所以要成爲一個出色的開發人員,學會優化技巧與時刻具有系統優化的意識是必須的。java
http://git.oschina.net/COOLFLYCOOL/seckillmysql
先是UPDATE貨存(貨存減1),再是INSERT購買明細。中間可能會出現重複秒殺,秒殺結束,系統內部錯誤等異常,只要出現異常,事務就會回滾。git
當一個事務開啓的時候拿到了數據庫表中某一行的行級鎖,另外一個事務進來數據庫時發現鎖住了同一行,若以前的事務不提交或回滾,這個行級鎖不會被釋放,後面進來的那個事務就要等待行級鎖。當第一個事務提交或回滾後,行級鎖被釋放,第二個事務就能得到這個行級鎖進行數據操做,多個事務以此類推,這些過程是一個串行化的操做,也是一個含有大量阻塞的操做。這是MySQL數據庫或是絕大多數關係型數據庫事務實現的方案。web
注:Java的GC操做:項目中DAO層各數據庫操做類經過MyBatis實現的生成相應對象注入spring容器中,當使用後再也不被使用時,就會進行垃圾回收。spring
經過分析事務的行爲與秒殺系統瓶頸能夠知道,要減小事務等待的時間,削弱阻塞的過程,就要想辦法減小行級鎖持有的時間。sql
分析:數據庫
參照優化思路一,持有行級鎖在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端執行,這樣就能夠避免事務執行過程當中的網絡延遲與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層:
可見秒殺成功,重複秒殺,秒殺結束都正常進行!