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