高并發(fā)秒殺API(六)

前言

本篇將完成高并發(fā)優(yōu)化筹燕,包括:

  • Redis后端緩存優(yōu)化
  • 并發(fā)優(yōu)化

一汛骂、高并發(fā)優(yōu)化分析

在優(yōu)化之前要明白高并發(fā)發(fā)生在哪

高并發(fā)位置分析

紅色的部分是可能出現(xiàn)高并發(fā)的點,綠色部分則沒有影響

當(dāng)用戶進(jìn)入詳情頁的是時候竿报,如果秒殺沒有開始忆某,在頁面就會顯示倒計時,如果開始了奏路,就會顯示執(zhí)行秒殺操作的按鈕畴椰,接著是執(zhí)行秒殺操作,最后是返回結(jié)果

1思劳、獲取系統(tǒng)時間

在詳情頁迅矛,當(dāng)秒殺還沒開始的時候,用戶會不斷的刷新頁面潜叛,這是很有可能的,這就會造成大量的請求壶硅,詳情頁實際應(yīng)該部署到CDN節(jié)點上威兜,CDN會把詳情頁做靜態(tài)化處理,詳情頁當(dāng)中還有許多獲取靜態(tài)資源的請求庐椒,這些靜態(tài)資源也會部署到CDN上

CDN的全稱是Content Delivery Network椒舵,即內(nèi)容分發(fā)網(wǎng)絡(luò)。其基本思路是盡可能避開互聯(lián)網(wǎng)上有可能影響數(shù)據(jù)傳輸速度和穩(wěn)定性的瓶頸和環(huán)節(jié)约谈,使內(nèi)容傳輸?shù)母毂仕蕖⒏€(wěn)定。通過在網(wǎng)絡(luò)各處放置節(jié)點服務(wù)器所構(gòu)成的在現(xiàn)有的互聯(lián)網(wǎng)基礎(chǔ)之上的一層智能虛擬網(wǎng)絡(luò)棱诱,CDN系統(tǒng)能夠?qū)崟r地根據(jù)網(wǎng)絡(luò)流量和各節(jié)點的連接泼橘、負(fù)載狀況以及到用戶的距離和響應(yīng)時間等綜合信息將用戶的請求重新導(dǎo)向離用戶最近的服務(wù)節(jié)點上。其目的是使用戶可就近取得所需內(nèi)容迈勋,解決 Internet網(wǎng)絡(luò)擁擠的狀況炬灭,提高用戶訪問網(wǎng)站的響應(yīng)速度。 ——百度百科

當(dāng)詳情頁部署在CDN上的時候靡菇,這些獲取靜態(tài)資源的請求是不用訪問系統(tǒng)重归,這樣也會導(dǎo)致獲取不到系統(tǒng)時間米愿,無法對是否開啟秒殺做判斷,所以要單獨的獲取時間

也就是鼻吮,在詳情頁可能會有大量的獲取靜態(tài)資源的請求育苟,無論是在秒殺開始前還是秒殺開始后,對于這種有可能出現(xiàn)高并發(fā)的地方椎木,其中一種的措施是把詳情頁部署在CDN中违柏,這樣不同的用戶會請求距離最近的CDN節(jié)點來獲取這些靜態(tài)資源,但是這些請求是不用訪問系統(tǒng)的拓哺,為了時刻獲取到系統(tǒng)時間勇垛,所以就要從系統(tǒng)中獲取時間

那么從系統(tǒng)中獲取時間的操作需不需要優(yōu)化呢?
** 從系統(tǒng)中獲取時間不需要優(yōu)化 **

Java訪問一次內(nèi)存(Cacheline)大約10ns士鸥,1秒等于十億納秒闲孤,當(dāng)我們訪問系統(tǒng)時間,本質(zhì)上也就是new了一個日期對象烤礁,然后返回給用戶讼积,在理想狀態(tài)下,這個操作可以1秒中實現(xiàn)一億次脚仔,沒有后端的訪問勤众,所以這個操作可以不用優(yōu)化

