大廠員工,手把手教你開發(fā)一個高并發(fā)、高可用的營銷活動
前言
這幾年工作中做過不少營銷活動,無論是電商業(yè)務(wù)、支付業(yè)務(wù)、還是信貸業(yè)務(wù),營銷在整個業(yè)務(wù)發(fā)展過程中都是必不可少的。如果前期營銷宣傳到位,會給業(yè)務(wù)帶來一波不小的流量。那么作為技術(shù),如何接住這波流量,而不是服務(wù)被打掛。今天大廠員工,手把手教你開發(fā)出一個高并發(fā)、高可用的營銷活動。
體驗
業(yè)務(wù)
任何脫離業(yè)務(wù)的技術(shù)都是無用功,所以我們先簡單介紹一下業(yè)務(wù)。
業(yè)務(wù)希望我們的用戶在比如購買商品,下單支付等場景,轉(zhuǎn)化率盡可能的高。那么為了獎勵和刺激用戶,我們希望通過一些優(yōu)惠券的方式,來激勵符合我們規(guī)則的用戶,進(jìn)行下單,進(jìn)行支付,進(jìn)行借錢,進(jìn)行購物等等后續(xù)操作。
比如某個用戶符合我們活動的規(guī)則,第一步我們會給他展示優(yōu)惠信息,激勵他來進(jìn)行下一步、完成這個任務(wù),然后在給他發(fā)獎、核銷等后續(xù)動作

