1. 程式人生 > >五、高併發秒殺系統高併發優化

五、高併發秒殺系統高併發優化

前面的四篇部落格已經將基本的餓秒殺系統完成,本人按照教程一步一步敲程式碼、測試完成。現在將學習如何在上面的基礎上對這個秒殺系統進行優化。

這篇將學習一下內容:
1、高併發系統優化思路分析
2、高併發優化技巧

  • 動靜態資源分離:
    CND(內容分發網路):快取靜態資源
    Redis:快取動態分離
  • 併發優化:
    SQL優化降低行級鎖持有時間
    儲存過程優化降低行級鎖持有時間

3、叢集化部署

一、優化分析

結合該高併發系統考慮,哪些是可能出現高併發點呢?
這裡寫圖片描述

上圖中,所有紅色部分都可能出現高併發的點。

1、為什麼單獨獲取系統時間
在詳情頁,可能出現使用者大量重新整理的情況,此時系統應該部署在CDN節點上,此時要做一個靜態化的處理,當再次重新整理時他獲取CDN靜態資源(css/js/picture),但是,時間要保持實時的,所以要單獨的做處理,單獨從伺服器系統獲取時間,這也是為什麼要在詳情頁單獨獲取系統時間了。

2、CDN是什麼
簡介:CDN(內容釋出網路),是一個加速使用者獲取資料的系統;既可以是靜態資源,又可以是動態資源,這取決於我們的決策策略,大部分視訊加速都依賴於CDN,比如:優酷,愛奇藝等,據此加速;

原理:CDN部署在距離使用者最近的網路節點上,使用者上網的時候通過網路運營商(電信,長城等)訪問距離使用者最近的要給都會網路網路節點上,然後通過都會網路跳到主幹網上,主幹網則根據訪問IP找到訪問資源所在的伺服器,但是,很大一部分內容在上一層節點已經找到,此時不用往下繼續找,直接返回所訪問的資源即可。減小了伺服器的負擔,一般網際網路公司都會建立自己的CDN機群或者租用CDN

3、獲取系統時間不用優化


獲取系統時間的操作不用優化,因為訪問一次記憶體Cacheline大約10ns,1秒內可以做很大數量級的時間獲取操作,所以不用什麼優化。

4、秒殺地址(Redis快取技術)

對於秒殺地址暴露的介面可以做快取呢?

秒殺介面是無法快取在CDN當中的,因為CDN適合快取不容易變化的資源,通常是靜態資源,比如css/jquery資源,每一個URL對應一個不變的內容,秒殺介面地址每次發生變化的,不適合放在CDN快取。

但是適合放在伺服器端做快取(後端快取),比如redis等,下一次訪問的時候直接去伺服器端快取裡面查詢,如果伺服器端快取有了就直接拿走,沒有的話再做正常的資料訪問處理;另外一個原因就是,一致性維護成本很低。

秒殺地址介面的優化策略:
請求地址,先訪問redis,如果redis快取中沒有所需要的資源或者訪問超時,則直接進入mysql獲取系統資源,將獲取的內容更新在redis中(策略:超時穿透,主動更新)。

5、秒殺操作
(1)、秒殺操作分析

  • 秒殺操作分析
    對於這種操作,是無法使用CDN優化的,另外,也不適合在後端快取,因為快取了其他資料,可能會出現不一致的情況。
    秒殺資料操作的一個困難的點就是一行資料大量使用者出現競爭的情況,同時出現大量的update操作,這樣該如何優化呢?

(架構+維護點)

設計一個原子計數器(redis/NoSQL來實現)用來記錄使用者的行為(用分散式MQ實現這個訊息佇列,即把訊息放在MQ當中),然後後端伺服器消費此訊息並落地(用Mysql實現,落地:記錄購買者,能抗住很大的訪問量)
但是這個技術有自己的弱點,就是成本方面:

運維成本和穩定性:NoSQL、MQ等;開發成本在資料一致性和回滾方案等;冪等性難以保證:重複秒殺的問題;不適合新手的架構。

  • 為什麼不用Mysql來解決秒殺操作
    因為Mysql執行update的減庫存比較低效,一條update操作的壓力測試結果可以抗住4wQPS,也就是說,一個商品在一秒內。可以被買4W次;

