高並發下Service層的寫法


最近在項目里遇到一個坑,先上簡易版的描述:每次從庫里查詢一下庫存余量,每次購買一個商品。

數據庫:

store為庫存量。

service層代碼:

@Override
    public synchronized void sell() {
        System.out.println("<======"+System.currentTimeMillis());
        
        //根據局id獲取商品信息
        Goods goods = goodDao.findOne(1);
        //獲取當前庫存
        int store = goods.getStore();
        System.out.println(Thread.currentThread().getName()+" begin:"+store);
        
        
        if(store - 1 >= 0) {
            store = store -1;
            goods.setStore(store);
            //save當前余量
            Goods save = goodDao.save(goods);
            System.out.println(Thread.currentThread().getName()+" end:"+save.getStore());
        }
        System.out.println(System.currentTimeMillis()+"========>");
        
    }

在這段代碼里,因為加了synchronized進行修飾,所以無論多少個線程過來,只會有一個線程對鎖住的代碼塊進行操作,那么,庫存始終減1,那么這樣是沒有問題的。

 

 

接下來,如果加入@Transactional,開啟聲明式事務,那么就會有坑了。

@Override
    @Transactional public synchronized void sell() {
        System.out.println("<======"+System.currentTimeMillis());
        
        //根據局id獲取商品信息
        Goods goods = goodDao.findOne(1);//獲取當前庫存
        int store = goods.getStore();
        System.out.println(Thread.currentThread().getName()+" begin:"+store);
        
        
        if(store - 1 >= 0) {
            store = store -1;
            goods.setStore(store);
            //展示庫存-1后的余量
            Goods save = goodDao.save(goods);
            //TODO 可能對其他表進行了操作....
            System.out.println(Thread.currentThread().getName()+" end:"+save.getStore());
        }
        System.out.println(System.currentTimeMillis()+"========>");
        
    }

由於加入了@Transactional,那么就會當做一個事務來進行處理。如果並發的去執行,那么會庫存扣減不一致。原因在於,第一個線程執行完成以后,aop的方法還在繼續,需要去commit,這個需要一定的時間。然后這個時候代碼塊已經走完了,釋放了鎖,那下一個線程過來去庫里查,還是commit前的庫存數量,所以,導致該問題。

解決辦法是自定義一個查詢方法,使用select ... for update的方式,給這條數據加上鎖。JPA的repositry里的寫法:

//PESSIMISTIC_WRITE:事務開始即獲得數據庫的鎖
    @Lock(value=LockModeType.PESSIMISTIC_WRITE)
    @Query(value = "select t from Goods t where t.id =?1 ")
    Goods queryById(Integer id);

那么就ok了,原理是這樣的: 在第一個線程進來的時候,開啟了一個事務,給當前這行數據加了一個行鎖,然后在代碼執行到最后的時候,雖然jvm里的鎖會釋放,第二個線程會進來,但是會卡在select for update這里,因為第一個事務還沒有提交,所以行鎖還在。直到第一個事務提交了以后,第二個線程才會繼續執行,查詢到數據,這個時候的數據,一定是commit完成以后的數據了。那就不會有臟數據的發生。

這次問題的主要原因是JVM鎖與@Transactional聲明式事務aop沒法同時執行的原因導致的。所以使用編程式事務是不存在上述問題的(我試過)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM