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