訂單管理是電商項目中的重點業務邏輯:
1.訂單表
order_id 訂單主鍵
username
order_num 訂單編號
payment 支付方式
pay_platform
delivery 送貨方式
is_confirm 送貨前確認電話
order_sum
ship_fee 是否付款
order_state
payment_cash 貨到付款方式
distri_id 配送商id
delivery_method 送貨方式
payment_no 支付號
order_time 下單時間
pay_time 付款時間
deposit_time 到賬時間
success_time 成功時間
update_time 最后修改時間 例如地址寫錯修改之類的
srv_type 業務類型 0 無業務 1 2 需要辦理CRM業務
is_deleted 刪除
is_call 是否外呼過 催付款 0 未外呼 1 已外呼
delivery_no 物流編號
is_plant 發票是否打印
收貨地址等信息字段
2.訂單明細
order_DETAIL_ID
order_id
item_id 商品主鍵
item_name
item_no 商品編號
sku_id
sku_spec 規格值
market_price
sku_price
quantity 購買數量
活動之類的信息
活動營銷之類的
3.存在的問題:
(3.1)並發問題
如果客戶A和客戶B同時購買某一個商品的同一個skuid,A購買2個,B購買3個,如果庫存是100
<1>EbSku sku = skuDao.getSkuById();
<2>sku.setStockInventory(sku.getStockInventory()-detail.getQuantity());
<3>skuDao.update(sku);
skuid stock
1001 100
A 1001(2) 98
B 1001(3) 97
假如A和B同時執行第1行代碼,A和B查詢上來的數據完全相同,在相同的數據基礎上來修改一定會產生並發的問題。
如果<1>加鎖那么AB查出來是不同的,也就不會產生並發問題。
如果查詢的時候使用 select * from sku where sku_id = ? for update 加上for update的話會開啟事務,事務掛起,如果同時另一個窗口查詢同樣的sql,需要等到第一條sql提交之后才可以;所以加for update 可以解決加鎖的問題,這就是herbnate的悲觀鎖。
解決辦法:
1.)可使用數據庫的悲觀鎖,在第<1>行的代碼的查詢上加上for update會開啟事務,由於spring的傳播特性,每一個提交訂單的操作都是用的一個事務,如果A和B對同一條數據操作,當執行根據skuid查詢sku對象的時候一定有一個人被阻塞在外,直到update代碼執行完畢另一個才能得到執行,這樣可以保證我們的數據安全。但是帶來的問題就是性能低,對於互聯網項目要求性能高的此種方式不適合。
2.)樂觀鎖:version控制並發的字段
skuid stock version
默認值 1001 100 1
A(2) 1001
B(3)
update所對應的sql
A:
update sku s set s.stock = 98 ,t.version = s.version+1 where sku_id = 1001 and t.version = 1
and t.stock >2
B;
update sku s set s.stock = 97 ,t.version = s.version+1 where sku_id = 1001 and t.version = 1
and t.stock >3
以上AB同時執行的時候,修改庫存的時候由於查詢上來的數據完全相同所以執行的sql傳遞過來的數據也相同,A和B誰先執行有隨機性,假設A先執行由於A和B對同一條數據在修改,那么B一定被阻塞在外,A提交事務后B才能得到執行,B的update的相應條數是0,相當於沒有得到執行,那么B執行的條件需要做更改:
導致相應條數是0有2個原因:
1>version 並發問題導致
2>stock導致的 可能庫存不夠了
update sku s set s.stock = 97 ,t.version = s.version+1 where sku_id = 1001 and t.version = 1
and t.stock >2
目前采用先查詢后修改的方式:
public void saveOrder(EbOrder order,List<EbOrderDetail> detailList){
orderDao.save(order);
for(EbOrderDetail detail : detailList){]
detail.setOrderId(order.getOrderId());
orderDetailDao.saveOrderDetail(detail);
//扣減庫存
EbSku sku = skuDao.getSkuById(detail.getSkuId());
sku.setStockInventory(sku.getStockInventory()-detail.getQuantity());
int flag = skuDao.update(sku);
if(flag == 0){
//一旦出現flag為0,那么就是出問題了,那么這個訂單不能提交了,訂單不讓入庫了,那么當前整個事務回滾;如何實現呢?拋出運行時異常,那么整個事務就回滾了;
//問題來了 拋出異常如何查看是version還是stock的問題?
EbSku sku = skuDao.getSkuById(detail.getSkuId());
if(sku.getStock() < detail.getQuantity()){
throw new 自定義異常;
}else{
//並發問題引起的
throw new 並發引起的自定義異常
}
}
//redis不受事務控制 對redis中的數據進行更新處理
}
}
在外層調用saveOrder()處
try{
orderService.saveOrder(order,detailList)
}catch(Exceptione){
if(e instanceof EbStockException){
model.addAttribute("tip","stock error")
}else if(e instanceof 自定義的版本號異常){
orderService.saveOrder(order,detailList);//當版本號問題的時候重新提交就可以了,但是如果重新提交再次發生並發那依然還是上次分析流程
}
}
分析:
其實在上述代碼中可以不查詢直接修改,但是在一些比較復雜的項目中,有些數據必須先查詢出來處理其他業務,本項目中可以直接進行修改
如果業務不是必須要查詢再修改就盡量不要先查詢后修改
skuDao.update(sku);
update sku t set t.stock = t.stock - #{quantity} where t.sku_id = #{skuId} and t.stock >= #{quantity}
此種情況一旦出現上述flag ==0 的情況 那一定是stock庫存的問題
訂單的流程