首先環境介紹下:商城商品可能存在幾個端(PC、APP),其次每個端對應的服務端又可能做了負載均衡(即也有多個服務端)。
要實現的目標和功能:保證商品不會出現超賣的情況。超賣商品后,無法對商品進行發貨,是一種不負責任的行為。
方案實現討論流程
“要實現不超賣,首先商品庫存的扣減不能使用框架進行更新,因為框架是設置值,如果在這段時間,又有人購買了,則商品庫存必然會出現問題。要采用手寫SQL方式。並且sql中還要判斷是否大於等於指定的購買量。”
UPDATE `SKU_Info` SET skuNum=skuNum-1000 WHERE id='00293cb7-d8cf-4470-a66d-bb45ca2b130000293cb7-d8cf-4470-a66d-bb45ca2b1300' AND skuNum>=1000;
“要實現不超賣,我們可以對方法加上同步鎖,這樣可以解決”。
“方法加上同步鎖后,用戶下單將會出現排隊的情況,性能有問題。”
“那我們可以實現對同一商品進行加鎖,這樣可以解決購買不同商品不會相互阻塞。如果有包含關系,也應該加鎖。比如A用戶購買商品1和商品2,B用戶購買商品1,因為他們都有商品1,則應該加鎖。”
“你這個方案應該可以解決問題,采用分布式鎖的方式可以解決,我們可以使用redis來做。”
“是的,確實可以解決問題,並且多個服務端也不存在問題了,就這么干。”
“我們可以對訂單中的所有商品的sku值進行排序,拼接成一個skuId值,然后MD5的值作為key,其它訂單進來方法時,按同樣的操作進行檢測是否正在下單,如果是,則等待。”
“你這種方案忽略了商品不同的情況,就比如上面的例子中,A購買商品1和商品2,B購買商品1,那么他們的key是不同的,因而達不到效果。”
“我們可以對每個商品sku的id定義個鎖,這樣每次購買時,我們針對每個商品進行檢測,這樣就可以了,絕對能夠保證同步。”
“這種方法可行,不過還是存在一個問題,服務端與redis的連接次數會比較多,如果一個用戶下單商品種類較多,那么仍然會比較慢,但這確實不失為一個好的方案。”
“既然這個方案仍然有可能有問題,那么還有沒有其它的方案。”
“數據庫本身是有鎖的,可以實現鎖同步的問題,那么有沒有辦法使用到數據庫的鎖來解決這個問題?”
“對呀,我們可以寫SQL語句去循環扣減庫存,最后判斷數據庫影響行數與商品種類是否匹配?如果不匹配,則是扣減失敗,進行還原,如果匹配,則扣減成功!”
“經過測試,我們用的MySQL不支持這種方案,里面需要用到if判斷,而if判斷必須要在存儲過程中才能使用。”
“那我們可以使用存儲過程來做。代碼如下”
DELIMITER $$ USE anke_skucenter$$ CREATE PROCEDURE minusSkuNum() BEGIN SET AUTOCOMMIT=0; START TRANSACTION; UPDATE SKU_Info SET skuNum=skuNum-100 WHERE id='0031394c-8058-49f5-9ba9-f971480ac2f2' AND skuNum>=100; IF(SELECT ROW_COUNT()<=0)THEN ROLLBACK; END IF; UPDATE `SKU_Info` SET skuNum=skuNum-1000 WHERE id='00293cb7-d8cf-4470-a66d-bb45ca2b130000293cb7-d8cf-4470-a66d-bb45ca2b1300' AND skuNum>=1000; IF(SELECT ROW_COUNT()<=0)THEN ROLLBACK; END IF; COMMIT; SET AUTOCOMMIT=1; END$$
“這個是初步的存儲過程,仍然需要將update語句變更為循環,改變傳入參數為商品id和數量,有誰會寫?”
“額,目前大家都不會寫,並且這個循環看上去也挺復雜的。”
“那么我們能不能在mybatis中獲取多條更新語句的影響行數?”
“不能,沒有任何框架支持,並且mysql本身就不支持,要不然也不會需要存儲過程了。”
“既然多條SQL不行,能不能放到一條SQL中去做更新呢?”
“先baidu下”
“哈哈,找到了,我們查詢的時候有時候回用到case when,那么我們更新的時候是否可以使用這個呢?嘗試代碼如下:”
update SKU_Info set skuNum=skuNum-(case when id='0031394c-8058-49f5-9ba9-f971480ac2f2' then 100 when id='00293cb7-d8cf-4470-a66d-bb45ca2b1300' then 1000 end) where (id='0031394c-8058-49f5-9ba9-f971480ac2f2' AND skuNum>=100) or (id='00293cb7-d8cf-4470-a66d-bb45ca2b1300' AND skuNum>=1000);
“經過測試,該段代碼執行正常,並且能夠正常返回需要的影響行數。”
“這個循環的SQL編寫在mybatis中不難,那么判斷最后扣減結果與商品種類不同時,如何進行補償呢?”
“這個可以使用@Transactional,我們在方法上加此注解,在方法內部判斷,如果不同,我們就拋出一個自定義異常,這樣就會自動進行回滾了。”
“測試一下”
“經過幾輪測試,確實可行,就這樣做。”
“具體實施為:先生成訂單,然后進行扣減,如果捕獲到扣減失敗的自定義異常,則對生成的訂單執行刪除標記。但存在一個問題,就是標記訂單為刪除狀態失敗的情況,這個訂單仍然存在,也是超賣了。”
“可以調整下,改為先進行扣減,扣減成功再生成訂單,這樣可以避免此問題。”
“嗯,此方法可以解決超賣問題,可能會存在商品扣減成功,但訂單未生成的情況。”
“這種問題會存在,但比超賣要好很多。”
“嗯嗯”
“嗯嗯”