2、秒殺地址接口分析

秒殺地址接口無法使用CDN緩存鲤脏,因為CDN適合對于請求的資源不宜變化的们颜,但是秒殺地址的返回數(shù)據(jù)是在變化的,隨著時間的推移猎醇,秒殺活動從未開始到結(jié)束窥突,這都是不斷變化的

但是適合使用服務(wù)器端緩存,比如Redis硫嘶,先訪問數(shù)據(jù)庫阻问,獲取到了秒殺的數(shù)據(jù),然后放到Redis緩存中沦疾,當(dāng)下一次訪問的時候称近,直接在緩存中查找,緩存中如果有相應(yīng)的數(shù)據(jù)哮塞,直接返回刨秆,而不用再訪問數(shù)據(jù)庫

3、秒殺操作優(yōu)化分析

該操作同樣也不能使用CDN緩存彻桃,大部分的寫操作和核心部分的請求一般無法使用CDN緩存坛善,并且后端緩存困難,因為庫存問題,極短的時間內(nèi)都要對數(shù)據(jù)庫進(jìn)行更新操作眠屎,無法使用緩存技術(shù)剔交,否則會出現(xiàn)不一致的錯誤

4、Java控制事務(wù)行為分析

Java控制事務(wù)行為

當(dāng)事務(wù)開啟的時候改衩,另一個事務(wù)要對數(shù)據(jù)庫的同一行進(jìn)行操作岖常,在當(dāng)前事務(wù)沒有commit或者rollback之前,其他事務(wù)是無法執(zhí)行的葫督,此時竭鞍,正在執(zhí)行的事務(wù)獲得了數(shù)據(jù)庫該行的行級鎖

當(dāng)然,如果一個事務(wù)持有行級鎖的時間極短的話橄镜,也可以忽略偎快,但是實際的情況并沒有那么理想


事務(wù)行為延遲分析

客戶端執(zhí)行update減庫存操作,獲取到執(zhí)行后的結(jié)果洽胶,然后SQL語句會通過網(wǎng)絡(luò)把結(jié)果返回給MySQL的時候晒夹,網(wǎng)絡(luò)延遲是必須考慮的,還要考慮的就是GC

Java GC:(Garbage Collection)垃圾回收機制

GC又分新生代GC和老生代GC姊氓,GC不一定每次出現(xiàn)丐怯,但是一定會出現(xiàn)

具體關(guān)于GC的內(nèi)容有很多,這也是成為Java開發(fā)人員必須要理解掌握的翔横,我作為初學(xué)者读跷,也沒有深入了解過GC,水平有限禾唁,這里只是先提到效览,要成為Java開發(fā)者,GC是避不開的

當(dāng)Java客戶端執(zhí)行這個事務(wù)的時候荡短,Java客戶端和數(shù)據(jù)庫之間的網(wǎng)絡(luò)延遲和可能的GC會持續(xù)較長時間钦铺,特別對于秒殺系統(tǒng)來說,行級鎖的持有時間是一定要優(yōu)化的

對異地機房之間的通信做一個簡單的分析


異地機房

也就是上海機房和北京機房之間的通訊(一來一回)大約需要13毫秒肢预,這是理想狀態(tài),實際應(yīng)該在20毫秒左右洼哎,也就是1秒最多執(zhí)行50次相同的操作

所以優(yōu)化的思路是:
** 把客戶端邏輯放在MySQL服務(wù)端烫映,避免網(wǎng)絡(luò)延遲和GC的影響,從而減少行級鎖的持有時間 **

二噩峦、Redis后端緩存優(yōu)化

首先是Redis安裝锭沟,這個就不多說了,Windows的最好下載MSI识补,Linux用戶可以下載安裝包族淮,在控制臺進(jìn)入Redis目錄,使用make、make install安裝Redis