看一下Java控制事務的行為分析:
(執行庫存減1操作)
Update table set num=num-1 where id=10 andnum>0,緊接著會進行一個inser購買明細的操作,然後commit/rollback;

然後第二個使用者Updatetable set num=num-1 where id=10 and num>0,緊接著等待行鎖,獲得鎖lock,來繼續執行,然後後面的使用者……

這樣下來的話,整個秒殺操作可以說是一種序列化的執行序列

(2)、分析瓶頸所在

update減庫存————》insert購買明細———–》commit/rollback:這個過程都存在網路延遲和GC;並非Java和sql本身慢,而是Java和通訊比較慢;

所以,Java執行時間+網路延遲時間+GC=這行操作的執行時間(大概在2ms。1秒有500次操作,對於秒殺系統來說這個效能呈指數級下降,並不好)。

(3)、優化思路分析

我們知道行級鎖是在commit之後釋放的,那麼我們優化的方向就是減少行級鎖的持有時間。

同城機房需要花0.5-2msmax(1000qps),update之後JVM-GC(50ms) max(20qps);

異地機房一次(北京上海之間額一次update Sql需要20ms。

如何判斷update更新庫存成功?

兩個條件:——Update自身不報錯,客戶端確認影響記錄數

優化思路:

把客戶端邏輯放在Mysql服務端,避免網路延遲和GC影響。

那麼,如何把邏輯放在Mysql服務端呢?

(4)、兩種優化解決方案

  • 定製SQL方案:update/+[auto_commit]/,需要修改Mysql原始碼;這樣可以在SQL執行完之後,直接在服務端完成commit,不用客戶端邏輯判斷之後來執行是否commit/rollback。 但是這個增加了修改Mysql原始碼的成本(不推薦)。
  • 使用儲存過程:整個事務在MySQL端完成(把整個熱點執行放在一個過程當中一次性完成,只需要返回執行的整個結果就行了,這樣可以避免網路延遲和GC干擾)。

    6、優化分析總結
    前端控制:暴露介面(動靜態資料分離)

按鈕防重複(避免重複請求)

動靜態資料分離:CDN快取,後端快取(redis技術實現的查詢)。

事務競爭優化:減少事務鎖時間(用Mysql來解決)。

二、Redis後端快取優化

1、Redis安裝

Redis在通常情況下都是使用機群來維護快取,此處用一個Redis快取為例。

此處應用的目的:使用redis優化地址介面,暴露介面。

若想使用Redis作為服務端的快取機制,則應該首先在服務端安裝Redis

2、優化編碼

第一:pom.xml檔案引入Redis在java環境下的客戶端Jedis。

<!--高併發優化:Redis在java環境中的客戶端Jedis -->  
    <dependency>  
        <groupId>redis.clients</groupId>  
        <artifactId>jedis</artifactId>  
        <version>2.7.3</version>  
    </dependency>  
    <!-- protostuff序列化依賴,寫自己的序列化方式-->  
    <dependency>  
        <groupId>com.dyuproject.protostuff</groupId>  
        <artifactId>protostuff-core</artifactId>  
        <version>1.0.8</version>  
    </dependency>  
    <dependency>  
        <groupId>com.dyuproject.protostuff</groupId>  
        <artifactId>protostuff-runtime</artifactId>  
        <version>1.0.8</version>  
    </dependency>  

第二:新增一個物件序列化的快取類RedisDao.java:

為什麼要使用物件序列化?

序列化的目的是將一個實現Serializable介面的物件轉換成一個位元組序列,可以把該位元組序列儲存起來(例如儲存到一個檔案裡),以後可以隨時將該位元組序列恢復為原來的物件。

序列化的物件佔原有空間的十分之一,壓縮速度可以達到兩個數量級,同時節省了CPU

Redis快取物件時需要將其序列化,而何為序列化,實際就是將物件以位元組形式儲存,這樣,不管物件的屬性是字元創、整形還是圖片、視訊等二進位制型別

都可以將其儲存在位元組陣列中。物件序列化後便可以持久化儲存或者網路傳輸。需要還原物件時候,只需將位元組陣列再反序列化即可。

package dao.cache;

import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/11
 *
 * 注意:往Readis中放入的物件一定要序列化之後再放入,
 * 序列化的目的是將一個實現Serializable介面的物件轉換成一個位元組序列,可以。把該位元組序列儲存起來
 * (例如:儲存在一個檔案裡)以後隨時將該位元組序列恢復原來的物件。
 *序列化的物件佔原來空間的十分之一,壓縮速度可以達到兩個數量級,同時節省了CPU
 *
 * Readis快取物件時需要將物件序列化,而何為序列化,實際上就是將物件以位元組儲存,這樣不管物件的屬性是字串、整形還是圖片、視訊等二進位制型別,
 * 都可以將其儲存在位元組陣列中。物件序列化後便可以持久化儲存或者網路傳輸。需要還原物件時,只需要將位元組陣列再反序列化即可。
 *
 * 因為要在專案中用到,所以要新增@Service, 把這個做成一個服務
 * 因為要初始化連線池JedisPool,所以要implements  InitializingBean並呼叫預設的
 * afterPropertiesSet()方法
 *
 */
@Service
public class RedisDao {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final JedisPool jedisPool;

    public RedisDao(String ip, int port) {
        //一個簡單的配置
        jedisPool = new JedisPool(ip,port);
    }

    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);

    public Seckill getSeckill(long seckillId){
        //快取Redis操作邏輯,而不應該放在Service下,因為這是資料訪問層的邏輯
        try{
            Jedis jedis = jedisPool.getResource();
            try{
                String key = "seckill:"+seckillId;
                //並沒有實現內部序列化操作
                //get->byte[]->反序列化-》Object(Seckill)
                //採用自定義序列化方式
                //採用自定義的序列化,在pom.xml檔案中引入兩個依賴protostuff:pojo
                byte[] bytes = jedis.get(key.getBytes());
                //重新獲取快取
                if(bytes != null){
                    Seckill seckill = schema.newMessage();
                    //將bytes按照從Seckill類建立的模式架構scheam反序列化賦值給物件seckill
                    ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
                    return seckill;
                }
            }finally {
                jedis.close();
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
        }
        return null;
    }


    public String putSeckill(Seckill seckill){
        //set Object (Seckill)---》序列化----》bytes[]
        try{
            Jedis jedis = jedisPool.getResource();
            try {
                String key = "seckill:"+seckill.getSeckillId();
                //protostuff工具
                //將seckill物件序列化成位元組陣列
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill,schema,
                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                //快取時間+key標記+物件序列化結果==》放入快取jedis快取池,返回結果result(OK/NO)
                int timeout = 60*60;
                String result = jedis.setex(key.getBytes(),timeout,bytes);
                return result;
            }finally {
                jedis.close();
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
        }
        return null;
    }
}

第三:配置檔案物件注入再spring-dao.xml檔案中為序列化快取類添加註入引數,用於自動例項化物件;

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context-3.0.xsd" >

    <!-- 配置整合Mybatis的過程 -->
    <!-- 1:配置資料庫相關引數  properties的屬性:${url}         -->
    <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 載入配置引數的檔案所在地 -->

    <!-- 2:資料庫連線池 -->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!-- 配置連線池屬性 -->
        <!-- c3p0連線池的基本屬性 -->
        <property name="driverClass" value="${driver}"/>
        <property name="jdbcUrl" value="${url}"/>
        <property name="user" value="${username}"/>
        <property name="password" value="${password}"/>

        <!--連線池的私有屬性根據高併發應用場景 -->
        <property name="maxPoolSize" value="30"/><!--連線池最多保留30個物件 -->
        <property name="minPoolSize" value="10"/>

        <!-- 關閉連線後不自動commit -->
        <property name="autoCommitOnClose" value="false"/>
        <!-- 獲取連線超時時間 -->
        <property name="checkoutTimeout" value="6000"/>
        <!-- 當前獲取連線失敗重試次數 -->
        <property name="acquireRetryAttempts" value="2"/>
    </bean>


    <!-- 使用框架趨勢:約定大於配置,將相應的檔案放在對應包下,通過已配置項可以自動掃描 -->
    <!-- 3:配置 SqlSessionFactory物件  真正的整合配置-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--注入資料庫連線池  -->
        <property name="dataSource" ref="dataSource"/>
        <!-- 配置Mybatis全域性配置檔案:mybatis-config.xml -->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!-- 掃描entity包 使用別名org.seckill.entity -->
        <property name="typeAliasesPackage" value="entity"/>
        <!-- 掃描Sql配置檔案:mapper需要的xml檔案 -->
        <property name="mapperLocations" value="classpath:mapper/*.xml"/>
    </bean>

    <!-- 4: 配置掃描Dao介面包,動態實現Dao介面,注入到spring容器中-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--注入SqlSessionFactory 使用sqlSessionFactoryBeanName可以在用的時候再找sqlSessionFactory,防止提前初始化-->
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
        <!-- 給出需要掃描Dao介面包-->
        <property name="basePackage" value="dao"/>
    </bean>


    <!--高併發優化模組-->
    <bean id="redisDao" class="dao.cache.RedisDao">

        <constructor-arg index="0" value="localhost"/>
        <constructor-arg index="1" value="6379"/>
    </bean>
</beans>

這裡寫圖片描述

第四:編寫測試類

package dao;

import dao.cache.RedisDao;
import entity.Seckill;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/11
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {

    private long seckillId = 1001;
    @Autowired
    private RedisDao redisDao;

    @Autowired
    private SeckillDao seckillDao;
    @Test
    public void testKill()throws Exception{
        //get  and  put

        //從快取中獲取
        Seckill seckill = redisDao.getSeckill(seckillId);
        if(seckill == null){//快取中沒有就從資料庫查詢
            seckill = seckillDao.queryById(seckillId);
            if(seckill !=null){
                String result = redisDao.putSeckill(seckill);//快取序列化物件
                System.out.println("放入快取結果:"+result);
                seckill = redisDao.getSeckill(seckillId);
                System.out.println(seckill);
            }
        }else {
            System.out.println("從快取中獲取成功:"+seckill);
        }

    }
}

開始測試!
開啟一個 cmd 視窗 使用cd命令切換目錄到 C:\redis 執行 redis-server.exe redis.windows.conf 。
這裡寫圖片描述

執行測試類!
這裡寫圖片描述

開啟redis視覺化工具連線本地redis 檢視快取
RedisDesktopManager:

這裡寫圖片描述

測試結果正常!

第五:進一步修改服務端Redis中介軟體的服務;才能用服務端的快取
修改SeckillServiceImpl.java

 /**
     * 高併發優化後
     * @param seckillId
     * @return
     */
    @Override
    public Exposer exportSeckillUrl(long seckillId) {
        //優化點:快取優化(用Redis快取起來,降低資料庫訪問壓力)
        //通過超時性來維護一致性
        /**
         *
         get from cache
             if null
                get db
             else
                put db
         *
         */
       //1:訪問redis
        Seckill seckill = redisDao.getSeckill(seckillId);
        if(seckill == null){
             seckill = seckillDao.queryById(seckillId);
            if(seckill == null){
                return new Exposer(false,seckillId);
            }else {
                redisDao.putSeckill(seckill);
            }
        }
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        //系統當前時間
        Date nowTime = new Date();
        if(nowTime.getTime() < startTime.getTime() || nowTime.getTime()>endTime.getTime()){
            return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
        }
        String md5 = getMD5(seckillId);
        return new Exposer(true,md5,seckillId);
    }

三、併發優化

1、優化分析
這一部分主要是針對秒殺進行併發優化的;秒殺操作是作為一個事務來執行的。

前面已經分析過了:Update減庫存——–》insert購買明細———-》commit/rollback:這個事務作為一個原子,裡面兩個過程都存在網路延遲和GC。
這裡寫圖片描述

改為:

原來的流程:
第一階段:秒殺開始先Update更新庫存,根據結果記錄數量決定是否插入明細。這個過程中存在網路延遲,資料庫事務持有行級鎖。

第二階段:根據插入insert的結果,最後執行commit/rollback,這個階段也存在網路延遲,資料庫事務持有行級鎖。

最終:行級鎖經歷了兩次的java程式碼執行+網路延遲+GC

方案一:SQL執行順序調整

第一階段:先插入Insert明細(同時根據主鍵判斷了是否重複秒殺),根據返回結果判斷如果不是重複秒殺則表明插入成功,然後進入第二階段;該階段雖然存在網路延遲但是沒有持有行級鎖;

第二階段:直接拿到行級鎖,然後更新Update庫存,最後根據返回結果決定commit/rollback;

該階段持有網路延遲並且持有行級鎖。

最終:行級鎖經歷了一次的java程式碼執行+網路延遲+GC;這種策略將只在最後的更新操作中持有行級鎖,降低了commit/rollback的持有時間,訪問速度提高到了原來的2倍。

方案二:服務端使用儲存過程

這種策略直接在服務端使用儲存過程將兩個階段insert和update操作直接繫結在一起,這樣行級鎖commit/rollback的持有在mysql端就執行完成結束了,然後通過網路返回結果。
最終:該策略相比於方案一,遮蔽掉了所有的網路延遲,大大的提高了訪問速度,可以讓mysql獲得更高的QPS,所以可以把它叫做深度優化

2、SQL調整優化編碼實現
方案一:利用sql順序的調整減掉一半的行級鎖持有時間,在Service實現類SeckillServiceImpl中調整:

 //秒殺是否成功,成功:減庫存,增加明細;失敗:丟擲異常,事務回滾
    /*使用註解控制事務方法的優點:
    1、開發團隊達成一致約定,明確標註事務方法的程式設計風格
    2、保證事務方法的執行時間儘可能短,不要穿插其他網路操作(RPC/HTTP請求),或者剝離到事務方法外部
    3、不是所有的方法都需要事務,如只有一條修改操作或只讀操作不需要事務控制*/
    @Transactional
    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        if(md5 == null || !md5.equals(getMD5(seckillId))){
            throw new SeckillException("秒殺資料被重寫了 (seckill data rewrite)");//秒殺資料被重寫了
            //return new SeckillExecution(seckillId,SeckillStateEnum.DATA_REWRITE);
        }
        //執行秒殺邏輯:減庫存+增加購買明細
        Date nowTime = new Date();
        try{

            //高併發優化前
            /*//減庫存
            int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
            if(updateCount <= 0){
                //沒有更新庫存記錄,說明秒殺結束
                throw new SeckillCloseException("說明秒殺結束(seckill is closed)");
                //return new SeckillExecution(seckillId,SeckillStateEnum.END);
            }else {
                //否則更新庫存成功,秒殺成功,增加明細
                int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
               //看是否該明細被重複插入,即使用者是否重複秒殺
                if(insertCount <= 0){
                    throw new RepeatKillException("重複秒殺(seckill repeated)");
                    //return new SeckillExecution(seckillId,SeckillStateEnum.REPEAT_KILL);
                }else{
                    //秒殺成功,得到成功插入的明細記錄,並返回秒殺資訊
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                    return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS);
                }
            }*/
            //高併發優化後

            //增加明細
            int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
            if(insertCount <= 0){
                throw new RepeatKillException("重複秒殺(seckill repeated)");
            }else{
                //減庫存
                int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
                if(updateCount <= 0){
                    //沒有更新庫存記錄,說明秒殺結束 ----rollback
                    throw new SeckillCloseException("seckill is closed");
                }else{
                    //秒殺成功,得到成功插入的明細記錄,並返回秒殺資訊
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                    return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS);
                }
            }
        }catch (SeckillCloseException e1){
            throw e1;
        }catch (RepeatKillException e2){
            throw e2;
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            //所以編譯期異常轉化為執行期異常
            throw new SeckillException(""+e.getMessage());
        }
    }

