最近在項目里遇到一個坑,先上簡易版的描述:每次從庫里查詢一下庫存余量,每次購買一個商品。
數據庫:
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沒法同時執行的原因導致的。所以使用編程式事務是不存在上述問題的(我試過)。