使用redis-server命令啟動Redis祝辣,這里只是簡單的使用Redis贴妻,所以就使用Redis默認(rèn)的配置

使用redis-cli -p 6379命令連接Redis,默認(rèn)的端口就是6379

要使用Java訪問Redis蝙斜,需要在pom.xml中添加相應(yīng)的依賴名惩,首先是引入的是Redis的客戶端Jedis,在Redis官網(wǎng)中可以看到各種語言訪問redis的客戶端是什么

Redis客戶端

星號代表的是推薦的客戶端孕荠,接著引入Jedis

<!-- redis客戶端:jedis -->
<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.7.3</version>
</dependency>

因為要優(yōu)化的是秒殺暴露接口娩鹉,所以打開SeckillServiceImpl類,找到exportSeckillUrl方法

Seckill seckill = seckillDao.queryById(seckillId);

要優(yōu)化的地方就是這一步稚伍,對數(shù)據(jù)庫的操作弯予,所有秒殺單都要請求暴露秒殺地址接口,可以使用Redis緩存起來个曙,這樣可以降低數(shù)據(jù)庫的訪問壓力

使用Redis常用的基本的邏輯是:

先從cache中取數(shù)據(jù)锈嫩,如果cache中有相應(yīng)的數(shù)據(jù),則直接返回困檩,如果cache中沒有數(shù)據(jù)祠挫,則從數(shù)據(jù)庫中獲取數(shù)據(jù),獲得的數(shù)據(jù)先put到cache中悼沿,然后返回給用戶

為了后期維護(hù)和可擴(kuò)展性等舔,肯定不能把這些邏輯直接寫在業(yè)務(wù)代碼中,也就是不能直接現(xiàn)在SeckillServiceImpl類的exportSeckillUrl方法中

之前說過對數(shù)據(jù)庫或者是其他用于存儲的類所在的包是DAO糟趾,也就是數(shù)據(jù)訪問對象慌植,在Dao包下新建一個cache包,在cache包下新建一個RedisDao類

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);
    }
}

要在后臺打印信息义郑,所以使用slf4j蝶柿,之前已經(jīng)引入了Jedis的依賴,所以可以直接使用非驮,有點類似于數(shù)據(jù)庫連接池的ConnectionPool

然后初始化一個構(gòu)造方法交汤,出入ip和port,用于連接Redis劫笙,因為本項目只是簡單的運用Redis芙扎,不需要復(fù)雜的配置,所以直接初始化JedisPool即可

這個類是對Redis進(jìn)行操作填大,我們使用Redis是要從緩存中獲取Seckill對象戒洼,如果緩存中沒有,還要對數(shù)據(jù)庫進(jìn)行操作允华,并把數(shù)據(jù)存放在緩存中

/**
 * 從Redis獲取數(shù)據(jù)圈浇,由于在redis中的數(shù)據(jù)都是字節(jié)數(shù)組寥掐,所以需要對數(shù)據(jù)進(jìn)行反序列化,轉(zhuǎn)化為想要的類型對象
 * @param seckillId
 * @return
 */
public Seckill getSeckill(long seckillId){
    //redis操作邏輯
    try{
        Jedis jedis = jedisPool.getResource();
        try{
            String key = "seckill: " + seckillId;
            //沒有實現(xiàn)內(nèi)部序列化操作磷蜀,采用自定義序列化
            byte[] bytes = jedis.get(key.getBytes());   
            if(bytes != null){
                //空對象
                Seckill seckill = schema.newMessage();
                ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
                //seckill被反序列
                return seckill;
            }
        } finally {
            jedis.close();
        }
    } catch(Exception e) {
        logger.error(e.getMessage(), e);
    }
    return null;
}

這個方法用于從Redis中獲取數(shù)據(jù)

首先是獲取到Jedis對象召耘,使用了Jedis就要關(guān)閉,所以在接下來的邏輯還要再try/catch蠕搜,在finally中關(guān)閉Jedis

byte[] bytes = jedis.get(key.getBytes());    

由于Redis并沒有實現(xiàn)內(nèi)部序列化操作怎茫,所以在Redis中存儲的數(shù)據(jù)均為字節(jié)碼數(shù)組,所以就要通過反序列化獲取到Object數(shù)據(jù)妓灌,也就是Seckill對象

GitHub上專門有一個對Java序列化技術(shù)的性能比對

本項目采用自定義序列化轨蛤,使用的是protostuff,需要兩個依賴

<!-- 自定義序列化技術(shù) -->
<dependency>
  <groupId>com.dyuproject.protostuff</groupId>
  <artifactId>protostuff-core</artifactId>
  <version>1.0.12</version>
</dependency>
    
<dependency>
  <groupId>com.dyuproject.protostuff</groupId>
  <artifactId>protostuff-runtime</artifactId>
  <version>1.0.12</version>
</dependency>

這樣就可以實現(xiàn)把一個對象轉(zhuǎn)換成二進(jìn)制數(shù)組虫埂,然后傳到Redis中

使用protostuff的API祥山,全局的定義一個動態(tài)的schema,是由protostuff自定轉(zhuǎn)換的掉伏,對性能幾乎沒影響

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

class對象代表類的字節(jié)碼對象缝呕,也就是這個類的類類型,可以通過反射獲取到這個類有哪些屬性和方法斧散,RuntimeSchema就是基于傳遞參數(shù)的字節(jié)碼對象來做一個模式供常,當(dāng)創(chuàng)建對象的時候,會根據(jù)不同的模式賦予相應(yīng)的值

序列化就是根據(jù)字節(jié)碼和字節(jié)碼所對應(yīng)的對象有哪些屬性鸡捐,把字節(jié)碼的數(shù)據(jù)傳遞給那些屬性栈暇,這樣就可以序列化好一個對象

傳遞的參數(shù)是這個對象的class,也就是這個對象的類類型箍镜,再傳遞一個類似schema的內(nèi)容源祈,會描述這個對象的結(jié)構(gòu)

回到getSeckill方法中

//Redis沒有實現(xiàn)內(nèi)部序列化操作,采用自定義序列化
byte[] bytes = jedis.get(key.getBytes());   
if(bytes != null){
    Seckill seckill = schema.newMessage();
    ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
    return seckill;
}

使用Jedis的get方法色迂,返回的是字節(jié)碼數(shù)組香缺,然后對字節(jié)碼數(shù)組進(jìn)行判斷,如果bytes不為空歇僧,則是從Redis中獲取到了數(shù)據(jù)图张,然后使用protostuff進(jìn)行轉(zhuǎn)換

protostuff提供了一個Util工具類來進(jìn)行轉(zhuǎn)換,需要傳遞的參數(shù)有這個對象的字節(jié)碼數(shù)組诈悍,空的對象埂淮,和自定義的模式schema,然后protostuff就會把字節(jié)碼數(shù)組按照傳入的模式写隶,即schema,將數(shù)據(jù)傳入空對象中讲仰,這樣Seckill對象就已經(jīng)被賦值了慕趴,這樣就是把一個字節(jié)數(shù)組轉(zhuǎn)化為我們想要的對象,也就是對象反序列化

當(dāng)Redis緩存中有數(shù)據(jù)的時候,使用getSeckill方法可以獲取到數(shù)據(jù)冕房,當(dāng)Redis緩存中沒有數(shù)據(jù)的時候躏啰,還需要向Redis緩存中存放數(shù)據(jù)

/**
 * 把數(shù)據(jù)放入Redis中時,需要對數(shù)據(jù)進(jìn)行序列化耙册,轉(zhuǎn)化為字節(jié)數(shù)組
 * @param seckill
 * @return
 */
