在秒殺系統設計中,超賣是一個經典、常見的問題,任何商品都會有數量上限,如何避免成功下訂單買到商品的人數不超過商品數量的上限,這是每個搶購活動都要面臨的難點。
1 超賣問題描述
在多個用戶同時發起對同一個商品的下單請求時,先查詢商品庫存,再修改商品庫存,會出現資源競爭問題,導致庫存的最終結果出現異常。
問題:當商品A一共有庫存15件,用戶甲先下單10件,用戶乙下單8件,這時候庫存只能滿足一個人下單成功,如果兩個人同時提交,就出現了超賣的問題。
可以采用多種方式解決超賣問題。使用synchronized可以保證數據一致性,但是效率低,並且分布式環境下無用;使用數據庫鎖表會造成數據庫性能低下。單體條件下,采用樂觀鎖是比較合適的方式,集群可以考慮分布式鎖。
2 樂觀鎖
2.1 樂觀鎖介紹
悲觀鎖,認為數據很容易被其他線程修改,為保證數據正確性,每次獲取並修改數據時,對數據加鎖。例如Java中的synchronized和Lock相關類。
而樂觀鎖,認為自己在操作時不會有其他線程干擾,所以不對被操作對象加鎖。在更新時會判斷修改期間是否有其他線程修改過。如果沒被修改過,則表示只有當前線程在操作,正常修改數據。如果數據被其他線程修改過,則會停止剛才的更新,選擇執行策略,例如拋棄、報錯、重試等。
樂觀鎖一般使用CAS算法實現。例如Java中的原子類、並發容器。
2.2 沒有鎖的更新操作
樂觀鎖,不是數據庫功能,是一種數據庫實踐。假設進行以下操作:從表中獲取某行數據,計算數據,更新數據該行數據。
CREATE TABLE theTable(
iD int NOT NULL,
val1 int NOT NULL,
val2 int NOT NULL
)
INSERT INTO theTable (iD, val1, val2) VALUES (1, 2 ,3);
沒有鎖的處理
-- 查詢數據
SELECT iD, val1, val2
FROM theTable
WHERE iD = @theId;
-- 計算新值
-- 更新數據
UPDATE
theTable
SET
val1 = @newVal1,
val2 = @newVal2
WHERE
iD = @theId;
-- 繼續執行
2.3 樂觀鎖的實現方式1--條件控制
--查詢數據
SELECT iD, val1, val2
FROM theTable
WHERE iD = @theId;
--計算新值
--更新數據
UPDATE
theTable
SET
val1 = @newVal1,
val2 = @newVal2
WHERE
iD = @theId
AND val1 = @oldVal1
AND val2 = @oldVal2;
--判斷影響行數
-- {if AffectedRows == 1 }
-- {繼續執行}
-- {else}
-- {數據過期}
-- {endif}
上面操作的關鍵在於,UPDATE指令的結構與后續受影響的行數檢查,從而判斷是否有人修改數據。上面所有操作沒有使用事務,這也表明樂觀鎖的關鍵不在於事務本身。
2.4 擴展:事務的使用
--查詢數據
SELECT iD, val1, val2
FROM theTable
WHERE iD = @theId;
--計算新值
--開始事務,更新數據
UPDATE
theTable
SET
val1 = @newVal1,
val2 = @newVal2
WHERE
iD = @theId
AND val1 = @oldVal1
AND val2 = @oldVal2;
--判斷影響行數
-- {if AffectedRows == 1 }
-- COMMIT TRANSACTION; // 提交事務
-- {繼續執行}
-- {else}
-- ROLLBACK TRANSACTION; // 回滾事務
-- {數據過期}
-- {endif}
使用了事務,便可以回滾修改。通過事務,我們可以確定每次回滾的操作量是多少,在何處放置事務邊界以及在何處檢查沖突。
對於其他進程在當前事務提交之前,會發生什么,取決於數據庫當前的隔離級別。以SQL Server為例,其隔離級別是READ_COMMITTED,更新的行被鎖定,直到COMMIT為止,因此“其他進程”無法對該行執行任何操作(保持等待狀態),而SELECT(實際上只能執行READ_COMMITTED) 。
2.5 樂觀鎖的實現方式2--版本號
使用版本號,也是樂觀鎖常用實現方式。通過在表中增加一個version字段:讀取數據時,將version字段值一並讀出,數據更新一次,則version值加1。當我們提交更新時,判斷表中最新的version值與之前讀出的version值是否一致,如果一致,則更新,否則視為過期數據。
--查詢數據
SELECT iD,val1,val2,VERSION
FROM theTable
WHERE iD = @theId;
--計算新值
UPDATE
theTable
SET
val1 = @newVal1,
val2 = @newVal2,
VERSION = VERSION + 1
WHERE
iD = @theId
AND VERSION = @oldversion;
--判斷影響行數
-- {if AffectedRows == 1 }
-- {繼續執行}
-- {else}
-- {數據過期}
-- {endif}
參考資料
https://stackoverflow.com/questions/17431338/optimistic-locking-in-mysql
本文由博客一文多發平台 OpenWrite 發布!