Spring事务传播行为(Propagation behavior)

来源:互联网 发布:总裁高级研修班知乎 编辑:程序博客网 时间:2024/06/05 09:23

Spring事务传播行为(Propagation behavior)

有的时候,一个业务类的方法需要调用另一个业务类的方法,如下面的情形所示。

public class OuterService {    private InnerService innerService;    @Transactional    public void outerMethod() {        /*do something*/        try {            innerService.innerMethod();        } catch(Exception e) {            /*catch the exception*/        }           }}public class InnerService {    @Transactional    public void innerMethod() {        /* do something */    }}

OuterService类的outerMethod()需要调用InnerService类的innerMethod()方法。如果innerMethod()发生某些异常并且回滚,那么,outerMethod()是否回滚?(即事务怎么由内传播到外)

1 七个传播行为

Spring针对这种情况,定义了一些行为,叫做事务传播行为(transaction propagation behavior)。Spring事务传播行为可以分为三类:

  • REQUIRED,SUPPORTS,MANDATORY (REQUIRES_NEW)

要求方法在事务中运行。如果outer method在事务中运行,就使用当前事务。(但REQUIRES_NEW比较特殊,始终会新建一个事务,始终在事务中运行。)如果outer method不存在事务,则分别是新建一个事务、以非事务方式运行和抛出异常。

  • NOT_SUPPORTED, NEVER

要求inner method以非事务的方式运行。如果当前存在事务,则分别是将当前事务挂起和抛出异常。

  • NESTED

innerMethod()的事务与outerMethod()的事务共享同一个physical transaction,但是,这个physical transaction中有很多savepoint。每个innerMethod()可以单独还原到savepoint,outerMethod()仍然可以正常提交。但是,如果outerMethod()回滚,则innerMethod()也要回滚。

2 重点说明

REQUIRED

如下图所示,method1 (outer method)处于事务之中,当method2(inner method) 传播行为设置为REQUIRED时,它们两者处于不同的logical transaction,但是,处于相同的physical transaction。当两者有一个回滚时,他们都会回滚,因为他们处于相同的physical transaction之中。

这里写图片描述
(来源:http://docs.spring.io/spring-framework/docs/4.2.x/spring-framework-reference/html/transaction.html#transaction-declarative)

如果outer method不处于事务之中,则inner method会新建一个事务。

见下面的例子,以银行转账业务为例。假设,每次转账之后,都将一条转账的记录插入数据库之中。

  • 设置outer method抛出异常并回滚,那么inner method也会回滚。

转账相关的类:

AccountService

package com.chris.service;import java.text.SimpleDateFormat;import java.util.Date;import java.util.List;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.transaction.annotation.Isolation;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import com.chris.dao.AccountDAO;import com.chris.domain.Account;import com.chris.domain.Record;public class AccountService {    private AccountDAO accountDAO;    @Autowired    private RecordService recordService;    public AccountDAO getAccountDAO() {        return accountDAO;    }    public void setAccountDAO(AccountDAO accountDAO) {        this.accountDAO = accountDAO;    }    @Transactional(propagation=Propagation.REQUIRED)    public void transfer(String from, String to, double money) {        accountDAO.outMoney(from, money);        accountDAO.inMoney(to, money);        Date date = new Date();        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");        //here the type is String, but in db, the type is datetime. However, it works.        String currentTime = sdf.format(date);        Record record = new Record(from, to, money, currentTime);        try {            recordService.insertRecord(record);        } catch(RuntimeException e) {        }           throw new RuntimeException("rollback outer transaction");           }}

插入转账记录的类RecordService:

package com.chris.service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import com.chris.dao.RecordDAO;import com.chris.domain.Record;public class RecordService {    private RecordDAO recordDAO;    public RecordDAO getRecordDAO() {        return recordDAO;    }    public void setRecordDAO(RecordDAO recordDAO) {        this.recordDAO = recordDAO;    }    @Transactional(propagation=Propagation.REQUIRED)    public void insertRecord(Record record) {        recordDAO.insertRecord(record);        //throw new RuntimeException("rollback the inner transaction");    }}

测试方法

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration("classpath:applicationContext.xml")public class TransferTest {    @Autowired    private AccountService accountService;    @Test    public void testTransfer() {        accountService.transfer("Jane", "Michael", 100d);    }}

数据库初始记录

mysql> select * from account;+----+---------+-----------+| id | name    | money     |+----+---------+-----------+|  1 | Michael | 1100.0000 ||  2 | Jane    |  900.0000 ||  3 | Kate    | 1000.0000 |+----+---------+-----------+3 rows in set (0.00 sec)mysql> select * from record;+----+-----------+---------+----------+---------------------+| id | from_user | to_user | money    | time                |+----+-----------+---------+----------+---------------------+| 29 | Jane      | Michael | 100.0000 | 2017-01-11 12:45:54 |+----+-----------+---------+----------+---------------------+1 row in set (0.00 sec)

运行之后,数据库的记录没有变。

REQUIRES_NEW

REQUIRES_NEW始终会新建一个事务,这个事务与原来的事务处于不同的logical transaction和physical transaction。如下图所示。因此两个事务是彼此独立的,不会相互影响。当然,如果inner method出现了RuntimeException并且抛出,没有被outer method 捕获并且处理,导致outer method异常,则两个都会回滚。

requires_new

  • outer method为REQUIRED,inner method为REQUIRES_NEW
    @Transactional(propagation=Propagation.REQUIRED)    public void transfer(String from, String to, double money) {        accountDAO.outMoney(from, money);        accountDAO.inMoney(to, money);              /*省略一部分*/        try {            recordService.insertRecord(record);        } catch(RuntimeException e) {        }    }    @Transactional(propagation=Propagation.REQUIRES_NEW)    public void insertRecord(Record record) {        recordDAO.insertRecord(record);         throw new RuntimeException("rollback the inner transaction");    }

运行之前,

mysql> select * from account;+----+---------+-----------+| id | name    | money     |+----+---------+-----------+|  1 | Michael | 1100.0000 ||  2 | Jane    |  900.0000 ||  3 | Kate    | 1000.0000 |+----+---------+-----------+3 rows in set (0.00 sec)mysql> select * from record;+----+-----------+---------+----------+---------------------+| id | from_user | to_user | money    | time                |+----+-----------+---------+----------+---------------------+| 29 | Jane      | Michael | 100.0000 | 2017-01-11 12:45:54 |+----+-----------+---------+----------+---------------------+1 row in set (0.00 sec)

运行之后,

mysql> select * from record;+----+-----------+---------+----------+---------------------+| id | from_user | to_user | money    | time                |+----+-----------+---------+----------+---------------------+| 29 | Jane      | Michael | 100.0000 | 2017-01-11 12:45:54 |+----+-----------+---------+----------+---------------------+1 row in set (0.00 sec)mysql> select * from account;+----+---------+-----------+| id | name    | money     |+----+---------+-----------+|  1 | Michael | 1200.0000 ||  2 | Jane    |  800.0000 ||  3 | Kate    | 1000.0000 |+----+---------+-----------+3 rows in set (0.00 sec)

可见,outer transaction运行成功,inner transaction运行失败。因为,inner method抛出RuntimeException,故回滚。由于是两个独立的physical transaction,并且,outer method捕获了inner method抛出的异常并处理,故outer method不会回滚。

NESTED

innerMethod()的事务与outerMethod()的事务共享同一个physical transaction,但是,这个physical transaction中有很多savepoint。每个innerMethod()可以单独还原到savepoint,outerMethod()仍然可以正常提交。但是,如果outerMethod()回滚,则innerMethod()也要回滚。

下面的例子中,outer method为REQUIRED,inner method为NESTED。inner method抛出RuntimeException。

    @Transactional(propagation=Propagation.REQUIRED)    public void transfer(String from, String to, double money) {        accountDAO.outMoney(from, money);        accountDAO.inMoney(to, money);        /* 省略一部分*/        Record record = new Record(from, to, money, currentTime);        try {            recordService.insertRecord(record);        } catch(RuntimeException e) {        }           }    @Transactional(propagation=Propagation.NESTED)    public void insertRecord(Record record) {        recordDAO.insertRecord(record);        throw new RuntimeException("rollback the inner transaction");    }

运行之前,

mysql> select * from account;+----+---------+-----------+| id | name    | money     |+----+---------+-----------+|  1 | Michael | 1200.0000 ||  2 | Jane    |  800.0000 ||  3 | Kate    | 1000.0000 |+----+---------+-----------+3 rows in set (0.00 sec)mysql> select * from record;+----+-----------+---------+----------+---------------------+| id | from_user | to_user | money    | time                |+----+-----------+---------+----------+---------------------+| 29 | Jane      | Michael | 100.0000 | 2017-01-11 12:45:54 |+----+-----------+---------+----------+---------------------+1 row in set (0.00 sec)

运行之后,

mysql> select * from record;+----+-----------+---------+----------+---------------------+| id | from_user | to_user | money    | time                |+----+-----------+---------+----------+---------------------+| 29 | Jane      | Michael | 100.0000 | 2017-01-11 12:45:54 |+----+-----------+---------+----------+---------------------+1 row in set (0.00 sec)mysql> select * from account;+----+---------+-----------+| id | name    | money     |+----+---------+-----------+|  1 | Michael | 1400.0000 ||  2 | Jane    |  600.0000 ||  3 | Kate    | 1000.0000 |+----+---------+-----------+3 rows in set (0.00 sec)

可见,虽然inner method异常,且处于同一个physical transaction, 但是,outer method仍然运行成功。

注意 如果inner method和outer method处于同一个service类中,则以上的叙述不保证正确。

3 总结

REQUIREDREQUIRES_NEWNESTED为比较常用的三个属性。

如有疑问,还请提出,共同进步。

参考

  • propagation
  • Transaction Management
  • 浅析Spring事务传播行为和隔离级别
0 0