校驗抽獎資格
那么根據(jù)以上我們業(yè)務(wù)的分析,我們第一步就是,用戶進(jìn)來,我們查詢活動,并且校驗用戶是否有資格參加活動。如果有多個活動,我們根據(jù)業(yè)務(wù)規(guī)則選擇一個活動讓用戶進(jìn)行參與。
那么這也是我們營銷活動的起點,第一步。如果成千上百萬的用戶一下子涌進(jìn)來,我們?nèi)ゲ樵償?shù)據(jù)庫活動信息,并且校驗規(guī)則,我們的數(shù)據(jù)庫瞬間就會崩掉。所以我們的核心思路是:逐級分流,逐步分散流量。通過備份、限流、降級、熔斷等手段提升可用性。
首先就是加緩存,對于一些靜態(tài)頁面,css,js等文件,可以放在客戶端緩存或者CDN里面。對于活動信息以及規(guī)則,在活動上線之前,將這些信息緩存到redis里面。用戶進(jìn)來時,我們直接取redis里面查詢活動信息,并且計算活動規(guī)則,全程不需要和數(shù)據(jù)庫進(jìn)行交互。最后,評估活動qps,進(jìn)行降級限流,如果流量過大,直接進(jìn)行攔截,防止系統(tǒng)雪崩。
public MktActivityInfo checkActivityRule(String phone) {
// 從redis緩存中取
MktActivityInfo activityInfo = activityCacheService.getActivityInfo();
if (activityInfo == null || StringUtils.isEmpty(activityInfo.getActivityId())) {
return null;
}
ActivityRuleContext context = new ActivityRuleContext();
context.setPhone(phone);
// redis緩存中取
List<MktActivityRule> mktActivityRules = activityCacheService.listActivityRule(activityInfo.getActivityId());
for (MktActivityRule mktActivityRule : mktActivityRules) {
BaseRuleService baseRuleService = BaseRuleFactory.getBaseRuleService(mktActivityRule.getRuleKey());
if (baseRuleService == null || !baseRuleService.check(context)) {
return null;
}
}
return activityInfo;
}
抽獎
一般到達(dá)抽獎,基本都是完成了前面的任務(wù),比如支付,下單等等,最終獲得抽獎資格
- 減庫存。將獎品的庫存信息提前緩存到redis里面,比如獎品100個緩存到redis里面。如果有100W人來搶100個獎品,最終也只有100個人通過redis的校驗
Long num = RedisUtils.decr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);
if (num == null || num < 0) {
// 將redis庫存加回,可做可不做,看業(yè)務(wù)需求
RedisUtils.incr(CACHE_MKT_ACTIVITY_PRIZE_NUM, stringRedisTemplate);
throw new RuntimeException("redis庫存不足 - " + ERROR_MSG);
}
- 根據(jù)業(yè)務(wù)場景,如果不是必中獎。在減庫存之前,做一個隨機(jī)數(shù)。如果在隨機(jī)數(shù)之外,直接返回”獎品被搶完“,限制大部分流量進(jìn)入到redis減庫存
int seed = ThreadLocalRandom.current().nextInt(0, 100) + 1; // 1-100
int random = NumberUtils.toInt(RedisUtils.get(CACHE_MKT_ACTIVITY_PRIZE_RANDOM, stringRedisTemplate));
if (seed > random) {
//log.warn("隨機(jī)比例被攔截 seed = {}, random = {}", seed, random);
throw new RuntimeException("隨機(jī)比例攔截 - " + ERROR_MSG);
}
-
放棄重試
失敗重試會影響系統(tǒng)性能,重試次數(shù)越多,對系統(tǒng)性能的影響越大。
抽獎過程中,從抽獎信息驗證到扣庫存、中獎信息入庫的整個過程中,任何一個環(huán)節(jié)異常或失敗,我們都不會進(jìn)行重試,全部當(dāng)做未中獎處理 -
防止獎品超發(fā)
一般我們會通過樂觀鎖,悲觀鎖,分布式鎖來解決。其中樂觀鎖的效率是最高的。
下面sql不是標(biāo)準(zhǔn)的樂觀鎖,標(biāo)準(zhǔn)的樂觀鎖使用一個version字段來判斷。不過下面的sql能很好的解決樂觀鎖容易失敗的弊端
update mkt_activity_prize set num = num - 1 where num >= 1
// 4. 真正數(shù)據(jù)庫減庫存,并且插入發(fā)獎記錄
// 如果redis預(yù)減庫存成功,這里大概率會成功,基本不會失敗,如果失敗,放棄重試,失敗重試會影響系統(tǒng)性能,重試次數(shù)越多,對系統(tǒng)性能的影響越大。
Boolean execute = transactionTemplate.execute(status -> {
// 4.1 扣減庫存
Integer update = mktActivityPrizeDao.occupyActivityPrize(activityPrize.getActivityId(), activityPrize.getPrizeId());
if (update == null || update <= 0) {
//log.warn("mysql 扣減庫存失敗 update = {}", update);
throw new RuntimeException("mysql庫存扣減失敗 - " + ERROR_MSG);
}
// 4.2 插入發(fā)獎記錄
MktActivityPrizeGrant grant = buildMktActivityPrizeGrant(phone, activityPrize);
Integer insert = mktActivityPrizeGrantDao.insert(grant);
if (insert == null || insert <= 0) {
//log.warn("mysql 插入發(fā)獎記錄失敗 insert = {}", insert);
throw new RuntimeException("mysql 插入發(fā)獎記錄失敗 - " + ERROR_MSG);
}
return true;
});
那么從以上幾個步驟我們可以看出,在真正的數(shù)據(jù)庫減少庫存的時候,隨機(jī)攔截 + redis減庫存已經(jīng)幫我們攔截了大部分流量了,也就只有少部分流量會進(jìn)入到我們真正的減庫存環(huán)節(jié)。如果減庫存的流量還是特別的大,我們還可以調(diào)整隨機(jī)比列,同時減庫存可以放到mq中,直接異步化發(fā)放獎品,基本少整個流程不會與數(shù)據(jù)庫進(jìn)行交互,瓶頸點幾乎可以說是沒有。這種架構(gòu),支撐百萬,千萬qps一點問題都沒有。
最后
本文根據(jù)真實的業(yè)務(wù)場景,詳細(xì)的剖析了一場營銷活動從技術(shù)的角度如何設(shè)計規(guī)劃,做到真正的高并發(fā),高可用,支撐業(yè)務(wù)穩(wěn)定的運(yùn)行。其中涉及到的技術(shù)點還是比較多的,很多細(xì)節(jié)沒有一一列舉,包括如何保證redis庫存和mysql一致,如果業(yè)務(wù)在活動中想修改庫存怎么辦,怎么保證不重復(fù)領(lǐng)取等等問題。
強(qiáng)烈建議大家有空可以自己實現(xiàn)一版,其中的一些細(xì)節(jié)還是非常考驗技術(shù)的,實現(xiàn)下來,一定會有不少的收獲,謝謝大家。





浙公網(wǎng)安備 33010602011771號