糟糕,線(xiàn)上庫(kù)存竟然變成負(fù)500......
前言
"快看我們的秒殺系統(tǒng)!庫(kù)存顯示-500了!"
3年前的這個(gè)電話(huà)讓我記憶猶新。
當(dāng)時(shí)某電商大促,我們自認(rèn)為完美的分布式架構(gòu),在0點(diǎn)整瞬間被擊穿。
數(shù)據(jù)庫(kù)連接池耗盡,庫(kù)存表出現(xiàn)負(fù)數(shù),客服電話(huà)被打爆...
今天這篇文章跟大家一起聊聊商品超賣(mài)的問(wèn)題,希望對(duì)你會(huì)有所幫助。
1.為什么會(huì)發(fā)生超賣(mài)?
首先我們一起看看為什么會(huì)發(fā)送超賣(mài)?
1.1 數(shù)據(jù)庫(kù)的"最后防線(xiàn)"漏洞
我們用下面的列子,給大家介紹一下商品超賣(mài)是如何發(fā)生的。
public boolean buy(int goodsId) {
// 1. 查詢(xún)庫(kù)存
int stock = getStockFromDatabase(goodsId);
if (stock > 0) {
// 2. 扣減庫(kù)存
updateStock(goodsId, stock - 1);
return true;
}
return false;
}
在并發(fā)場(chǎng)景下可能變成下圖這樣的:
圖片
請(qǐng)求1和請(qǐng)求2都將庫(kù)存更新成9。
根本原因:數(shù)據(jù)庫(kù)的查詢(xún)和更新操作,不是原子性校驗(yàn),多個(gè)事務(wù)可能同時(shí)通過(guò)stock>0的條件檢查。
1.2 超賣(mài)的本質(zhì)
商品超賣(mài)的本質(zhì)是:多個(gè)請(qǐng)求同時(shí)穿透緩存,同一時(shí)刻讀取到相同庫(kù)存值,最終在數(shù)據(jù)庫(kù)層發(fā)生覆蓋。
就像100個(gè)人同時(shí)看上一件衣服,都去試衣間前看了眼牌子,出來(lái)時(shí)都覺(jué)得自己應(yīng)該拿到那件衣服。
2.防止超賣(mài)的方案
2.1 數(shù)據(jù)庫(kù)樂(lè)觀(guān)鎖
數(shù)據(jù)庫(kù)樂(lè)觀(guān)鎖的核心原理是通過(guò)版本號(hào)控制并發(fā)。
例如下面這樣的:
UPDATE product
SET stock = stock -1, version=version+1
WHERE id=123 AND version=#{currentVersion};
Java的實(shí)現(xiàn)代碼如下:
@Transactional
public boolean deductStock(Long productId) {
Product product = productDao.selectForUpdate(productId);
if (product.getStock() <= 0) return false;
int affected = productDao.updateWithVersion(
productId,
product.getVersion(),
product.getStock()-1
);
return affected > 0;
}
基于數(shù)據(jù)庫(kù)樂(lè)觀(guān)鎖方案的架構(gòu)圖如下:
圖片
優(yōu)缺點(diǎn)分析:
優(yōu)點(diǎn) | 缺點(diǎn) |
無(wú)需額外中間件 | 高并發(fā)時(shí)DB壓力大 |
實(shí)現(xiàn)簡(jiǎn)單 | 可能出現(xiàn)大量更新失敗 |
適用場(chǎng)景:日訂單量1萬(wàn)以下的中小系統(tǒng)。
2.2 Redis原子操作
Redis原子操作的核心原理是使用:Redis + Lua腳本。
核心代碼如下:
// Lua腳本保證原子性
String lua = "if redis.call('get', KEYS >= ARGV[1] then " +
"return redis.call('decrby', KEYS[1], ARGV " +
"else return -1 end";
public boolean preDeduct(String itemId, int count) {
RedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(itemId), count);
return result != null && result >= 0;
}
該方案的架構(gòu)圖如下:
圖片
性能對(duì)比:
- 單節(jié)點(diǎn)QPS:數(shù)據(jù)庫(kù)方案500 vs Redis方案8萬(wàn)
- 響應(yīng)時(shí)間:<1ms vs 50ms+
2.3 分布式鎖
目前最常用的分布式鎖的方案是Redisson。
下面是Redisson的實(shí)現(xiàn):
RLock lock = redisson.getLock("stock_lock:"+productId);
try {
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
// 執(zhí)行庫(kù)存操作
}
} finally {
lock.unlock();
}
注意事項(xiàng)
1.鎖粒度要細(xì)化到商品級(jí)別
2.必須設(shè)置等待時(shí)間和自動(dòng)釋放
3.配合異步隊(duì)列使用效果更佳
該方案的架構(gòu)圖如下:
圖片
2.4 消息隊(duì)列削峰
可以使用 RocketMQ的事務(wù)消息。
核心代碼如下:
// RocketMQ事務(wù)消息示例
TransactionMQProducer producer = new TransactionMQProducer("stock_group");
producer.setExecutor(new TransactionListener() {
@Override
public LocalTransactionState executeLocalTransaction(Message msg) {
// 扣減數(shù)據(jù)庫(kù)庫(kù)存
return LocalTransactionState.COMMIT_MESSAGE;
}
});
該方案的架構(gòu)圖如下:
圖片
技術(shù)指標(biāo):
- 削峰能力:10萬(wàn)QPS → 2萬(wàn)TPS
- 訂單處理延遲:<1秒(正常時(shí)段)
2.5 預(yù)扣庫(kù)存
預(yù)扣庫(kù)存是防止商品超賣(mài)的終極方案。
核心算法如下:
// Guava RateLimiter限流
RateLimiter limiter = RateLimiter.create(1000); // 每秒1000個(gè)令牌
public boolean preDeduct(Long itemId) {
if (!limiter.tryAcquire()) return false;
// 寫(xiě)入預(yù)扣庫(kù)存表
preStockDao.insert(itemId, userId);
return true;
}
該方案的架構(gòu)圖如下:
圖片
性能數(shù)據(jù):
- 百萬(wàn)級(jí)并發(fā)支撐能力
- 庫(kù)存準(zhǔn)確率99.999%
- 訂單處理耗時(shí)200ms內(nèi)
3.避坑指南
3.1 緩存與數(shù)據(jù)庫(kù)不一致
某次大促因緩存未及時(shí)失效,導(dǎo)致超賣(mài)1.2萬(wàn)單。
錯(cuò)誤示例如下:
// 錯(cuò)誤示例:先刪緩存再寫(xiě)庫(kù)
redisTemplate.delete("stock:"+productId);
productDao.updateStock(productId, newStock); // 存在并發(fā)寫(xiě)入窗口
3.2 未考慮庫(kù)存回滾
秒殺取消后,忘記恢復(fù)庫(kù)存,引發(fā)后續(xù)超賣(mài)。
正確做法是使用事務(wù)補(bǔ)償。
例如下面這樣的:
@Transactional
public void cancelOrder(Order order) {
stockDao.restock(order.getItemId(), order.getCount());
orderDao.delete(order.getId());
}
庫(kù)存回滾和訂單刪除,在同一個(gè)事務(wù)中。
3.3 鎖粒度過(guò)大
鎖粒度過(guò)大,全局限流導(dǎo)致10%的請(qǐng)求被誤殺。
錯(cuò)誤示例如下:
// 錯(cuò)誤示例:全局限鎖
RLock globalLock = redisson.getLock("global_stock_lock");
總結(jié)
其實(shí)在很多大廠(chǎng)中,一般會(huì)將防止商品超賣(mài)的多種方案組合使用。
架構(gòu)圖如下:
圖片
通過(guò)組合使用:
- Redis做第一道防線(xiàn)(承受80%流量)
- 分布式鎖控制核心業(yè)務(wù)邏輯
- 預(yù)扣庫(kù)存+消息隊(duì)列保證最終一致性
實(shí)戰(zhàn)經(jīng)驗(yàn):某電商在2023年雙11中:
- Redis集群承載98%請(qǐng)求
- 分布式鎖攔截異常流量
- 預(yù)扣庫(kù)存保證最終準(zhǔn)確性
系統(tǒng)平穩(wěn)支撐了每秒12萬(wàn)次秒殺請(qǐng)求,0超賣(mài)事故發(fā)生!
記?。簺](méi)有銀彈方案,只有適合場(chǎng)景的組合拳!