Spring事务传播行为(Propagation behavior)

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为比较常用的三个属性。

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

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值