public String putSeckill(Seckill seckill){
    try{
        Jedis jedis = jedisPool.getResource();
        try{
            String key = "seckill: " + seckill.getSeckillId();
            byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                
            //超時緩存
            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;
}

大部分代碼和getSeckill方法相同给僵,主要分析不同部分

try{
    String key = "seckill: " + seckill.getSeckillId();
    byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                
    //超時緩存
    int timeout = 60 * 60;//一小時
    String result = jedis.setex(key.getBytes(), timeout, bytes);
    return result;
} finally {
    jedis.close();
}

我們需要把Object對象,這里是Seckill對象轉(zhuǎn)換成字節(jié)碼數(shù)組详拙,然后put到Redis中帝际,這個過程也就是序列化的過程

首先設(shè)置一個key,然后使用protostuff的IOUtil工具來得到相應(yīng)對象的字節(jié)碼數(shù)組饶辙,傳遞的對象有Seckill對象本身蹲诀,之前自定義的模式schema,需要一個緩存器弃揽,緩存器的大小直接設(shè)置默認(rèn)的大小即可脯爪,當(dāng)對象數(shù)據(jù)特別大的時候,會有緩沖的過程

接著使用Jedis的setex方法矿微,也就是超時緩存痕慢,返回的是String類型,如果錯誤涌矢,則返回的是錯誤信息掖举,如果正確,則會返回OK

然后回到SeckillServiceImpl類蒿辙,既然有了RedisDao拇泛,通過注解注入到Spring容器中

@Autowired
private RedisDao redisDao;

然后把exportSeckillUrl方法中

Seckill seckill = seckillDao.queryById(seckillId);

替換為一下內(nèi)容

//優(yōu)化點:緩存優(yōu)化
//1、訪問redis
Seckill seckill = redisDao.getSeckill(seckillId);
if(seckill == null){
    //2思灌、訪問數(shù)據(jù)庫
    seckill = seckillDao.queryById(seckillId);
    if(seckill == null){
        return new Exposer(false, seckillId);
    }else{
        //3俺叭、放入Redis中
        redisDao.putSeckill(seckill);
    }
}

首先通過RedisDao從Redis中獲取數(shù)據(jù),然后對獲取到的數(shù)據(jù)進(jìn)行判斷泰偿,如果為空熄守,說明緩存中沒有相應(yīng)的數(shù)據(jù),這時就需要從數(shù)據(jù)庫中獲取數(shù)據(jù)耗跛,獲取到的數(shù)據(jù)還要再放入Redis緩存中

三裕照、秒殺操作并發(fā)優(yōu)化

事務(wù)執(zhí)行流程

當(dāng)一個事務(wù)在執(zhí)行對數(shù)據(jù)庫的更改操作的時候,會獲得該行的行級鎖调塌,然后通過update返回的結(jié)果來決定是否進(jìn)行insert操作晋南,最后是commit或者rollback,這時行級鎖也被釋放羔砾,期間肯定會有網(wǎng)絡(luò)延遲和GC的影響

1负间、簡單優(yōu)化

Paste_Image.png

主要的目的是減少行級鎖的持有時間偶妖,通過對事務(wù)執(zhí)行的流程進(jìn)行下調(diào)換就可以使行級鎖的持有時間大幅度減少

把insert操作放在前面,我們在創(chuàng)建success_killed表的時候就設(shè)置了seckilId和userPhone為聯(lián)合主鍵政溃,這個主鍵沖突的概率并不是很高

然后才是update減庫存操作趾访,這時候會獲取到該行的行級鎖,通過insert的返回結(jié)果來決定是否進(jìn)行update操作董虱,最后是commit或者rollback

@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) {
    if(md5 == null || !md5.equals(getMD5(seckillId))){
        throw new SeckillException("seckill data rewrite");
    }
    //執(zhí)行秒殺邏輯:減庫存 + 記錄購買行為
    Date nowTime = new Date();
    try {
        //記錄購買行為
        int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
        if(insertCount <= 0){
            //重復(fù)秒殺
            throw new RepeatKillException("seckill repeated");
        } else {
            //減庫存
            int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
            if(updateCount <= 0){
                //沒有更新記錄扼鞋,即秒殺活動結(jié)束
                throw new SeckillCloseException("seckill is closed");
            } else {
                //秒殺成功
                SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
            }
        }
    } catch(SeckillCloseException e1) {
        throw e1;
    } catch(RepeatKillException e2){
        throw e2;
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
        //所有編譯期異常轉(zhuǎn)化為運行期異常
        throw new SeckillException("seckill inner error: " + e.getMessage());
    }
}

主要改變的是中間try/catch部分

2、深度優(yōu)化

同樣的目的是降低行級鎖到commit或者rollback之間的時間愤诱,同時還要讓MySQL獲得更高的QPS云头,所以將事務(wù)SQL放在MySQL端來執(zhí)行,之前是在MyBatis的配置文件中編寫SQL語句的转锈,這里使用存儲過程盘寡,將編寫的SQL語句直接在MySQL端執(zhí)行

在sql目錄下新建一個seckill.sql文件

-- 秒殺執(zhí)行存儲過程
DELIMITER $$ --console ; 轉(zhuǎn)換為 $$
-- 定義存儲過程
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,create_time)
            value (v_seckill_id,v_phone,v_kill_time);
        select row_count() into insert_count;
        IF (insert_count = 0) THEN
            ROLLBACK;
            set r_result = -1;-- 重復(fù)秒殺
        ELSEIF (insert_count < 0) THEN
            ROLLBACK;
            set r_result = -2;-- 系統(tǒng)錯誤
        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;-- 秒殺結(jié)束
            ELSEIF (insert_count < 0) THEN
                ROLLBACK;
                set r_result = -2;
            ELSE 
                COMMIT;
                set r_result = 1;
            END IF;
        END IF;
    END 
