最近写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不起作用,想要让注解生效就要用代理对象的方法,不能用原生对象的.


解决方法:在配置文件中添加标签将代理暴露出来,使AopContext.currentProxy()获取当前代理


将代码修改为


1   


2


3


4


  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方法的对象不是代理对象,可能还要看源码,懂的人可以在评论区分享一下。