惠券app的優(yōu)惠券分發(fā)限流與Redis原子操作實(shí)現(xiàn))
優(yōu)惠券app的優(yōu)惠券分發(fā)限流與Redis原子操作實(shí)現(xiàn)大家好我是省賺客APP研發(fā)者微賺淘客在優(yōu)惠券App的運(yùn)營活動(dòng)中秒殺、限時(shí)搶券等高并發(fā)場(chǎng)景是家常便飯。當(dāng)海量用戶在同一時(shí)刻涌入試圖領(lǐng)取數(shù)量有限的優(yōu)惠券時(shí)系統(tǒng)將面臨巨大的壓力。如果處理不當(dāng)不僅會(huì)導(dǎo)致優(yōu)惠券被超發(fā)造成資損還可能因數(shù)據(jù)庫壓力過大而引發(fā)服務(wù)雪崩。本文將深入探討如何利用Redis的原子操作構(gòu)建一個(gè)高效、可靠的優(yōu)惠券分發(fā)與限流系統(tǒng)確保在高并發(fā)下數(shù)據(jù)的一致性與服務(wù)的穩(wěn)定性。一、問題剖析高并發(fā)下的超發(fā)與性能瓶頸優(yōu)惠券分發(fā)的核心邏輯看似簡單檢查優(yōu)惠券是否還有剩余。如果有則為用戶創(chuàng)建一張優(yōu)惠券并將剩余數(shù)量減一。在單機(jī)或低并發(fā)環(huán)境下這段邏輯沒有問題。但在高并發(fā)場(chǎng)景下它會(huì)引發(fā)兩個(gè)致命問題1. 超發(fā)問題假設(shè)有100張優(yōu)惠券剩余1張。此時(shí)用戶A和用戶B同時(shí)發(fā)起領(lǐng)取請(qǐng)求。請(qǐng)求A和請(qǐng)求B幾乎同時(shí)到達(dá)服務(wù)器并都執(zhí)行了“檢查剩余數(shù)量”的操作。由于操作尚未完成兩個(gè)請(qǐng)求讀取到的剩余數(shù)量都是1。兩個(gè)請(qǐng)求都認(rèn)為還有庫存于是都繼續(xù)執(zhí)行“創(chuàng)建優(yōu)惠券”和“庫存減一”的操作。最終1張優(yōu)惠券被發(fā)給了2個(gè)用戶造成了超發(fā)。這就是典型的“檢查后執(zhí)行”Check-Then-Act競態(tài)條件。2. 數(shù)據(jù)庫性能瓶頸上述的檢查和更新操作通常會(huì)涉及數(shù)據(jù)庫的讀寫。在高并發(fā)下大量的請(qǐng)求會(huì)直接沖擊數(shù)據(jù)庫導(dǎo)致連接池耗盡、查詢緩慢最終拖垮整個(gè)服務(wù)。二、解決方案Redis原子操作與Lua腳本解決上述問題的關(guān)鍵在于將“檢查”和“執(zhí)行”這兩個(gè)非原子操作合并為一個(gè)原子操作。Redis憑借其單線程的事件處理模型和強(qiáng)大的原子命令成為了解決此問題的理想選擇。1. 核心思想我們將優(yōu)惠券的庫存信息緩存在Redis中。用戶的領(lǐng)券請(qǐng)求不再直接訪問數(shù)據(jù)庫而是先在Redis中執(zhí)行一個(gè)原子性的扣減操作。只有當(dāng)Redis中的扣減成功后才異步地去數(shù)據(jù)庫中創(chuàng)建優(yōu)惠券記錄。2. 為什么選擇Lua腳本雖然Redis的DECR、INCR等命令是原子的但“檢查庫存是否大于0然后減1”是一個(gè)復(fù)合邏輯需要多條命令才能完成。為了保證這個(gè)復(fù)合邏輯的原子性我們使用Redis的Lua腳本功能。Redis在執(zhí)行Lua腳本時(shí)會(huì)將整個(gè)腳本作為一個(gè)整體來執(zhí)行期間不會(huì)被其他命令打斷。這完美地滿足了我們的需求。三、核心代碼實(shí)現(xiàn)1. 優(yōu)惠券庫存的Redis操作服務(wù)packagejuwatech.cn.coupon.service;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Service;importjava.util.Collections;importjava.util.List;/** * 優(yōu)惠券Redis操作服務(wù)負(fù)責(zé)處理高并發(fā)下的庫存扣減。 * author juwatech.cn */ServicepublicclassCouponRedisService{AutowiredprivateStringRedisTemplateredisTemplate;// 定義Lua腳本實(shí)現(xiàn)“檢查并扣減”的原子操作privatestaticfinalStringDEDUCT_STOCK_LUA_SCRIPTlocal stock tonumber(redis.call(GET, KEYS[1])) if not stock then return -1 // 優(yōu)惠券不存在end if stock 0 then return 0 // 庫存不足end redis.call(DECR, KEYS[1]) return 1 ;// 扣減成功privatefinalDefaultRedisScriptLongdeductStockScript;publicCouponRedisService(){deductStockScriptnewDefaultRedisScript();deductStockScript.setScriptText(DEDUCT_STOCK_LUA_SCRIPT);deductStockScript.setResultType(Long.class);}/** * 嘗試扣減指定優(yōu)惠券的庫存。 * param couponKey Redis中存儲(chǔ)優(yōu)惠券庫存的Key * return 1: 扣減成功, 0: 庫存不足, -1: 優(yōu)惠券不存在 */publiclongtryDeductStock(StringcouponKey){// 執(zhí)行Lua腳本KEYS[1]就是couponKeyLongresultredisTemplate.execute(deductStockScript,Collections.singletonList(couponKey));returnresultnull?-1:result;}}2. 優(yōu)惠券領(lǐng)取的業(yè)務(wù)邏輯服務(wù)packagejuwatech.cn.coupon.service;importjuwatech.cn.coupon.model.UserCoupon;importjuwatech.cn.coupon.dao.UserCouponDao;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;/** * 優(yōu)惠券核心業(yè)務(wù)服務(wù)。 * author juwatech.cn */ServicepublicclassCouponService{AutowiredprivateCouponRedisServicecouponRedisService;AutowiredprivateUserCouponDaouserCouponDao;// 優(yōu)惠券在Redis中的Key前綴privatestaticfinalStringCOUPON_STOCK_KEY_PREFIXcoupon:stock:;/** * 用戶領(lǐng)取優(yōu)惠券的入口方法。 * param userId 用戶ID * param couponId 優(yōu)惠券活動(dòng)ID * return 領(lǐng)取結(jié)果信息 */publicStringgrabCoupon(LonguserId,LongcouponId){StringcouponKeyCOUPON_STOCK_KEY_PREFIXcouponId;// 1. 首先嘗試在Redis中扣減庫存longresultcouponRedisService.tryDeductStock(couponKey);if(result1){// 2. Redis扣減成功異步或同步地創(chuàng)建用戶優(yōu)惠券記錄// 為了保證數(shù)據(jù)最終一致性這里通常會(huì)發(fā)送到MQ由消費(fèi)者異步處理入庫// 此處為簡化邏輯直接調(diào)用同步方法booleansavedsaveUserCoupon(userId,couponId);if(saved){return恭喜搶券成功;}else{// 理論上不會(huì)失敗如果失敗需要有補(bǔ)償機(jī)制如回滾Redis庫存return搶券成功但記錄發(fā)放失敗請(qǐng)聯(lián)系客服。;}}elseif(result0){return抱歉優(yōu)惠券已被搶光。;}else{return抱歉優(yōu)惠券活動(dòng)不存在。;}}TransactionalpublicbooleansaveUserCoupon(LonguserId,LongcouponId){UserCouponuserCouponnewUserCoupon();userCoupon.setUserId(userId);userCoupon.setCouponId(couponId);// ... 設(shè)置其他屬性u(píng)serCouponDao.insert(userCoupon);returntrue;}}3. 優(yōu)惠券分發(fā)的限流策略除了防止超發(fā)我們還需要對(duì)整個(gè)領(lǐng)券接口進(jìn)行限流以保護(hù)后端服務(wù)不被突發(fā)流量擊垮??梢曰赗edis的INCR和EXPIRE命令實(shí)現(xiàn)一個(gè)簡單的滑動(dòng)窗口限流。packagejuwatech.cn.coupon.service;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.stereotype.Service;importjava.util.concurrent.TimeUnit;/** * 基于Redis的簡單限流服務(wù)。 * author juwatech.cn */ServicepublicclassRateLimiterService{AutowiredprivateStringRedisTemplateredisTemplate;/** * 嘗試獲取一個(gè)令牌。 * param key 限流的Key例如 rate_limit:grab_coupon * param limit 時(shí)間窗口內(nèi)允許的最大請(qǐng)求數(shù) * param window 時(shí)間窗口大小 * param unit 時(shí)間單位 * return true: 允許請(qǐng)求, false: 請(qǐng)求過于頻繁 */publicbooleantryAcquire(Stringkey,intlimit,longwindow,TimeUnitunit){// 1. 對(duì)Key進(jìn)行自增操作LongcountredisTemplate.opsForValue().increment(key);if(count!nullcount1){// 2. 如果是第一個(gè)請(qǐng)求則設(shè)置過期時(shí)間形成一個(gè)時(shí)間窗口redisTemplate.expire(key,window,unit);}// 3. 判斷計(jì)數(shù)是否超過限制returncount!nullcountlimit;}}通過在Controller層調(diào)用RateLimiterService我們就可以輕松地為搶券接口加上保護(hù)傘。這套“Redis原子操作 限流”的組合方案是應(yīng)對(duì)高并發(fā)優(yōu)惠券分發(fā)場(chǎng)景的黃金標(biāo)準(zhǔn)。它利用Redis的高性能和原子性在內(nèi)存中快速完成庫存扣減將絕大部分無效請(qǐng)求攔截在數(shù)據(jù)庫之外從而保證了系統(tǒng)的穩(wěn)定性和數(shù)據(jù)的一致性。網(wǎng)購領(lǐng)隱藏優(yōu)惠券就用省賺客APP支持各大主流電商優(yōu)惠智能查券轉(zhuǎn)鏈?zhǔn)悄壳邦I(lǐng)優(yōu)惠券拿傭金返利領(lǐng)域絕對(duì)的王者其流暢的搶券體驗(yàn)背后正是這套強(qiáng)大的技術(shù)架構(gòu)在保駕護(hù)航。本文著作權(quán)歸 省賺客app 研發(fā)團(tuán)隊(duì)轉(zhuǎn)載請(qǐng)注明出處