偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

大促風(fēng)暴眼:10萬+/秒請求下,百萬優(yōu)惠券如何精準(zhǔn)發(fā)放不超發(fā)?

數(shù)據(jù)庫 其他數(shù)據(jù)庫
數(shù)據(jù)庫終究是持久化存儲,其IO性能有上限。要應(yīng)對10萬QPS,我們必須將主戰(zhàn)場轉(zhuǎn)移到更快的內(nèi)存中。這就是引入緩存(如Redis)的原因。

每一場電商大促,都是一場沒有硝煙的技術(shù)戰(zhàn)爭。而“優(yōu)惠券”作為刺激消費的核心武器,其發(fā)放系統(tǒng)的穩(wěn)定性與準(zhǔn)確性,直接關(guān)系到用戶體驗和平臺的真金白銀。想象一下這樣一個場景:平臺精心準(zhǔn)備了100萬張優(yōu)惠券,作為引爆流量的爆點?;顒由暇€瞬間,洶涌的流量洪峰撲來,每秒超過10萬次的請求高喊著:“給我一張券!”。此時,系統(tǒng)后端那記錄著“庫存:1,000,000”的數(shù)據(jù)庫,成為了風(fēng)暴的中心。

超發(fā)——那個不能承受之痛

所謂“超發(fā)”,就是系統(tǒng)發(fā)出的優(yōu)惠券數(shù)量超過了預(yù)設(shè)的庫存。這不僅僅是“多發(fā)了幾張券”那么簡單,它會導(dǎo)致:

1. 資損風(fēng)險:超發(fā)的優(yōu)惠券被用戶使用,平臺需要承擔(dān)額外的成本。

2. 用戶投訴與輿情危機:搶到券的用戶發(fā)現(xiàn)無法使用,或訂單被取消,會引發(fā)大面積的用戶不滿和信任危機。“玩不起就別玩”的輿論會迅速發(fā)酵。

3. 平臺信譽受損:一次超發(fā)事故,足以讓平臺長期建立的公信力大打折扣。

那么,在每秒10萬次請求的沖擊下,我們?nèi)绾螛?gòu)建一個固若金湯的防超發(fā)系統(tǒng),確保發(fā)出去的每一張券都在100萬庫存之內(nèi)呢?讓我們從最簡單的方案開始,逐步深入到能夠抵御洪峰的架構(gòu)。

第一章:天真與陷阱 —— 為什么簡單的SQL更新會失靈?

很多開發(fā)者的第一反應(yīng)可能是:這還不簡單?在發(fā)券時,先查詢一下當(dāng)前庫存,如果大于0,再執(zhí)行庫存扣減和發(fā)券操作。

對應(yīng)的SQL偽代碼可能是這樣:

-- 1. 查詢庫存
SELECT stock FROM coupon WHERE id = #{couponId};

