最近寫spring事務時用到REQUIRES_NEW遇到一些不回滾的問題,所以就記錄一下。
場景1:在一個服務層里面方法1和方法2都加上事務,其中方法二設置上propagation=Propagation.REQUIRES_NEW,方法1調用方法2並且在執行完方法2后拋出一個異常,如下代碼
1 @Service 2 public class BookServiceImpl implements BookService { 3 4 @Autowired 5 private JdbcTemplate jdbcTemplate; 6 7 @Transactional(timeout=4) 8 public void update() { 9 // TODO Auto-generated method stub 10 //售賣 扣除庫存數量 11 String sellSql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 12 //入賬的sql 將賺到的錢添加到account表中的balance 13 String addRmbSql = "UPDATE account SET balance = balance + ? * (SELECT price FROM book WHERE NAME = ?)"; 14 Object []params = {1,"Spring"}; 15 16 jdbcTemplate.update(sellSql, params); 17 18 testUpdate(); 19 20 jdbcTemplate.update(addRmbSql, params); 21 22 throw new RuntimeException("故意的一個異常"); 23 } 24 @Transactional(propagation=Propagation.REQUIRES_NEW) 25 public void testUpdate() { 26 //這個業務沒什么意義,只是用來測試REQUIRES_NEW的 當執行后SpringMVC這本書庫存-1 27 String sql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 28 Object []params = {1,"SpringMVC"}; 29 jdbcTemplate.update(sql, params); 30 31 }
三張表分別是對應account表,book表,book_stock表
1 private static ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring/*.xml"); 2 3 @Test 4 public void testREQUIRES_NEW() { 5 6 BookService bean = ac.getBean(BookService.class); 7 8 bean.update(); 9 }
結果是無論是方法1還是方法2都回滾了,那么REQUIRES_NEW就不起作用了,為了探索原因我修改了一下代碼
在第5行的地方打印出對象的類型是什么
1 @Test 2 public void testREQUIRES_NEW() { 3 4 BookService bean = ac.getBean(BookService.class); 5 System.out.println("update的調用者:"+bean.getClass()); 6 bean.update(); 7 }
在第11行的地方打印對象類型
1 @Transactional(timeout=4) 2 public void update() { 3 // TODO Auto-generated method stub 4 //售賣 5 String sellSql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 6 //入賬的sql 7 String addRmbSql = "UPDATE account SET balance = balance + ? * (SELECT price FROM book WHERE NAME = ?)"; 8 Object []params = {1,"Spring"}; 9 10 jdbcTemplate.update(sellSql, params); 11 System.out.println("testUpdate的調用者:"+this.getClass()); 12 testUpdate(); 13 14 jdbcTemplate.update(addRmbSql, params); 15 16 throw new RuntimeException("故意的一個異常"); 17 }
運行結果是
顯然調用update的對象是一個代理對象,調用testUpdate的對象不是一個代理對象,這就是為什么添加REQUIRES_NEW不起作用,想要讓注解生效就要用代理對象的方法,不能用原生對象的.
解決方法:在配置文件中添加標簽<aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>將代理暴露出來,使AopContext.currentProxy()獲取當前代理
將代碼修改為
<!-- 開啟事務注解 --> <tx:annotation-driven transaction-manager="transactionManager"/> <!-- 將代理暴露出來 --> <aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
11 12行將this替換為((BookService)AopContext.currentProxy())
1 @Transactional(timeout=4) 2 public void update() { 3 // TODO Auto-generated method stub 4 //售賣 5 String sellSql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 6 //入賬的sql 7 String addRmbSql = "UPDATE account SET balance = balance + ? * (SELECT price FROM book WHERE NAME = ?)"; 8 Object []params = {1,"Spring"}; 9 10 jdbcTemplate.update(sellSql, params); 11 System.out.println("testUpdate的調用者:"+((BookService)AopContext.currentProxy()).getClass()); 12 ((BookService)AopContext.currentProxy()).testUpdate(); 13 14 jdbcTemplate.update(addRmbSql, params); 15 16 throw new RuntimeException("故意的一個異常"); 17 }
運行結果
調用的對象變成代理對象了 那么結果可想而知第一個事務被掛起,第二個事務執行完提交了 然后異常觸發,事務一回滾 SpringMVC這本書庫存-1,其他的不變
我還看到過另一種解決方法
在第7行加一個BookService類型的屬性並且給個set方法,目的就是將代理對象傳遞過來... 看26 27行顯然就是用代理對象去調用的方法 所以就解決問題了 不過還是用第一個方案好
1 @Service 2 public class BookServiceImpl implements BookService { 3 4 @Autowired 5 private JdbcTemplate jdbcTemplate; 6 7 private BookService proxy; 8 9 public void setProxy(BookService proxy) { 10 this.proxy = proxy; 11 } 12 13 @Transactional(timeout=4) 14 public void update() { 15 // TODO Auto-generated method stub 16 //售賣 17 String sellSql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 18 //入賬的sql 19 String addRmbSql = "UPDATE account SET balance = balance + ? * (SELECT price FROM book WHERE NAME = ?)"; 20 Object []params = {1,"Spring"}; 21 22 jdbcTemplate.update(sellSql, params); 23 /* System.out.println("testUpdate的調用者:"+((BookService)AopContext.currentProxy()).getClass()); 24 ((BookService)AopContext.currentProxy()).testUpdate();*/ 25 26 System.out.println(proxy.getClass()); 27 proxy.testUpdate(); 28 29 jdbcTemplate.update(addRmbSql, params); 30 31 throw new RuntimeException("故意的一個異常"); 32 }
OK這個問題解決那就下一個
場景2:在一個服務層里面方法1和方法2都加上事務,其中方法二設置上propagation=Propagation.REQUIRES_NEW,方法1調用方法2並且在執行方法2時拋出一個異常 沒注意看是不是覺得兩個場景是一樣的,因為我是拷貝下來改的... 差別就是在哪里拋出異常 這次是在方法2里面拋出異常, 我將代碼還原至場景1的第一個解決方案,然后在方法2里面拋出異常 代碼如下
1 @Service 2 public class BookServiceImpl implements BookService { 3 4 @Autowired 5 private JdbcTemplate jdbcTemplate; 6 7 @Transactional(timeout=4) 8 public void update() { 9 // TODO Auto-generated method stub 10 //售賣 11 String sellSql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 12 //入賬的sql 13 String addRmbSql = "UPDATE account SET balance = balance + ? * (SELECT price FROM book WHERE NAME = ?)"; 14 Object []params = {1,"Spring"}; 15 16 jdbcTemplate.update(sellSql, params); 17 18 System.out.println("testUpdate的調用者:"+((BookService)AopContext.currentProxy()).getClass()); 19 ((BookService)AopContext.currentProxy()).testUpdate(); 20 21 jdbcTemplate.update(addRmbSql, params); 22 23 } 24 @Transactional(propagation=Propagation.REQUIRES_NEW) 25 public void testUpdate() { 26 //這個業務沒什么意義,只是用來測試REQUIRES_NEW的 27 String sql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 28 Object []params = {1,"SpringMVC"}; 29 jdbcTemplate.update(sql, params); 30 31 throw new RuntimeException("在方法二里面拋出一個異常"); 32 }
預期結果是testUpdate這個事務是要回滾的,update這個方法的事務正常執行,所以數據庫的變化是balance字段的錢要+60 Spring這本書的庫存-1,但是結果是數據庫完全沒有變化
分析:在testUpdate方法內拋異常被spring aop捕獲,捕獲后異常又被拋出,那么異常拋出后,是不是update方法沒有手動捕獲,而是讓spring aop自動捕獲,所以在update方法內也捕獲到了異常,因此都回滾了
這張圖片的代碼是我debug模式下 在testUpdate方法中執行到拋出異常的地方 再點step over 跳到的地方 顯然spring aop捕獲到了異常后,再次拋出,這就是為什么update方法會捕獲到異常
OK問題很簡單 解決方案也很簡單 只需要手動捕獲該異常,不讓spring aop捕獲就OK了
將update方法改為
1 @Transactional(timeout=4) 2 public void update() { 3 // TODO Auto-generated method stub 4 //售賣 5 String sellSql = "UPDATE book_stock SET stock = stock - ? WHERE isbn = (SELECT isbn FROM book WHERE NAME = ?)"; 6 //入賬的sql 7 String addRmbSql = "UPDATE account SET balance = balance + ? * (SELECT price FROM book WHERE NAME = ?)"; 8 Object []params = {1,"Spring"}; 9 10 jdbcTemplate.update(sellSql, params); 11 12 try { 13 System.out.println("testUpdate的調用者:"+((BookService)AopContext.currentProxy()).getClass()); 14 ((BookService)AopContext.currentProxy()).testUpdate(); 15 } catch (RuntimeException e) { 16 // TODO Auto-generated catch block 17 System.out.println(e.getMessage()); 18 e.printStackTrace(); 19 } 20 21 jdbcTemplate.update(addRmbSql, params); 22 23 }
執行結果 update執行成功 testUpdate回滾
總結:同一個Service不同事務的嵌套會出現調用的對象不是代理對象的問題,如果是多個不同Service的不同事務嵌套就沒有這個問題。場景2的要記得手動捕獲異常,不然全回滾了.至於為什么調用testUpdate方法的對象不是代理對象,可能還要看源碼,懂的人可以在評論區分享一下。
如果有錯誤,請評論區指正