Spring事务的隔离级别和传播行为
一、基本概念
事务传播行为是 Spring 框架独有的事务增强特性,它不是事务实际提供方的数据库行为。Spring 在 TransactionDefinition 接口中规定了 7 种类型的事务传播行为。这是 Spring 提供的强大的工具箱,使得使用事务传播行为更加便利。
1️⃣什么是事务传播行为?
用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法时的事务如何传播。用伪代码说明:
public void methodA(){
methodB();
//doSomething;
}
@Transactional(Propagation=XXX)
public void methodB(){
//doSomething;
}
代码中 methodA() 嵌套调用了 methodB(),methodB() 的事务传播行为由 @Transactional(Propagation=XXX) 决定。注意:methodA() 并没有开启事务,某一个事务传播行为修饰的方法并非必须要在开启事务的外围方法中调用。
2️⃣Spring 中七种事务传播行为
二、验证准备
1️⃣首先在数据库中创建两张表:
CREATE TABLE `transaction_check` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL COMMENT '名称',
`create_name` char(11) DEFAULT NULL COMMENT '创建人',
`create_time` date DEFAULT NULL COMMENT '创建时间',
`modify_name` char(11) DEFAULT NULL COMMENT '修改人',
`modify_time` date DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='事务验证表';
CREATE TABLE `transaction_support` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL COMMENT '名称',
`create_name` char(11) DEFAULT NULL COMMENT '创建人',
`create_time` date DEFAULT NULL COMMENT '创建时间',
`modify_name` char(11) DEFAULT NULL COMMENT '修改人',
`modify_time` date DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='事务支持表';
2️⃣bean、dao、service 代码
@Data
public class TransactionCheck {
private Integer id;
private String name;
private String createName;
private Date createTime;
private String modifyName;
private Date modifyTime;
}
@Data
public class TransactionSupport {
private Integer id;
private String name;
private String createName;
private Date createTime;
private String modifyName;
private Date modifyTime;
}
@Mapper
public interface TransactionCheckDao {
int insertTransactionCheck(TransactionCheck transactionCheck);
TransactionCheck queryById(Integer id);
}
@Mapper
public interface TransactionSupportDao {
int insertTransactionSupport(TransactionSupport transactionSupport);
TransactionSupport queryById(Integer id);
}
@Service
public class TransactionCheckImpl implements ITransactionCheck {
@Autowired
private TransactionCheckDao transactionCheckDao;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void itcRequired(TransactionCheck transactionCheck) {
transactionCheckDao.insertTransactionCheck(transactionCheck);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void itcRequiresNew(TransactionCheck transactionCheck) {
transactionCheckDao.insertTransactionCheck(transactionCheck);
}
@Override
@Transactional(propagation = Propagation.NESTED)
public void itcNested(TransactionCheck transactionCheck) {
transactionCheckDao.insertTransactionCheck(transactionCheck);
}
}
@Service
public class TransactionSupportImpl implements ITransactionSupport {
@Autowired
private TransactionSupportDao transactionSupportDao;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void itsRequired(TransactionSupport transactionSupport) {
transactionSupportDao.insertTransactionSupport(transactionSupport);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void itsRequiredE(TransactionSupport transactionSupport) {
transactionSupportDao.insertTransactionSupport(transactionSupport);
throw new RuntimeException();
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void itsRequiresNew(TransactionSupport transactionSupport) {
transactionSupportDao.insertTransactionSupport(transactionSupport);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void itsRequiresNewE(TransactionSupport transactionSupport) {
transactionSupportDao.insertTransactionSupport(transactionSupport);
throw new RuntimeException();
}
@Override
@Transactional(propagation = Propagation.NESTED)
public void itsNested(TransactionSupport transactionSupport) {
transactionSupportDao.insertTransactionSupport(transactionSupport);
}
@Override
@Transactional(propagation = Propagation.NESTED)
public void itsNestedE(TransactionSupport transactionSupport) {
transactionSupportDao.insertTransactionSupport(transactionSupport);
throw new RuntimeException();
}
}
三、分情况验证
1️⃣PROPAGATION_REQUIRED
1.1 场景一:此场景外围方法没有开启事务。
//验证方法一
@GetMapping("/firInsert")
public void firInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("firCheck");
iTransactionCheck.itcRequired(transactionCheck);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("firSupport");
iTransactionSupport.itsRequired(transactionSupport);//插入
throw new RuntimeException();
//原因:外围方法未开启事务。两方法均在自己的事务中独立运行,外围方法异常不影响内部两方法独立的事务。
}
//验证方法二
@GetMapping("/secInsert")
public void secInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("secCheck");
iTransactionCheck.itcRequired(transactionCheck);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("secSupportException");
iTransactionSupport.itsRequiredE(transactionSupport);//未插入
//原因:外围方法没有事务,两方法均在自己的事务中独立运行。secSupport 插入方法抛出异常,只回滚该方法。 secCheck 方法不受影响。
}
结论:在外围方法未开启事务的情况下,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
1.2 场景二:外围方法开启事务,这个是使用率比较高的场景。
//验证方法一
@GetMapping("/firInsert")
@Transactional(propagation = Propagation.REQUIRED)
public void firInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("firCheck");
iTransactionCheck.itcRequired(transactionCheck);//未插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("firSupport");
iTransactionSupport.itsRequired(transactionSupport);//未插入
throw new RuntimeException();
//原因:外围方法开启事务,内部方法加入外围方法事务。外围方法回滚,内部方法也要回滚。
}
//验证方法二
@GetMapping("/secInsert")
@Transactional(propagation = Propagation.REQUIRED)
public void secInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("secCheck");
iTransactionCheck.itcRequired(transactionCheck);//未插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("secSupportException");
iTransactionSupport.itsRequiredE(transactionSupport);//未插入
//原因:外围方法开启事务,内部方法加入外围方法事务。内部方法抛出异常回滚,外围方法感知异常致使整体事务回滚。
}
//验证方法三
@GetMapping("/thiInsert")
@Transactional
public void thiInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("thiCheck");
iTransactionCheck.itcRequired(transactionCheck);//未插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("thiSupportException");
try {
iTransactionSupport.itsRequiredE(transactionSupport);//未插入
} catch (Exception e) {
System.out.println("方法回滚");
}
//原因:外围方法开启事务,内部方法加入外围方法事务。内部方法抛出异常回滚,即使方法被catch不被外围方法感知,整体事务依然回滚。
}
结论:在外围方法开启事务的情况下,Propagation.REQUIRED修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。
2️⃣PROPAGATION.REQUIRES_NEW
2.1 场景一:外围方法没有开启事务。
@GetMapping("/firInsert")
public void firInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("firCheck");
iTransactionCheck.itcRequiresNew(transactionCheck);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("firSupport");
iTransactionSupport.itsRequiresNew(transactionSupport);//插入
throw new RuntimeException();
//原因:外围方法没有开启事务,内部方法分别开启自己的事务。外围方法抛出异常不会影响内部方法。
}
//验证方法二
@GetMapping("/secInsert")
public void secInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("secCheck");
iTransactionCheck.itcRequiresNew(transactionCheck);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("secSupportException");
iTransactionSupport.itsRequiresNewE(transactionSupport);//未插入
//原因:外围方法没有开启事务,内部方法分别开启自己的事务。secSupportException 方法抛出异常回滚,其他事务不受影响。
}
结论:在外围方法未开启事务的情况下,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
2.2 场景二:外围方法开启事务。
//验证方法一
@GetMapping("/firInsert")
@Transactional(propagation = Propagation.REQUIRED)
public void firInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("firCheck");
iTransactionCheck.itcRequired(transactionCheck);//未插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("firSupport");
iTransactionSupport.itsRequiresNew(transactionSupport);//插入
TransactionSupport ts = new TransactionSupport();
ts.setName("firSupportBackups");
iTransactionSupport.itsRequiresNew(ts);//插入
throw new RuntimeException();
//原因:外围方法开启事务,firCheck 和外围方法一个事务,firSupport 和 firSupportBackups 分别在独立的新事务中。外围方法抛出异常只回滚和外围方法同一事务的方法,所以 firCheck 未插入。
}
//验证方法二
@GetMapping("/secInsert")
@Transactional(propagation = Propagation.REQUIRED)
public void secInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("secCheck");
iTransactionCheck.itcRequired(transactionCheck);//未插入
TransactionSupport ts = new TransactionSupport();
ts.setName("secSupport");
iTransactionSupport.itsRequiresNew(ts);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("secSupportException");
iTransactionSupport.itsRequiresNewE(transactionSupport);//未插入
//原因:外围方法开启事务,secCheck 和外围方法一个事务,secSupport 和 secSupportException 分别在独立的新事务中。thiSupportException 抛出异常,首先该事务被回滚,异常继续抛出被外围方法感知,外围方法事务回滚,所以 secCheck 未插入。
}
//验证方法三
@GetMapping("/thiInsert")
@Transactional(propagation = Propagation.REQUIRED)
public void thiInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("thiCheck");
iTransactionCheck.itcRequired(transactionCheck);//插入
TransactionSupport ts = new TransactionSupport();
ts.setName("thiSupport");
iTransactionSupport.itsRequiresNew(ts);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("thiSupportException");
try {
iTransactionSupport.itsRequiresNewE(transactionSupport);//未插入
} catch (Exception e) {
System.out.println("回滚");
}
//原因:外围方法开启事务,thiCheck 和外围方法一个事务,thiSupport 和 thiSupportException 分别在独立的新事务中。thiSupportException 抛出异常,首先该事务被回滚,异常被 catch 不会被外围方法感知,外围方法事务不回滚,所以 thiCheck 插入。
}
结论:在外围方法开启事务的情况下,Propagation.REQUIRES_NEW修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
3️⃣PROPAGATION_NESTED
3.1 场景一:此场景外围方法没有开启事务。
//验证方法一
@GetMapping("/firInsert")
public void firInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("firCheck");
iTransactionCheck.itcNested(transactionCheck);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("firSupport");
iTransactionSupport.itsNested(transactionSupport);//插入
throw new RuntimeException();
//原因:外围方法未开启事务。两方法在自己的事务中独立运行,外围方法异常不影响内部两方法独立的事务。
}
//验证方法二
@GetMapping("/secInsert")
public void secInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("secCheck");
iTransactionCheck.itcNested(transactionCheck);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("secSupportException");
iTransactionSupport.itsNestedE(transactionSupport);//未插入
//原因:外围方法未开启事务。两方法在自己的事务中独立运行。secSupportException 抛出异常只会回滚该方法,不影响 secCheck 方法。
结论:在外围方法未开启事务的情况下Propagation.NESTED和Propagation.REQUIRED作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
3.2 场景二:外围方法开启事务。
//验证方法一
@GetMapping("/firInsert")
@Transactional
public void firInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("firCheck");
iTransactionCheck.itcNested(transactionCheck);//未插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("firSupport");
iTransactionSupport.itsNested(transactionSupport);//未插入
throw new RuntimeException();
//原因:外围方法开启事务,内部事务为外围事务的子事务,外围方法回滚,内部方法也要回滚。
}
//验证方法二
@GetMapping("/secInsert")
@Transactional
public void secInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("secCheck");
iTransactionCheck.itcNested(transactionCheck);//未插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("secSupportException");
iTransactionSupport.itsNestedE(transactionSupport);//未插入
//原因:外围方法开启事务,内部事务为外围事务的子事务,内部方法抛出异常回滚,且外围方法感知异常致使整体事务回滚。
}
//验证方法三
@GetMapping("/thiInsert")
@Transactional
public void thiInsert() {
TransactionCheck transactionCheck = new TransactionCheck();
transactionCheck.setName("thiCheck");
iTransactionCheck.itcNested(transactionCheck);//插入
TransactionSupport transactionSupport = new TransactionSupport();
transactionSupport.setName("thiSupportException");
try {
iTransactionSupport.itsNestedE(transactionSupport);//未插入
} catch (Exception e) {
System.out.println("回滚");
}
//原因:外围方法开启事务,内部事务为外围事务的子事务,插入【thiSupportException】的内部方法抛出异常,可以单独对子事务回滚。
}
结论: 在外围方法开启事务的情况下Propagation.NESTED修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务。
4️⃣REQUIRED,REQUIRES_NEW,NESTED异同
由“1.2 场景二”和“3.2 场景二”对比可知:
- NESTED 和 REQUIRED 修饰的内部方法都属于外围方法事务,如果外围方法抛出异常,这两种方法的事务都会被回滚。
- 但是 REQUIRED 是加入外围方法事务,所以和外围事务同属于一个事务,一旦 REQUIRED 事务抛出异常被回滚,外围方法事务也将被回滚。
- 而 NESTED 是外围方法的子事务,有单独的保存点,所以 NESTED 方法抛出异常被回滚,不会影响到外围方法的事务。
由“2.2 场景二”和“3.2 场景二”对比可知:
- NESTED 和 REQUIRES_NEW 都可以做到内部方法事务回滚而不影响外围方法事务。
- 但是因为 NESTED 是嵌套事务,所以外围方法回滚之后,作为外围方法事务的子事务也会被回滚。
- 而 REQUIRES_NEW 是通过开启新的事务实现的,内部事务和外围事务是两个事务,外围事务回滚不会影响内部事务。