事務傳播行爲引發的問題

事務傳播行爲引發的問題

有了前文的基礎篇對事物傳播行爲和隔離級別有了清楚的認識,我們在看下實戰中會遇到哪些問題。

業務背景:
一個商品可以領取券碼,功能是這樣的,一個商品預先設置庫存總量,並且導入庫存券碼。商品維度的信息和數量是緩存到redis中的,用戶領取的時候將redis中的商品總數減去1,然後進入到詳情頁這個時候有個分配庫存的動作,將庫存改爲狀態已用,領取流水改爲已分配,填充券碼,就如下圖所示:
分配庫存詳情頁
看起來非常簡單,rest請求爲product/detail/{productId}
service邏輯:

@Transaction(propagation = Propagation.REQUIRED)
public Object assignStoreIfNecessary(String uid, Long productId){
    Product product = productService.getProductById(productId);
....
    ProductFlow productFlow = flowService.getFlow(uid,productId);
    if(productFlow.getStatus != 0){
        ....
        return result;
    }
    //加上分佈式鎖分配庫存
    lockTemplate.doBiz(new LockedCallback<Store>() {
            @Override
            public Store callback() {
                Store unusedStore = storeService.findUnusedStore(productId);
                if(unusedStore == null){
                    throw new BusinessException("3","未能獲取到一個庫存 uid="+uid+",productId="+productId);
                }
                flowService.update2Used(unusedStore.getId());
                log.info("uid={},assign storeId={},productId={}",uid,unusedStore.getId(),productId);
                return unusedStore;
            }
        },"assign-store","assign-store"+rightId, "assignstoreKey",4);
}
flow.setStoreId(unusedStore.getId());
flow.setStoreTicketCode(unusedStore.getTicketCode());
flow.setStatus(1);
flowService.update(flow);

獲取商品的券碼SQL執行如下:

SELECT * FROM `store` WHERE `productId` = #{productId}  and `status` =0 ORDER BY `id` LIMIT 1

爲了防止多個線程併發導致,同一個商品不同用戶獲取到相同的券碼我們加入了分佈式鎖,關於分佈式鎖可以參考我之前的文章一行代碼分佈式鎖
在看下更新操作:

@Transactional(propagation = Propagation.REQUIRES)
@Override
 public int update2Used(Long storeId) {
     Assert.notNull(storeId,"update2Used storeId="+storeId);
     return storeMapper.updateStatusById(storeId, 1);
 }

整個的流程就是根據productId和uid獲取一個庫存券碼(這一步是有分佈式鎖),將這個券碼置爲已使用狀態,領取流水的填充庫存和修改狀態,有什麼問題???????

然而線上還真就出現了同一個商品多個用戶分配到了同一個券碼!!!!!

當時懷疑的幾個點:

1)分佈式鎖沒有起到作用,多個線程同時執行一條SQL獲取同一行庫存(同一個券碼)
2)如果鎖沒問題,那麼線程肯定是讀取未提交的數據,查看隔離級別
3)事務傳播(組內一個同事提出的)

拿到分配重複的券碼,去流水錶查看確實有兩個用戶分配了同一個券碼
看了先線上日誌兩個線程獲取鎖的是串行的 第一個釋放鎖,晚於第二個獲取鎖的時間,中間相差幾毫秒,所以分佈式鎖沒有問題1)排除掉了。
查看了線上Mysql隔離級別爲readCommit,所以2)排除了。
只剩下事務傳播了,我們將上面的代碼簡化下:

@Transaction(propagation = Propagation.REQUIRED)
public Object assignStoreIfNecessary(String uid, Long productId){
    before();// (A)
    //lock start
    @Transaction(propagation = Propagation.REQUIRED)
    flowService.update2Used(unusedStore.getId());
    //lock end
    after(); //(B)
}

現如今明顯的問題是,A線程讀取到B線程沒有提交的數據,但是B執行獲取一個商品的券碼並將其設置爲已使用了(lockStart和lockEnd部分的代碼),但是A沒有感知到。所以處理方式就是讓B執行的(lockStart和lockEnd部分的代碼)能讓A感知到。
再回到Propagation.REQUIRED 和Propagation.REQUIRES_NEW的,不熟悉的基礎篇看下,上文的代碼中外層Service使用的Propagation.REQUIRED內層的Service也是Propagation.REQUIRED,沒有開啓使用則創建事務,所以內層事務用的是外層的事務,真個方法全部完成後纔是事務才提交,其他線程才能感知到。

有一種方式讓這個方法加上鎖,真個事務提交,其他線程也就能感知到了,但是這種方式不可取,影響性能。
再有如果能有一種方式讓方法內部B執行的(lockStart和lockEnd)最爲一個獨立的事務,不受外層影響,在配合上分佈式鎖,就可以了。所以只需要做一點改動:

@Transaction(propagation = Propagation.REQUIRED)
flowService.update2Used(unusedStore.getId());
改成
@Transaction(propagation = Propagation.REQUIRES_NEW)
flowService.update2Used(unusedStore.getId());

就可以了,改完線上確實好了。

那麼以前爲什麼沒有碰到類似的事情?service調用service很常見啊,以前也是這樣寫的。
因爲以前寫的都是跟個人有關(每個人都會處理不同的行,併發操作的同一張表的不同行,而且事務是個人的事務,不會有併發–當然多個pc同一個用戶一起訪問除外–不法分子),不像這類多個線程共享的庫存(可同時查詢和修改,非個人級的),對於共享的我們通常是lockStart查詢後修改lockEnd,但是方法如果被外部調用就要注意這個部分要作爲獨立的事務,否則還會出現一行代碼導致看似是讀未提交,實質是因爲查詢修改是所有用戶都能直接訪問共享行)要做獨立的事務的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章