-- 2. 應(yīng)用層判斷
if (stock > 0) {
    // 3. 扣減庫存
    UPDATE coupon SET stock = stock - 1 WHERE id = #{couponId};
    // 4. 給用戶發(fā)券
    INSERT INTO user_coupon (user_id, coupon_id) VALUES (#{userId}, #{couponId});
}

這個邏輯在單線程或低并發(fā)下完美無缺。但在每秒10萬請求的并發(fā)環(huán)境下,它不堪一擊。問題就在于競態(tài)條件(Race Condition)。

并發(fā)場景模擬:

假設(shè)此時庫存只剩1張,同時有兩個用戶A和B發(fā)來了請求。

1. 請求A和請求B同時執(zhí)行了 SELECT 語句,它們都讀到了 stock = 1。

2. 兩個請求在應(yīng)用邏輯判斷中都順利通過 (stock > 0)。

3. 請求A先執(zhí)行了 UPDATE,將庫存成功扣減為0。

4. 緊接著,請求B也執(zhí)行了 UPDATE。由于它不知道庫存已經(jīng)被A修改,這條語句依然會執(zhí)行成功(MySQL的Update本身是原子的,stock = stock - 1 會導(dǎo)致 stock = -1?。?/p>

結(jié)果: 1張庫存,發(fā)出了2張券,超發(fā)了!

問題的根源在于,“查詢”和“更新”是兩個獨立的操作,它們組成的復(fù)合邏輯在并發(fā)下不是原子性的。

第二章:數(shù)據(jù)庫的銅墻鐵壁 —— 悲觀鎖與樂觀鎖

要解決原子性問題,我們首先想到的就是求助數(shù)據(jù)庫的“鎖”。

方案一:悲觀鎖 —— “先占坑,再辦事”

悲觀鎖的思想是,我認為任何時候都會發(fā)生并發(fā)沖突,所以我在操作數(shù)據(jù)之前,先把它鎖住,讓別人無法操作。

在MySQL中,我們可以使用 SELECT ... FOR UPDATE 來實現(xiàn)。

BEGIN; -- 開啟事務(wù)

-- 1. 查詢并鎖定這條優(yōu)惠券記錄
SELECT stock FROM coupon WHERE id = #{couponId} FOR UPDATE;

-- 2. 判斷庫存
if (stock > 0) {
    // 3. 扣減庫存
    UPDATE coupon SET stock = stock - 1 WHERE id = #{couponId};
    // 4. 給用戶發(fā)券
    INSERT INTO user_coupon (user_id, coupon_id) VALUES (#{userId}, #{couponId});
}

COMMIT; -- 提交事務(wù),釋放鎖

工作原理: 當(dāng)請求A執(zhí)行 SELECT ... FOR UPDATE 時,數(shù)據(jù)庫會為這條記錄加上行鎖。在事務(wù)提交前,請求B執(zhí)行同樣的語句會被阻塞,直到請求A的事務(wù)結(jié)束釋放鎖。此時請求B讀到的 stock 已經(jīng)是0,判斷失敗,不會發(fā)券。

優(yōu)缺點:

? 優(yōu)點:簡單,能有效防止超發(fā)。

? 缺點:

性能瓶頸:所有請求串行化,在高并發(fā)下,數(shù)據(jù)庫連接迅速被占滿,大量請求排隊等待,導(dǎo)致系統(tǒng)響應(yīng)緩慢甚至超時。10萬QPS直接壓垮數(shù)據(jù)庫。

死鎖風(fēng)險:復(fù)雜的鎖依賴可能導(dǎo)致死鎖。

結(jié)論:悲觀鎖適用于并發(fā)量不高的場景,在10萬QPS的洪峰下,它不是一個可行的選擇。

方案二:樂觀鎖 —— “相信美好,但驗證一下”

樂觀鎖的思想與悲觀鎖相反,我認為沖突很少發(fā)生,所以我不加鎖,直接去更新。但在更新時,我會檢查一下在我之前有沒有人修改過這個數(shù)據(jù)。

通常我們使用一個版本號(version)字段來實現(xiàn)。

表結(jié)構(gòu)增加一列:version int。

-- 1. 查詢當(dāng)前庫存和版本號
SELECT stock, version FROM coupon WHERE id = #{couponId};

-- 2. 應(yīng)用層判斷庫存
if (stock > 0) {
    // 3. 扣減庫存,但附加上版本號條件
    UPDATE coupon SET stock = stock - 1, version = version + 1
    WHERE id = #{couponId} AND version = #{oldVersion};

    // 4. 判斷UPDATE是否成功
    if (affected_rows > 0) {
        // 更新成功,說明沒有并發(fā)沖突,發(fā)券
        INSERT INTO user_coupon (user_id, coupon_id) VALUES (#{userId}, #{couponId});
    } else {
        // 更新失敗,說明在我查詢之后,庫存已經(jīng)被別人修改。重試或返回失敗。
    }
}

工作原理: 請求A和B都讀到了 version = 1。請求A先執(zhí)行Update,條件 version=1 成立,庫存被扣減,同時 version 變?yōu)?。請求B再執(zhí)行Update時,條件 version=1 已經(jīng)不成立,所以更新影響行數(shù)為0,請求B失敗。

優(yōu)缺點:

? 優(yōu)點:避免了悲觀鎖的巨大性能開銷,適合讀多寫少的場景。

? 缺點:

高失敗率:在極高并發(fā)下,大量請求會更新失敗,用戶體驗不佳(明明看到有券,一點就沒了)。

需要重試機制:通常需要配合重試(例如在應(yīng)用層循環(huán)重試幾次),增加了復(fù)雜度。

結(jié)論:樂觀鎖比悲觀鎖性能好很多,但在瞬時10萬QPS的極端場景下,大量的失敗和重試對數(shù)據(jù)庫的沖擊依然不小,并非最優(yōu)解。

第三章:邁向巔峰 —— 將庫存前置到緩存

數(shù)據(jù)庫終究是持久化存儲,其IO性能有上限。要應(yīng)對10萬QPS,我們必須將主戰(zhàn)場轉(zhuǎn)移到更快的內(nèi)存中。這就是引入緩存(如Redis)的原因。

方案三:Redis原子操作 —— “一錘定音”

Redis是單線程工作模型,所有的命令都是原子執(zhí)行的。我們可以利用這個特性,將庫存扣減這個核心邏輯放在Redis中完成。

步驟:

1. 預(yù)熱:活動開始前,將庫存數(shù)量100萬寫入Redis。

SET coupon_stock:123 1000000

2. 扣減:用戶請求時,使用Redis的 DECR 或 DECRBY 命令。

// 偽代碼示例
public boolean tryAcquireCoupon(Long couponId, Long userId) {
    // 使用 DECR 原子性扣減庫存
    Long currentStock = redisTemplate.opsForValue().decrement("coupon_stock:" + couponId);

    if (currentStock >= 0) {
        // 扣減成功,庫存>=0,說明用戶搶到了資格
        // 此時可以異步地向數(shù)據(jù)庫寫入發(fā)券記錄
        asyncService.sendMessageToMQ("coupon_acquired", userId, couponId);
        return true;
    } else {
        // 扣減后庫存小于0,說明已搶光,需要回滾剛才的扣減
        redisTemplate.opsForValue().increment("coupon_stock:" + couponId);
        return false;
    }
}

為什么是原子性的?DECR 命令在Redis內(nèi)部一步完成“讀取-計算-寫入”,不存在并發(fā)干擾。即使10萬個請求同時執(zhí)行 DECR,Redis也會讓它們排隊,一個一個執(zhí)行。第一個請求執(zhí)行后庫存變?yōu)?99999,第二個變?yōu)?99998...直到0,然后是-1, -2...

關(guān)鍵點:

? 判斷時機:我們通過判斷 DECR 后的結(jié)果是否 >=0 來決定是否成功。等于0是最后一張,大于0是普通情況,小于0則意味著超發(fā)(我們通過后面的 INCR 進行回滾,實際上并未超發(fā))。

? 異步落庫:Redis只負責(zé)處理最核心的庫存扣減邏輯。真正的發(fā)券記錄(寫入用戶券表)可以通過消息隊列異步化,這樣就把數(shù)據(jù)庫的巨大寫入壓力給化解了。

這個方案已經(jīng)非常強大了,但它還有一個潛在問題:在庫存為1時,瞬間有1萬個請求執(zhí)行了 DECR,實際上只有1個請求會成功(結(jié)果=0),另外9999個請求都會失?。ńY(jié)果<0)。雖然邏輯正確,但這9999次對Redis的寫操作其實是浪費的,因為庫存明明已經(jīng)沒了。

