前言
本篇將完成高并發(fā)優(yōu)化筹燕,包括:
- Redis后端緩存優(yōu)化
- 并發(fā)優(yōu)化
一汛骂、高并發(fā)優(yōu)化分析
在優(yōu)化之前要明白高并發(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ù)行為分析
當(dāng)事務(wù)開啟的時候改衩,另一個事務(wù)要對數(shù)據(jù)庫的同一行進(jìn)行操作岖常,在當(dāng)前事務(wù)沒有commit或者rollback之前,其他事務(wù)是無法執(zhí)行的葫督,此時竭鞍,正在執(zhí)行的事務(wù)獲得了數(shù)據(jù)庫該行的行級鎖
當(dāng)然,如果一個事務(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的客戶端是什么
星號代表的是推薦的客戶端孕荠,接著引入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)化
當(dāng)一個事務(wù)在執(zhí)行對數(shù)據(jù)庫的更改操作的時候,會獲得該行的行級鎖调塌,然后通過update返回的結(jié)果來決定是否進(jìn)行insert操作晋南,最后是commit或者rollback,這時行級鎖也被釋放羔砾,期間肯定會有網(wǎng)絡(luò)延遲和GC的影響
1负间、簡單優(yōu)化
主要的目的是減少行級鎖的持有時間偶妖,通過對事務(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)化完成了