$$
-- 存儲過程定義結(jié)束 

首先要知道的是MySQL中的console是用分號來隔離的,但是在存儲過程中也是通過分號來決定是否換行撮慨,所以使用MySQL的DELIMITER暫時地把MySQL的隔離符號改為$$

DELIMITER $$ --console ; 轉(zhuǎn)換為 $$

接著創(chuàng)建存儲過程

CREATE PROCEDURE `seckill`.`execute_seckill`
    (in v_seckill_id bigint, in v_phone bigint,
     in v_kill_time timestamp, out r_result int)

同時定義一些變量:

  • in:輸入?yún)?shù)竿痰,在存儲過程中可以被使用
  • out:輸出參數(shù),在存儲過程中不能被使用砌溺,但是可以被賦值

然后開始編寫存儲過程的邏輯

DECLARE insert_count int DEFAULT 0;
START TRANSACTION;

先是定義一個變量insert_count影涉,默認(rèn)設(shè)置為0,然后開啟事務(wù)

先執(zhí)行insert語句规伐,插入用戶的購買明細(xì)

insert ignore into
    success_killed (seckill_id,user_phone,create_time)
    value (v_seckill_id,v_phone,v_kill_time);
select row_count() into insert_count;

不要忘記ignore關(guān)鍵字蟹倾,然后使用** MySQL的內(nèi)置函數(shù)row_count(),用于顯示上一條修改類型SQL語句執(zhí)行后被影響的行數(shù) **猖闪,把row_count()的值賦值給insert_count變量鲜棠,接著對insert_count進(jìn)行判斷

IF (insert_count = 0) THEN
    ROLLBACK;
    set r_result = -1;-- 重復(fù)秒殺
ELSEIF (insert_count < 0) THEN
    ROLLBACK;
    set r_result = -2;-- 系統(tǒng)錯誤
ELSE
  • insert_count = 0:未修改語句,設(shè)置輸出參數(shù)r_result的值為-1培慌,對應(yīng)之前在Java中創(chuàng)建的數(shù)據(jù)字典就是重復(fù)秒殺
  • insert_count < 0:SQL錯誤/未執(zhí)行修改SQL豁陆,設(shè)置輸出參數(shù)r_result的值為-2,對應(yīng)之前在Java中創(chuàng)建的數(shù)據(jù)字典就是系統(tǒng)錯誤

如果insert_count的值大于0吵护,則執(zhí)行update操作

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;

同樣最后使用row_count()函數(shù)來判斷是否commit或者rollback

IF (insert_count = 0) THEN
    ROLLBACK;
    set r_result = 0;-- 秒殺結(jié)束
ELSEIF (insert_count < 0) THEN
    ROLLBACK;
    set r_result = -2;
ELSE 
    COMMIT;
    set r_result = 1;
END IF;
  • insert_count = 0:未更改數(shù)據(jù)盒音,設(shè)置輸出參數(shù)r_result的值為0,對應(yīng)之前在Java中創(chuàng)建的數(shù)據(jù)字典就是秒殺結(jié)束
  • insert_count < 0:SQL錯誤/未執(zhí)行修改SQL馅而,設(shè)置輸出參數(shù)r_result的值為-2祥诽,對應(yīng)之前在Java中創(chuàng)建的數(shù)據(jù)字典就是系統(tǒng)錯誤
  • insert_count > 0:成功更改數(shù)據(jù),設(shè)置輸出參數(shù)r_result的值為1瓮恭,對應(yīng)之前在Java中創(chuàng)建的數(shù)據(jù)字典就是秒殺成功

最后結(jié)尾的時候使用$$結(jié)束存儲過程雄坪,然后把換行符再改為分號

DELIMITER ;

整個過程就是把insert插入購買明細(xì)及update減庫存操作放入存儲過程中,在MySQL端直接執(zhí)行這些語句屯蹦,然后就可以直接調(diào)用這個存儲過程

set @r_result = -3;
-- 執(zhí)行存儲過程
call execute_seckill(1003,13522233356,now(),@r_result);
-- 獲取結(jié)果
select @r_result;

首先是定義一個變量诸衔,在console中定義變量使用@盯漂,通過call執(zhí)行存儲過程

然后在Service層的SeckillService類中添加通過使用存儲過程執(zhí)行秒殺操作的方法,通過Java客戶端來調(diào)用存儲過程笨农,這時只要獲取到返回值,通過數(shù)據(jù)字典就可以判斷執(zhí)行結(jié)果

/**
 * 執(zhí)行秒殺操作by存儲過程
 * @param seckillId
 * @param userPhone
 * @param md5
 * @return
 */
SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);

接著在SeckillServiceimpl中復(fù)寫這個方法

public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
    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);
        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);
    }
}

首先要驗證的是MD5帖渠,如果MD5值錯誤谒亦,就返回SeckillExecution對象,數(shù)據(jù)字典中傳遞數(shù)據(jù)被篡改

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

重新獲取系統(tǒng)時間

Date killTime = new Date();

接著要使用SeckillDao獲取到數(shù)據(jù)庫的執(zhí)行結(jié)果空郊,所以在SeckillDao類中創(chuàng)建一個方法

/**
 * 使用存儲過程執(zhí)行秒殺
 * @param paramMap
 */
void killByProcedure(Map<String, Object> paramMap);

傳入的參數(shù)是Map份招,這個方法要在SeckillDao.xml中使用,也就是MyBatis的配置文件狞甚,還是要通過MyBatis調(diào)用存儲過程

<select id="killByProcedure" statementType="CALLABLE">
    call execute_seckill(
    #{seckillId, jdbcType=BIGINT, mode=IN},
    #{phone, jdbcType=BIGINT, mode=IN},
    #{killTime, jdbcType=TIMESTAMP, mode=IN},
    #{result, jdbcType=INTEGER, mode=OUT}
    )