3、深度優化

(1)編寫sql語句,建立儲存過程:

--使用儲存過程執行秒殺  
DELIMITER$$ -- console;轉換為$$;定義換行符:表示  

-- 定義儲存過程  
-- 引數:in 輸入引數;out 輸出引數  
--row_count():返回上一條修改型別sql(delete,insert,update)的影響行數。  
--row_count():0:未修改資料;>0:表示修改資料的行數;<0:sql錯誤/未執行修改sql。  
CREATE PROCEDURE 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 ignoresuccess_killed(seckill_id,user_phone,create_time)  
    VALUES(v_seckill_id,v_phone,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 start_time < v_kill_time  
             AND end_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;   -- 秒殺成功返回值為1  
      END IF;  
    END IF;  
  END  
$$  

-- 測試  
DELIMITER;-- 把DELIMITER重新定義還原成分號;  

SET @r_result =-3;  
-- 執行儲存過程  
CALLexecute_seckill(1003,18864598658,now(),@r_result);  
-- 獲取結果  
select @r_result;  


drop procedure execute_seckill; -- 刪除儲存過程

按照上述的SQL語句在mysql資料庫查詢中執行,建立資料庫的儲存過程execute_seckill,然後用下面的語句執行儲存過程測試。

使用儲存過程:

  • 使用儲存過程優化:降低事務行級鎖持有時間;
  • 但是不要過過度依賴儲存過程,要是根據實際需求而定;
  • 簡單邏輯可以應用儲存過程
  • QPS得到提升,一個秒殺單可以接近6000/qps

(2)Service呼叫Procedure實現

第一:Mybatis在SeckillDao.java介面中, 新增呼叫儲存過程的方法宣告:

 /**
     * 秒殺操作優化:使用儲存過程執行秒殺
     * @param paramMap
     */
    void killByProcedure(Map<String,Object> paramMap);

第二步,(Mybatis)在SeckillDao.xml配置檔案當中,編寫SQL語句,帶入引數,呼叫儲存過程:

<!--秒殺操作優化儲存部分 -->  
   <!-- mybatis呼叫儲存過程 id和介面中的方法想偶同,傳入引數-->  
   <select id="killByProcedure"statementType="CALLABLE">  
   callexecute_seckill(  
   #{seckillId,jdbcType=BIGINT,mode=IN},  
   #{phone,jdbcType=BIGINT,mode=IN},  
   #{killTime,jdbcType=TIMESTAMP,mode=IN},  
   #{result,jdbcType=INTEGER,mode=OUT}  
    )  
   </select>   

第三步,在SeckillService.java介面中宣告方法executeSeckillProcedure:

/**
     * 執行秒殺操作  通過儲存過程
     * @param seckillId
     * @param userPhone
     * @param md5
     * @return
     * @throws SeckillException
     * @throws RepeatKillException
     * @throws SeckillCloseException
     */
    SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5)throws SeckillException, RepeatKillException, SeckillCloseException;

