日活3kw的實(shí)際庫存業(yè)務(wù)場(chǎng)景中的超賣到底怎么解決的
這個(gè)問題其實(shí)可以說是隨便一百度幾乎可以出來全是解決方案,其實(shí)超賣問題在實(shí)際業(yè)務(wù)場(chǎng)景中是十分復(fù)雜的。沒有什么絕對(duì)的解決方案。都是因人而異的。
"超賣"是指商品售出數(shù)量超過實(shí)際庫存量的情況。通常在處理商品庫存扣減時(shí),我們會(huì)先檢查庫存是否充足,如果足夠則進(jìn)行扣減,否則直接返回下單失敗。
然而,在高并發(fā)環(huán)境下,可能出現(xiàn)以下情形:
在高并發(fā)情況下,當(dāng)兩個(gè)并發(fā)線程同時(shí)查詢庫存時(shí),假設(shè)數(shù)據(jù)庫中庫存僅剩1個(gè),兩個(gè)線程都獲得了1的庫存量。在經(jīng)過庫存校驗(yàn)后,它們分別開始執(zhí)行庫存扣減操作,最終導(dǎo)致庫存變成負(fù)數(shù)。
這種情況是高并發(fā)環(huán)境下典型的超賣問題。
超賣問題的根源在于并發(fā)操作,因此解決超賣問題實(shí)質(zhì)上就是解決并發(fā)問題。在上述情況中,關(guān)鍵在于確保庫存扣減過程的原子性和有序性。
- 原子性指的是庫存查詢、庫存判斷和庫存扣減這一系列操作作為一個(gè)不可分割的整體,不會(huì)被中斷,也不會(huì)被其他線程同時(shí)執(zhí)行。這確保了操作的完整性和一致性。
- 有序性則要求多個(gè)并發(fā)操作按照一定的順序執(zhí)行,避免出現(xiàn)競爭條件,從而保證數(shù)據(jù)的準(zhǔn)確性和正確性。
通過確保庫存扣減操作的原子性和有序性,可以有效解決高并發(fā)環(huán)境下的超賣問題,保障系統(tǒng)的穩(wěn)定性和可靠性。
一、實(shí)現(xiàn)方案:數(shù)據(jù)庫
從三個(gè)角度考慮實(shí)現(xiàn):
- 數(shù)據(jù)庫層面的悲觀鎖
- 數(shù)據(jù)庫層面的樂觀鎖
- 依賴數(shù)據(jù)庫執(zhí)行引擎的順序執(zhí)行機(jī)制
以上三個(gè)角度簡單來說:在處理庫存扣減時(shí),常見的方法是通過數(shù)據(jù)庫操作實(shí)現(xiàn)。確保操作的原子性和有序性通常可以通過加鎖實(shí)現(xiàn),無論是悲觀鎖還是樂觀鎖都可以達(dá)到這個(gè)目的。
1.悲觀鎖的實(shí)現(xiàn)方式
-- 開始事務(wù)
BEGIN;
-- 查詢商品信息并加鎖
SELECT quantity FROM items WHERE id = 1 FOR UPDATE;
-- 修改商品數(shù)量為2
UPDATE items SET quantity = 2 WHERE id = 1;
-- 提交事務(wù)
COMMIT;
注意:
在前述討論中,我們提到了使用SELECT...FOR UPDATE會(huì)對(duì)數(shù)據(jù)進(jìn)行鎖定,但需要注意鎖的級(jí)別。在 MySQL InnoDB 中,默認(rèn)使用行級(jí)鎖。行級(jí)鎖是基于索引的,如果一條 SQL 語句沒有使用索引,那么不會(huì)使用行級(jí)鎖,而會(huì)使用表級(jí)鎖將整個(gè)表鎖定。因此,這一點(diǎn)需要引起注意。
然而,使用悲觀鎖可能會(huì)導(dǎo)致請(qǐng)求阻塞和排隊(duì),在高并發(fā)情況下可能對(duì)數(shù)據(jù)庫造成負(fù)擔(dān)。樂觀鎖則通過版本號(hào)等方式控制順序執(zhí)行,但在高并發(fā)環(huán)境下可能會(huì)出現(xiàn)大量失敗操作,不適合高并發(fā)場(chǎng)景,因?yàn)樵诟逻^程中也需要加行級(jí)鎖,可能會(huì)導(dǎo)致阻塞。
2.樂觀鎖的實(shí)現(xiàn)方式
在MySQL中,樂觀鎖主要通過CAS(Compare and Swap)機(jī)制來實(shí)現(xiàn),通常通過版本號(hào)來實(shí)現(xiàn)。CAS是一種樂觀鎖技術(shù),當(dāng)多個(gè)線程嘗試使用CAS同時(shí)更新同一個(gè)變量時(shí),只有其中一個(gè)線程能成功更新變量的值,而其他線程會(huì)失敗。失敗的線程不會(huì)被掛起,而是會(huì)被告知在這次競爭中失敗,并可以再次嘗試。
舉例來說,對(duì)于之前提到的庫存扣減問題,通過樂觀鎖可以實(shí)現(xiàn)以下操作:
//查詢出商品信息,quantity = 3
select quantity from items where id=1
//根據(jù)商品信息生成訂單
//修改商品quantity為2
update items set quantity=2 where id=1 and quantity = 3;
盡管如此,即使不使用鎖也是可行的??梢砸蕾嚁?shù)據(jù)庫執(zhí)行引擎的順序執(zhí)行機(jī)制,只需確保庫存不會(huì)變?yōu)樨?fù)數(shù)。這種情況下,可以通過巧妙設(shè)計(jì)的SQL語句來實(shí)現(xiàn)操作的原子性和有序性。
3.數(shù)據(jù)庫執(zhí)行引擎的實(shí)現(xiàn)方式
舉例來說,假設(shè)有一張名為"inventory"的表,其中包含"product_id"和"stock"字段,可以通過以下SQL語句來實(shí)現(xiàn)庫存扣減:
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 'your_product_id' AND stock > 0;
這樣的SQL語句能夠確保在庫存大于0的情況下進(jìn)行扣減,避免庫存變?yōu)樨?fù)數(shù)。通過這種方式,可以在不加鎖的情況下有效地管理庫存扣減操作。
有人可能會(huì)覺得數(shù)據(jù)庫執(zhí)行引擎的實(shí)現(xiàn)方式挺好的。然而,這種解決方案并不理想。實(shí)際上,這種方式與樂觀鎖方案的缺點(diǎn)相同,都完全依賴于數(shù)據(jù)庫。在高并發(fā)情況下,多個(gè)線程同時(shí)更新庫存時(shí)可能會(huì)導(dǎo)致阻塞。這不僅會(huì)導(dǎo)致操作速度變慢,還可能給數(shù)據(jù)庫帶來壓力。
通常情況下,MySQL的熱點(diǎn)行更新最多也只能承受200-300個(gè)并發(fā)更新。如果需要更高的并發(fā)處理能力,一種方法是提升硬件水平,另一種方法是進(jìn)行一些技術(shù)改造,比如采用inventory hint的方式。
說到這里,數(shù)據(jù)庫層面的超賣的解決實(shí)現(xiàn)方案也就聊的差不多了。
二、實(shí)現(xiàn)方式:Redis
我們可以利用Redis的單線程執(zhí)行特性,結(jié)合Lua腳本執(zhí)行過程中的原子性保障,實(shí)現(xiàn)庫存扣減操作。通過在Redis中使用如下Lua腳本:
local key = KEYS[1] -- 商品的鍵名
-- 獲取商品當(dāng)前的庫存量
local remaining_stock = tonumber(redis.call("GET", key))
local quantity_to_reduce = tonumber(ARGV[1]) -- 扣減的數(shù)量
-- 如果庫存足夠,則減少庫存并返回新的庫存量
if remaining_stock >= quantity_to_reduce then
redis.call("DECRBY", key, quantity_to_reduce)
return "Stock reduced successfully"
else
return "Insufficient stock"
end
通過先從Redis中獲取當(dāng)前剩余庫存,然后進(jìn)行足夠性檢查并執(zhí)行扣減操作,可以有效避免并發(fā)問題。由于Lua腳本在執(zhí)行過程中不會(huì)被中斷,且Redis是單線程執(zhí)行的,因此在腳本中進(jìn)行這些操作可以確保原子性和有序性。這種方法結(jié)合了Redis的高性能和分布式緩存特性,使得使用Lua腳本扣減庫存非常高效。
三、我們實(shí)際項(xiàng)目中如何處理超賣的
在實(shí)際應(yīng)用中,通常會(huì)結(jié)合使用數(shù)據(jù)庫和Redis兩種方案來實(shí)現(xiàn)庫存扣減操作。一種常見的做法是首先利用Redis進(jìn)行扣減操作以應(yīng)對(duì)高并發(fā)流量,然后將扣減結(jié)果同步到數(shù)據(jù)庫中,實(shí)現(xiàn)扣減并進(jìn)行持久化存儲(chǔ),以防止Redis宕機(jī)導(dǎo)致數(shù)據(jù)丟失。
具體流程如下:
- 首先在Redis中進(jìn)行庫存扣減操作,然后發(fā)送一個(gè)消息到消息隊(duì)列(MQ)。
- 消費(fèi)者接收到消息后,執(zhí)行數(shù)據(jù)庫中的真正庫存扣減以及其他業(yè)務(wù)邏輯操作。
Redis扣減操作示例代碼:
可以使用上述提到的Lua腳本方式,或者采用如下Redisson方式
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class InventoryConsumer {
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("inventory_lock");
try {
lock.lock();
// 執(zhí)行庫存扣減及其他業(yè)務(wù)邏輯操作
// 例如:更新數(shù)據(jù)庫中的庫存信息
// 注意:在鎖內(nèi)執(zhí)行扣減操作
} finally {
lock.unlock();
redisson.shutdown();
}
}
}
消費(fèi)者示例代碼:
public class InventoryConsumer {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/database", "user", "password");
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
// 在收到消息時(shí)執(zhí)行數(shù)據(jù)庫操作,進(jìn)行庫存扣減及其他業(yè)務(wù)邏輯
try {
Statement stmt = conn.createStatement();
stmt.executeUpdate("UPDATE inventory SET stock = stock - 1 WHERE product_id = '123'");
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
}, "inventory_update");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
jedis.close();
}
}
}
通過結(jié)合使用Redis和數(shù)據(jù)庫,可以充分發(fā)揮它們各自的優(yōu)勢(shì),實(shí)現(xiàn)高效的庫存管理并確保數(shù)據(jù)的一致性和持久性。
這種方法確實(shí)有助于確保Redis中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)最終保持一致,同時(shí)也有助于避免超賣的情況發(fā)生。然而,存在一個(gè)潛在問題,即可能導(dǎo)致少賣的情況發(fā)生。
四、少賣的解決方案
在上述流程中,如果第一步成功執(zhí)行,導(dǎo)致Redis中的庫存成功扣減,但隨后的第二步消息未能成功發(fā)送,或者在后續(xù)消費(fèi)過程中消息丟失或失敗,就可能出現(xiàn)Redis中庫存減少而數(shù)據(jù)庫庫存未減少的情況,從而導(dǎo)致實(shí)際業(yè)務(wù)操作未能發(fā)生。這種情況會(huì)導(dǎo)致Redis中出現(xiàn)多扣的情況,進(jìn)而引發(fā)少賣的問題。
為了解決這類問題,需要引入一種對(duì)賬機(jī)制,實(shí)施準(zhǔn)實(shí)時(shí)核對(duì),及時(shí)發(fā)現(xiàn)并處理這類情況。如果發(fā)現(xiàn)存在較大的少賣問題,需要將這些庫存重新添加回去。
在許多成熟的電商公司中,無論之前的方案多么完善,這種對(duì)賬系統(tǒng)都是不可或缺的。及時(shí)進(jìn)行核對(duì),發(fā)現(xiàn)超賣、少賣等問題至關(guān)重要。
一個(gè)簡單的示例是,在消費(fèi)者處理消息時(shí),記錄每次庫存變化的日志,包括扣減和增加操作,然后定期對(duì)比Redis中的庫存和數(shù)據(jù)庫中的庫存,檢查是否存在不一致的情況。如果發(fā)現(xiàn)多扣或少賣的情況,可以根據(jù)日志記錄進(jìn)行修正。
這種對(duì)賬機(jī)制可以幫助保證系統(tǒng)的數(shù)據(jù)一致性,并及時(shí)發(fā)現(xiàn)并糾正潛在的問題,確保業(yè)務(wù)操作的準(zhǔn)確性和穩(wěn)定性。
綜上所述可得沒有完美的解決方案,引入新的中間件總會(huì)面臨的的問題。這就需要根據(jù)實(shí)際業(yè)務(wù)進(jìn)行權(quán)衡了。