</select>

CALLABLE是jdbc專門為調(diào)用存儲過程而開發(fā)的锁摔,使用call,后面跟存儲過程的名稱

每個傳遞的參數(shù)都包括要傳遞的值哼审,jdbc的類型谐腰,參數(shù)的模式

回到SeckillServiceImpl的executeSeckillProcedure中,要使用killByProcedure涩盾,需要傳入Map類型的參數(shù)十气,所以要先聲明Map對象,

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

Result現(xiàn)在為空春霍,所以賦值null

然后就可以使用SeckillDao來獲取數(shù)據(jù)庫執(zhí)行的結(jié)果砸西,因為使用SeckillDao的方法的時候可能會出現(xiàn)異常,所以try/catch

try {
    seckillDao.killByProcedure(map);
    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);
}

然后是獲取result址儒,使用MapUtil芹枷,要使用MapUtil,需要在pom.xml中引入依賴

<dependency>
  <groupId>commons-collections</groupId>
  <artifactId>commons-collections</artifactId>
  <version>3.2</version>
</dependency>

使用MapUtil傳遞的參數(shù)需要傳遞map莲趣,獲取指定屬性的值鸳慈,如果沒有獲取到該屬性,則賦值-2妖爷,表示系統(tǒng)內(nèi)部錯誤

int result = MapUtils.getInteger(map, "result", -2);

接著就可以通過result進(jìn)行判斷

if(result == 1){
    SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
    return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, sk);
}else{
    return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
}

如果等于1蝶涩,獲取用戶購買明細(xì),返回SeckillExecution對象絮识,否則也返回SeckillExecution對象绿聘,但是可能是各種各樣的異常,所以使用SeckillStateEnum的stateOf方法

public static SeckillStateEnum stateOf(int index){
    for(SeckillStateEnum state : values()){
        if(state.getState() == index){
            return state;
        }
    }
    return null;
}

通過傳遞的數(shù)字對應(yīng)相應(yīng)的字符串次舌,如果不在數(shù)據(jù)字典中的異常熄攘,就要在catch中再返回一次,傳遞的參數(shù)是系統(tǒng)錯誤

logger.error(e.getMessage(), e);
return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);

至此彼念,高并發(fā)優(yōu)化完成了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挪圾,一起剝皮案震驚了整個濱河市浅萧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哲思,老刑警劉巖洼畅,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異棚赔,居然都是意外死亡帝簇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門靠益,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丧肴,“玉大人,你說我怎么就攤上這事胧后∮蟾。” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵壳快,是天一觀的道長纸巷。 經(jīng)常有香客問我,道長濒憋,這世上最難降的妖魔是什么何暇? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮凛驮,結(jié)果婚禮上裆站,老公的妹妹穿的比我還像新娘。我一直安慰自己黔夭,他們只是感情好宏胯,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著本姥,像睡著了一般肩袍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婚惫,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天氛赐,我揣著相機與錄音,去河邊找鬼先舷。 笑死艰管,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蒋川。 我是一名探鬼主播牲芋,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了缸浦?” 一聲冷哼從身側(cè)響起夕冲,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎裂逐,沒想到半個月后歹鱼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡卜高,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年醉冤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片篙悯。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖铃绒,靈堂內(nèi)的尸體忽然破棺而出鸽照,到底是詐尸還是另有隱情,我是刑警寧澤颠悬,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布矮燎,位于F島的核電站,受9級特大地震影響赔癌,放射性物質(zhì)發(fā)生泄漏诞外。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一灾票、第九天 我趴在偏房一處隱蔽的房頂上張望峡谊。 院中可真熱鬧,春花似錦刊苍、人聲如沸既们。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啥纸。三九已至,卻和暖如春婴氮,著一層夾襖步出監(jiān)牢的瞬間斯棒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工主经, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荣暮,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓旨怠,卻偏偏與公主長得像渠驼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容