第四步,在SeckillServiceImpl.java這個實現類中實現上述定義的方法,在Java客戶端呼叫存戶過程:

/**
     * 通過java客戶端呼叫儲存過程
     * @param seckillId
     * @param userPhone
     * @param md5
     * @return
     * @throws SeckillException
     * @throws RepeatKillException
     * @throws SeckillCloseException
     */
    @Override
    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        if(md5 == null || !md5.equals(getMD5(seckillId))){
            return new SeckillExecution(seckillId,SeckillStateEnum.DATA_REWRITE);
        }
        //執行秒殺邏輯:減庫存+增加購買明細
        Date killTime = new Date();
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("seckillId",seckillId);
        map.put("phone",userPhone);
        map.put("killTime",killTime);
        map.put("result",null);
        //執行儲存過程
        try{
            seckillDao.killByProcedure(map);
            //獲取result
            //此處pom.xml,中引入MapUtil用於獲取集合的值
            int result = MapUtils.getInteger(map,"result",-2);
            if (result == 1){
                SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS,sk);
            }else {
                return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
            }

        }catch(Exception e)
        {
            logger.error(e.getMessage(),e);
            return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
        }
    }

第六步,在SeckillServiceTest.java類中編寫測試方法:

@Test
    public void executeSeckillProcedureTest()
    {
        long seckillId=1001;
        long phone=13476191899l;
        Exposer exposer=seckillService.exportSeckillUrl(seckillId);

        if(exposer.isExposed())
        {
            String md5=exposer.getMd5();
            SeckillExecution execution=seckillService.executeSeckillProcedure(seckillId, phone, md5);
            logger.info(execution.getStateInfo());
            System.out.println(execution.getStateInfo());

        }
    }

4 總結

資料層

資料庫技術:資料庫設計和實現

Mybatis理解和使用技巧:和資料表對應的entity—–Dao介面–—Dao介面配置sql語句的檔案。

Mybatis和Spring的整合技巧:包掃描/物件的注入

業務層技術回顧

站在使用者的角度上進行業務介面設計和封裝

SpringIOC配置技巧:注入

Spring宣告式事務的使用和理解

Web技術回顧

Restful介面的運用:post/get

Spring MVC的使用技巧

前端互動分析過程

Bootstrap和JS的使用:使用現有的格式,使用模組/物件類似的劃分。

併發優化

系統優化點的分析和抽取

事務、鎖、網路延遲理解

前端,CDN,快取等理解和使用

叢集化部署