程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 兩個與spring事務相關的問題,spring事務相關

兩個與spring事務相關的問題,spring事務相關

編輯:JAVA綜合教程

兩個與spring事務相關的問題,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, String type) {
        keyGeneratorDao.insert(key, type);
        keyGeneratorDao.update(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, String type) {
        keyGeneratorDao.insert(key, type);
        Integer.parseInt("aaa");//throw exception
        keyGeneratorDao.update(key, type);
}

        測試結果顯示事務回滾正常,可以排除事務環境配置問題。

        挖的第二個坑:做測試一定要與原問題代碼盡量保持一致,否則會產生其它的不明原因影響判斷。通過對比原問題的代碼發現我寫的測試代碼與問題代碼有區別,readOnly是加在包含有寫操作的方法上,而我的是兩個方法,只有在讀的方法上增加了readOnly,於時再次修改代碼:
       

    @Transactional(readOnly = true)
    public Integer getUID4(String key, String type) {
        keyGeneratorDao.insert(key, type);
        keyGeneratorDao.update(key, type);
        return keyGeneratorDao.select(key, type);
    }


        測試結果顯示運行不正常,提示如下錯誤:Cause: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed,到這的確說明在在加了readOnly=true的事務內是不允許寫入操作的。為什麼這段代碼在線上運行是成功的呢,於時查看前端的調用,發現前端調用的並不是直接標識了Transactional的方法,而是根據不同的具體業務重新包裝的方法,比如我們需要生成訂單的編號,前端只調用genOrderCode而不調用getUID。

 

  • 非事務方法在內部調用了本類事務方法,然後非事務方法被外部調用
    • Service
      • genOrderCode,是一個非事務方法,內部調用了getUID
      • getUID,是一個事務方法
    @Autowired
    private IkeyGeneratorDao keyGeneratorDao;

    @Transactional(readOnly = true)
    public Integer getUID(String key, String type) {
        keyGeneratorDao.insert(key, type);
        keyGeneratorDao.update(key, type);
        return keyGeneratorDao.select(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, String type) {
        keyGeneratorDao.insert(key, type);
        Integer.parseInt("aaa");//throw exception
        keyGeneratorDao.update(key, type);
        return keyGeneratorDao.select(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, 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, String type) {
        keyGeneratorDao.insert(key, type);
        Integer.parseInt("aaa");//throw exception
        keyGeneratorDao.update(key, type);
        return keyGeneratorDao.select(key, type);
    }


    再執行相同的測試,數據正常回滾,這裡提供兩張圖,可以看的清楚些(因為常用的就這兩種,其它的有興趣可以多多研究)

  • REQUIRED

      

  • REQUIRES_NEW

      


   通過事務的兩個小問題,總結出解決問題的一些小技巧或者叫經驗:發現問題之後,不要局限於某個點,最好根據上下文來結合分析,比如問題一的readonly可寫入,單看那段代碼很難找出合理的解釋,只有結合前後端調用才能找出根本原因。寫單元測試盡量寫相同的代碼,否則有可能會出現一些干擾項影響判斷。學習呢,有時間盡量學的全點,比如@Transactional這個注解,除了readOnly還有Propagation等等。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved