事务传播机制
简化记忆版本
- REQUIRED:有事务加入,没有事务创建,Spring默认
- MANDATORY:必须在事务中被调用,没有抛异常
- SUPPORTS:有事务加入,没有以非事务运行
- NOT_SUPPORTED:不需要事务,有事务则挂起,避免回滚。例如记日志,避免日志信息回滚
- REQUIRES_NEW:当前方法创建新事务运行,如果有事务则挂起,主要是为了控制敏感资源事务粒度,避免从时间锁,可以局部回滚
- NESTED:如果有事务,加入事务但是记录保存点,如果没有事务创建事务,主要作用局部回滚(通过savepoint实现),不挂起已有事务
- NEVER:该方法不应该在事务中运行,如果存在事务则抛异常
不同传播机制之间的区别
REQUIRED与MANDATORY:REQUIRED没有事务自己创建,MANDATOR没有事务异常。
传播机制 | 区别 |
---|---|
REQUIRED与MANDATORY | 方法没有事务,REQUIRED自己创建;而MANDATOR抛出异常 |
REQUIRED与REQUIRES_NEW | 方法在事务中执行,REQUIRED加入该事务;REQUIRES_NEW会挂起已有的事务,创建自己的事务 |
NESTED与REQUIRES_NEW | 方法在事务中执行NESTED加入该事务,并记录回滚点;REQUIRES_NEW挂起已有事务,创建新事务 |
NESTED与REQUIRED | 都是有事务加入,没有事务创建,但NESTED比REQUIRED多了保存回滚点 |
NOT_SUPPORTED与NEVER | 方法在事务中被调用NOT_SUPPORTED会挂起事务,而NEVER会抛出异常 |
MANDATORY与NEVER | 两者相反, 没有在事务中MANDATORY抛出异常,而有事务NEVER抛出异常 |
SUPPORTS与没有传播机制注解 | 感觉没有区别,发现有区别的朋友可以补充一下 |
当SUPPORTS方法被NEVER、NOT_SUPPORTED、没有@Transactional注解这些方法调用,不会有事务,和不加注解效果一样。
当SUPPORTS方法被REQUIRED、MANDATORY、REQUIRES_NEW、NESTED这些方法调用时,加入事务,和不加注解效果也一样。
所以SUPPORTS应用场景是啥?
问题测试
REQUIRED与MANDATORY
@Service
public class ServiceA
{@Transactional(propagation = Propagation.REQUIRED)public void methodA(){methodB();}@Transactional(propagation = Propagation.MANDATORY)public void methodB(){}
}@Service
public class ServiceC
{@Resourceprivate ServiceA serviceA;public void methodC(){// 创建一个事务执行方法AserviceA.methodA();// methodB是MANDATORY,但是methodC没有事务,所以直接抛出异常serviceA.methodB();}
}
NESTED回滚测试
@Service
public class ServiceA
{@Transactional(propagation = Propagation.NESTED)public void methodA(){// 数据库操作TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}
}@Service
public class ServiceB
{@Transactional(propagation = Propagation.NESTED)public void methodB(){// 数据库操作}
}@Service
public class ServiceC
{@Resourceprivate ServiceA serviceA;@Resourceprivate ServiceB serviceB;@Transactionalpublic void methodC(){serviceA.methodA();serviceB.methodB();}
}
上面的操作哪些会回滚?
答案是:
methodA会回滚
methodB不回滚
因为methodA、methodB是NESTED,记录了回滚点,所以只有methodA被回滚。
NESTED传播测试
做一点小变动,如果把methodA上的@Transactional(propagation = Propagation.NESTED)注解去掉呢?
放在methodC方法上会怎样?
@Service
public class ServiceA
{public void methodA(){// 数据库操作TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}
}@Service
public class ServiceB
{@Transactional(propagation = Propagation.NESTED)public void methodB(){// 数据库操作}
}@Service
public class ServiceC
{@Resourceprivate ServiceA serviceA;@Resourceprivate ServiceB serviceB;@Transactional(propagation = Propagation.NESTED)public void methodC(){serviceA.methodA();serviceB.methodB();}
}
答案是:
methodA会回滚
methodB会回滚
因为methodA没有注解,就相当于使用的是methodC的事务,并且所以回滚点在methodC,
methodB也使用了methodC的事务,也设置了回滚点,但是在methodA中回滚,整个事务都回滚了。
如何只回滚methodB,不回滚methodA呢?
答案是:
将methodA的TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()放在方法放methodB中就可以。
当methodB是NESTED的时候,在其中回滚,因为有回滚点,所以只会回滚methodB中操作,不会影响methodC方法。
REQUIRES_NEW回滚测试
再来一点点小变动:
将methodB的NESTED换为REQUIRES_NEW会怎样?
@Service
public class ServiceA
{public void methodA(){// 数据库操作TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}
}@Service
public class ServiceB
{@Transactional(propagation = Propagation.REQUIRES_NEW)public void methodB(){// 数据库操作}
}@Service
public class ServiceC
{@Resourceprivate ServiceA serviceA;@Resourceprivate ServiceB serviceB;@Transactional(propagation = Propagation.NESTED)public void methodC(){serviceA.methodA();serviceB.methodB();}
}
答案是:
methodA会回滚
methodB不回滚
把methodB的NESTED换为REQUIRES_NEW就可以,因为REQUIRES_NEW为挂起原有事务,并创建新的事务,
所以methodC的事务不会影响到methodB的事务。
异常对不同传播机制回滚影响测试
再来一点点小变动:
将methodA中的手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
换为:throw new RuntimeException(“我要异常”);
会怎样呢?
@Service
public class ServiceA
{@Transactional(propagation = Propagation.NESTED)public void methodA(){// 数据库操作throw new RuntimeException("我要异常");}
}@Service
public class ServiceB
{@Transactional(propagation = Propagation.NESTED)public void methodB(){// 数据库操作}
}@Service
public class ServiceC
{@Resourceprivate ServiceA serviceA;@Resourceprivate ServiceB serviceB;@Transactionalpublic void methodC(){serviceA.methodA();serviceB.methodB();}
}
答案是:
methodA会回滚
methodB会回滚
如果异常这一次把methodB的传播机制换NESTED为REQUIRES_NEW还能生效吗?
答案是:不能。
很难理解?
的确比较绕,其实很简单,因为异常了,根本就没有执行到methodB,就直接回滚了。
事务内部调用
再来点简单点的问题。
// 调用 1
public class ServiceA
{@Transactionalpublic void methodA(){userMapper.insertSelective(UserTO.builder().username("tim").email("test@gmail.com").build());methodB();}public void methodB(){eventLogMapper.insertSelective(EventLogTO.builder().type((short) 1).message("创建用户").build());}
}
这个事务会生效吗?
答案是:会,很简单,在方法methodA中调用methodB,其实和把methodB的方法体放入methodA方法体中没有什么一样。
// 调用2
public class ServiceA
{public void methodA(){methodB();}@Transactionalpublic void methodB(){}
}
这个事务会生效吗?
答案是:不能,很多朋友都知道,因为没有走代理,Spring的事务必须通过代理。
如何让它生效呢?
不要直接调用,通过Spring的方式来呗。
@EnableAspectJAutoProxy(exposeProxy = true)
ServiceA serviceA = (ServiceA)AopContext.currentProxy();
serviceA.methodB();
或者:
@Resource
private ApplicationContext applicationContext;
ServiceA serviceA = applicationContext.getBean(this.getClass());
serviceA.methodB();
或者:
@Service
public class ServiceA
{@Resourceprivate ServiceB serviceB;public void methodA(){serviceB.methodB();}
}@Service
public class ServiceB
{ @Transactionalpublic void methodB(){}
}
SQL
CREATE TABLE user (id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',username varchar(30) DEFAULT NULL COMMENT '姓名',age smallint(5) unsigned DEFAULT NULL COMMENT '年龄',sex tinyint DEFAULT -1 COMMENT '性别(-1-保密,1-男,2-女)',email varchar(255) DEFAULT NULL COMMENT '邮箱',status tinyint DEFAULT 1 COMMENT '状态(-1-删除,1-正常)' ,create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';CREATE TABLE event_log (id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',message varchar(255) DEFAULT NULL COMMENT '信息',type smallint(5) unsigned DEFAULT NULL COMMENT '事件类型',create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='事件日志';