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之中。
如果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异常,则两个都会回滚。
- 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 总结
REQUIRED
、REQUIRES_NEW
和NESTED
为比较常用的三个属性。