Spring事務相關問題解決方案
有些spring相關的知識點之前一直沒有仔細研究:比如spring的事務,並不是沒有使用,也曾經簡單的在某些需要事務處理的方法上通過增加事務註解來實現事務功能,僅僅是跟隨使用(甚至並未測試過事務的正確性),至於如何在專案中配置事務,如何才能將事務寫正確,事務的其它的一些原理性的東西從未花時間研究。最近同事正好丟擲了一個問題,藉此機會學習了一遍。
問題一:增加了readOnly=true的事務中包含寫操作,為什麼線上執行這段程式碼是正常的呢?
@Transactional(readOnly = true) public Integer getUID(String key,String type) { keyGeneratorDao.insert(key,type); keyGeneratorDao.update(key,type); return keyGeneratorDao.select(key,type); }
我為什麼對這個問題感興趣?
不懂這個readOnly引數的含義,之前寫@Transactional的註解,那都是使用的預設值,不帶顯示引數。提出配置了readOnly引數後,理論上應該程式報錯而實際上沒有報錯,想搞清楚為什麼。
開始寫單元測試:
在單元測試類中寫事務函式以及測試方法
@Autowired private IkeyGeneratorDao keyGeneratorDao; @Transactional(readOnly=true) public Integer getId(String key,String type){ return keyGeneratorDao.select(key,type); } @Transactional public Integer getUID(String key,type); return this.getId(key,type); } @Test public void testCreateGuid(){ int guid=this.getUID("12345","jim"); System.out.println(guid); }
測試結果顯示正常,與上面提到的不允許進行寫操作的觀點相反,於是想起典型的事務生效問題。
挖的第一個坑:如果事務採用的是cglib動態代理,呼叫的方法與事務方法處在同一個類中事務不生效。
將兩個事務事務轉移到單獨的類中,然後測試,類程式碼省略,只是將上面兩個標記了@Transactional的方法封裝在一個單獨的類中。
@Autowired private KeyGeneratorService keyService; @Test public void testCreateGuid2(){ int guid=this.keyService.getUID("12345","jim"); System.out.println(guid); }
測試結果顯示也是正常,於是想確認下事務到底是否生效,加入異常以測試資料是否回滾,修改程式碼如下:
@Transactional public Integer getUID3(String key,type); Integer.parseInt("aaa");//throw exception keyGeneratorDao.update(key,type); }
測試結果顯示事務回滾正常,可以排除事務環境配置問題。
挖的第二個坑:做測試一定要與原問題程式碼儘量保持一致,否則會產生其它的不明原因影響判斷。通過對比原問題的程式碼發現我寫的測試程式碼與問題程式碼有區別,readOnly是加在包含有寫操作的方法上,而我的是兩個方法,只有在讀的方法上增加了readOnly,於時再次修改程式碼:
@Transactional(readOnly = true) public Integer getUID4(String key,type); }
測試結果顯示執行不正常,提示如下錯誤:Cause: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed,到這的確說明在在加了readOnly=true的事務內是不允許寫入操作的。為什麼這段程式碼在線上執行是成功的呢,於時檢視前端的呼叫,發現前端呼叫的並不是直接標識了Transactional的方法,而是根據不同的具體業務重新包裝的方法,比如我們需要生成訂單的編號,前端只調用genOrderCode而不呼叫getUID。
非事務方法在內部呼叫了本類事務方法,然後非事務方法被外部呼叫ServicegenOrderCode,是一個非事務方法,內部呼叫了getUIDgetUID,是一個事務方法
@Autowired private IkeyGeneratorDao keyGeneratorDao; @Transactional(readOnly = true) public Integer getUID(String key,type); } public String genOrderCode(Date orderDate) { SimpleDateFormat df = new SimpleDateFormat("yyMMddHH"); String ticketDate = df.format(orderDate); Integer uid = getUID(ticketDate,ORDER_CODE); return ticketDate + genRandom2(uid.toString(),3,3) ; }
Test,外部類呼叫非事務方法
@Test public void testCreateGuid3(){ String guid=this.keyService.genOrderCode(new Date()); System.out.println(guid); }
測試結果居然是正常的,這與線上執行的結果相同,後面經同事提醒,這又是一個不正確使用事務的案例。
挖的第三個坑:當呼叫一個類的非事務方法且這個非事務方法內部呼叫了本類自身的事務方法,那麼事務也不會生效。
問題二:下面的程式碼可以實現事務回滾嗎?
Service
- genOrderCode方法呼叫
- getUID2兩個方法都是具備相同的事務引數
- getUID2丟擲異常
- genOrderCode捕獲這個異常
@Transactional public Integer getUID2(String key,type); } @Transactional public String genOrderCode(Date orderDate) { try{ SimpleDateFormat df = new SimpleDateFormat("yyMMddHH"); String ticketDate = df.format(orderDate); Integer uid = getUID2(ticketDate,ORDER_CODE); return ticketDate + genRandom2(uid.toString(),3) ; }catch(Exception ex){ System.out.println(ex); } return null; }
Test
@Test public void testCreateGuid3(){ String guid=this.keyService.genOrderCode(new Date()); System.out.println(guid); }
執行測試程式碼,發現可以成功提交,但資料是不完整的,因為更新操作沒有完成。為什麼會是這樣的呢?因為預設的Propagation.REQUIRED指明多個操作處於一個事務中,由於genOrderCode有異常處理,所以即使getUID2中發生異常,系統也會認定提交是合法的,因此會出現插入操作正常更新不正常但事務正常提交併不回滾的情況。
如果顯示指定Propagation.REQUIRES_NEW呢?
@Transactional(propagation=Propagation.REQUIRES_NEW) public Integer getUID2(String key,type); }
再執行相同的測試,資料正常回滾,這裡提供兩張圖,可以看的清楚些(因為常用的就這兩種,其它的有興趣可以多多研究)
REQUIRED
REQUIRES_NEW
通過事務的兩個小問題,總結出解決問題的一些小技巧或者叫經驗:發現問題之後,不要侷限於某個點,最好根據上下文來結合分析,比如問題一的readonly可寫入,單看那段程式碼很難找出合理的解釋,只有結合前後端呼叫才能找出根本原因。寫單元測試儘量寫相同的程式碼,否則有可能會出現一些干擾項影響判斷。學習呢,有時間儘量學的全點,比如@Transactional這個註解,除了readOnly還有Propagation等等。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。