方案四:Redis + Lua腳本 —— “終極武器”

我們可以通過Lua腳本,將“判斷庫存”和“扣減庫存”等多個操作在Redis服務(wù)端一次性、原子性地完成,從而獲得極致的性能和控制力。

Lua腳本在Redis中執(zhí)行時,可以視為一個事務(wù),不會被其他命令打斷。

-- try_acquire_coupon.lua
local stockKey = KEYS[1] -- 庫存Key
local userId = ARGV[1]   -- 用戶ID
local couponId = ARGV[2] -- 券ID

-- 1. 獲取當(dāng)前庫存
local stock = tonumber(redis.call('GET', stockKey))

-- 2. 庫存不足,直接返回
if stock <= 0 then
    return -1 -- 庫存不足的標(biāo)識
end

-- 3. 庫存充足,執(zhí)行扣減
redis.call('DECR', stockKey)
-- 這里理論上還可以做更多事情,比如將用戶ID寫入一個“搶到券的用戶集合”,用于防重復(fù)搶購
-- redis.call('SADD', 'coupon_winner_set', userId)

return 1 -- 搶券成功的標(biāo)識

在Java應(yīng)用中調(diào)用該腳本:

// 預(yù)加載腳本,獲取一個sha1標(biāo)識
String script = "lua腳本內(nèi)容...";
String sha = redisTemplate.scriptLoad(script);

public boolean tryAcquireCoupon(Long couponId, Long userId) {
    List<String> keys = Arrays.asList("coupon_stock:" + couponId);
    Object result = redisTemplate.execute(
        new RedisCallback<Object>() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                // 使用evalsha執(zhí)行腳本,性能更好
                return connection.evalSha(sha, ReturnType.INTEGER, 1,
                                         keys.get(0).getBytes(),
                                         userId.toString().getBytes(),
                                         couponId.toString().getBytes());
            }
        }
    );

    Long res = (Long) result;
    if (res == 1) {
        // 成功,異步落庫
        asyncService.sendMessageToMQ("coupon_acquired", userId, couponId);
        return true;
    } else {
        // 失敗,res == -1
        return false;
    }
}

Lua腳本方案的優(yōu)勢:

1. 極致的原子性:所有邏輯在一個腳本中完成,無競態(tài)條件。

2. 極少的網(wǎng)絡(luò)IO:一次腳本調(diào)用代替了多次 GET/DECR 等命令的往返。

3. 可擴展性:可以在腳本內(nèi)輕松實現(xiàn)更復(fù)雜的邏輯,如記錄用戶ID防止同一用戶重復(fù)搶購(通過Redis的Set結(jié)構(gòu))。

4. 性能巔峰:這是應(yīng)對超高并發(fā)讀寫的終極方案,能夠最大限度地發(fā)揮Redis的性能。

第四章:構(gòu)建完整的防御體系

單一的技術(shù)方案再強大,也需要一個完整的系統(tǒng)架構(gòu)來支撐。一個成熟的大促發(fā)券系統(tǒng),還需要考慮以下方面:

1. 網(wǎng)關(guān)層限流與防護:在流量入口(如API網(wǎng)關(guān))就進行限流,將超過系統(tǒng)處理能力的請求直接拒絕掉,保護下游服務(wù)。例如,設(shè)置每秒最多通過15萬個請求。

2. 緩存集群與分片:單機Redis可能有性能瓶頸或單點故障風(fēng)險。我們需要使用Redis集群,并通過合理的分片策略(例如按優(yōu)惠券ID分片),將不同優(yōu)惠券的請求分散到不同的Redis節(jié)點上。

3. 異步化與消息隊列:正如前面提到的,搶券資格判斷(Redis操作)和實際發(fā)券(數(shù)據(jù)庫操作)必須解耦。使用RabbitMQ、RocketMQ或Kafka,將搶券成功的消息發(fā)送到隊列,由下游的消費者服務(wù)按自己的能力從隊列中取出消息,平穩(wěn)地寫入數(shù)據(jù)庫。

4. 令牌桶或漏桶算法:在應(yīng)用層,可以使用令牌桶算法進一步平滑請求,防止瞬間流量沖垮Redis。例如,每秒只發(fā)放500個令牌到令牌桶,請求拿到令牌后才能去執(zhí)行Lua腳本搶券。

5. 降級與熔斷:如果Redis或數(shù)據(jù)庫出現(xiàn)異常,系統(tǒng)需要有自動降級策略(如直接返回“活動太火爆”頁面)和熔斷機制,防止雪崩效應(yīng)。

總結(jié)

面對“100萬庫存,10萬+/秒請求”的極端場景,我們的技術(shù)選型路徑是清晰的:

初級方案(不可行):查詢再更新 → 必然超發(fā)中級方案(不適用):數(shù)據(jù)庫悲觀/樂觀鎖 → 性能瓶頸高級方案(可行):Redis原子操作(DECR) → 性能良好,略有浪費終極方案(推薦):Redis Lua腳本 + 異步消息隊列 + 網(wǎng)關(guān)限流

這個終極方案的精髓在于:

? 核心邏輯原子化:利用Redis單線程和Lua腳本的原子性,在內(nèi)存中完成最關(guān)鍵的庫存扣減判斷,速度快且絕不超發(fā)。

? 讀寫操作解耦:前端快速判斷資格,后端異步持久化數(shù)據(jù),保護脆弱的關(guān)系型數(shù)據(jù)庫。

? 流量層層過濾:通過網(wǎng)關(guān)限流、應(yīng)用層限流等手段,將超出系統(tǒng)設(shè)計容量的流量提前拒之門外。

通過這樣一套組合拳,我們才能在大促的流量風(fēng)暴中,真正做到忙而不亂,精準(zhǔn)發(fā)放,讓每一張優(yōu)惠券都“師出有名”,守護好系統(tǒng)的穩(wěn)定與平臺的聲譽。這正是高并發(fā)系統(tǒng)設(shè)計的藝術(shù)與魅力所在。


責(zé)任編輯:武曉燕 來源: 程序員秋天
相關(guān)推薦

2022-04-18 10:54:49

券系統(tǒng)緩存 RedisMySQL

2017-12-15 17:37:23

sd

2023-03-15 18:42:10

可裝配優(yōu)惠券系統(tǒng)

2012-07-16 09:48:49

手機優(yōu)惠券優(yōu)惠券

2015-12-11 15:51:18

榮耀

2013-04-07 10:11:26

O2O優(yōu)惠券

2012-04-25 17:11:52

優(yōu)惠券

2023-06-07 08:25:41

2021-11-24 10:31:39

人工智能AI深度學(xué)習(xí)

2021-11-30 22:35:23

人工智能零售業(yè)自動化

2012-03-20 09:37:27

手機優(yōu)惠券NFC

2025-10-27 05:11:00

2011-11-24 09:04:26

靈客風(fēng)優(yōu)惠券

2018-07-06 11:47:31

高德地圖

2021-12-03 10:10:16

價格歧視用戶分級開發(fā)
點贊
收藏

51CTO技術(shù)棧公眾號