秒殺實現通過樂觀鎖控制超賣問題
通過悲觀鎖控制每個用戶只能下一單,避免用戶多次點擊,發送的多次下單請求(即多個線程)都成功,避免惡意攻擊
????????????????????????每個請求訪問Tomcat時,就會分配一個線程處理請求
業務邏輯:
注*以下邏輯中報錯也可以改為給前端返回錯誤信息
1.檢查數據庫查看庫存是否>0,不滿足直接報錯
2.當數據庫庫存>0,檢查該用戶對本商品的訂單是否存在,如果已存在說明下過單,報錯
3.如果訂單也不存在,對數據庫數據進行減庫存,這里容易出現超賣,使用樂觀鎖
4.減庫存后創建訂單,這個過程和2步驟有關聯,應該使用悲觀鎖控制2-4步驟的訪問
樂觀鎖(本質sql的where語句驗證):即修改數據庫前查看數據前后是否一致,不一致說明中間有其他線程修改數據,則放棄修改。
update product set sale=slae-1 where sale=?
//?為檢查數據庫庫存是否>0時查詢的值,這樣在修改數據時通過where在驗證一遍數據是否被修改
缺點:高并發場景下修改數據庫成功率太低!
優化:寬松的樂觀鎖,嚴格來說不算樂觀鎖了,就是條件更改
update product set sale=slae-1 where sale>0
將條件變為sale>0哪怕中間有其他線程進行減庫存,只要數量依然>0依然允許修改,這樣就可以完美符合我們減庫存的預期(可以使用Jmeter工具進行高并發測試)
悲觀鎖(本質synchronized同步機制):避免同一個用戶對創建訂單接口訪問進行多次請求時,多次請求都創建訂單成功。如果不是同一用戶,允許異步訪問數據庫創建多個不同用戶訂單
1.應該將2-4步驟抽離出單獨放到一個方法里面,因為減庫存和創建訂單是一個事務,應該將其放到同一事務中執行
2.同步鎖不建議直接加到2-4步驟的方法上面,因為方法同步鎖,鎖對象都是this,如果該類中有多個鎖的對象都是this,容易照成線程的過度阻塞,導致程序反應變慢
所以對象鎖不應該綁定this而是和userID綁定,因此就要使用同步代碼塊進行上鎖,鎖對象都為和userID掛鉤的同一種對象即可-----這里使用userID.toString().intern()來指定鎖對象
2.1為什么使用userID.toString().intern()來充當鎖對象:
如果直接使用Integer類型的userID參數對象,來作為鎖對象毫無意義,因為不同請求傳遞的參數都會在堆中new出不同的對象,這樣哪怕是多個線程(請求)訪問接口,傳遞的同一userID同步鎖也無法限制他們進行同步訪問代碼塊,因為鎖對象根本不是同一對象
直接使用this也不行,因為我們只需要限制參數為同一userID的請求訪問代碼塊時要同步,不同userID的請求之間不需要使用同一把鎖限制他們。而是使用多把鎖,限制同一userID的請求進行同步訪問代碼塊2-4步驟,多把鎖允許不同userID的請求可以異步訪問訪問代碼塊2-4步驟。如果使用了this,所有請求訪問代碼塊2-4步驟時都會同步訪問,不符合我們預期
3.這個同步代碼塊不應該封裝到2-4步驟的事務方法中去,而是封裝到調用該方法的位置
如果封裝到2-4步驟的事務方法中,同一userID線程雖然訪問代碼塊被同步限制了,但是除了代碼塊后事務還沒有真正提交,這時其他同一userID的線程又進入代碼塊中,查詢數據庫中有無訂單時,可能沒有訂單,因為方法沒執行完畢,事務未提交,數據庫中還沒有訂單信息。
所以為了避免這種情況,代碼塊應該封裝到調用該事務方法的地方,將該事務方法放到代碼塊中執行
4.@Transaction事務失效
上面我們說應該在其他方法中調用2-4步驟的事務方法,但是同一類中調用事務方法,事務不會生效,只有通過spring創建的代理對象,引用事務方法時事務才會生效.
所以在引用事務方法時要通過spring框架的ApplicationContext對象的getBean(類名.class)來獲取代理對象調用事務方法,這樣事務才會生效!!
代碼實現:(僅作參考,結合業務邏輯食用)
@AutowiredApplicationContext context;//模仿秒殺減庫存,創建訂單@Overridepublic Boolean killInSecond(Integer userID,Integer productID){//檢查庫存是否>0Product product = pm.selectByPrimaryKey(productID);if(product.getSales()<=0){throw new MyExceptionHandler("庫存不足");}//調用2-4步驟方法Boolean result=false;synchronized (userID.toString().intern()){//使用代理對象調用事務方法ProductServiceImpl bean = context.getBean(ProductServiceImpl.class);result=bean.ProductAndOrder(userID,productID);}return result;}@AutowiredOrderMapper om;@AutowiredRedisIdIncrement redisId;//redis全局唯一ID生成工具,想省事可以直接uuid//創建訂單,減庫存操作@Transactionalpublic Boolean ProductAndOrder(Integer userID,Integer productID){//檢查數據庫中書否存在該用戶訂單Integer orderCount = om.selectOrderByUserIdAndProductId(userID, productID);if(orderCount>0){throw new MyExceptionHandler("用戶已下單");}//訂單不存在減庫存,寬松樂觀鎖Integer result = pm.updateProductBysale(productID);if(result!=1){throw new MyExceptionHandler("庫存不足");}//創建訂單//獲取redis唯一IDLong orderId = redisId.getRedisID("order");//封裝訂單Order order=new Order(orderId.toString(),userID,"","",productID,"",null,1,0,null,null,null,null,new BigDecimal(100));result = om.insertCompleteOrder(order);if(result!=1){return false;}return true;}
mybatisXML文件的樂觀鎖sql語句
<!-- 對單個商品減庫存--><update id="updateProductBysale">update product set sales=sales-1 where id=#{productId} and sales>0</update>