从源码级别理解让人抓狂的@Transactional注解
✨这里是第七人格的博客✨小七,欢迎您的到来~✨
🍅系列专栏:实战🍅
✈️本篇内容: 从源码级别理解让人抓狂的@Transactional注解✈️
🍱本篇收录完整代码地址:无🍱
楔子
小七最近经常遇到小伙伴对@Transactional的使用有诸多疑问,比如@Transactional什么时候生效,@Transactional什么时候不生效,还有些小伙伴说@Transactional会吃掉代码的异常,错误抛不出来,弄得小七都差点怀疑自己了。所以小七决定通过这篇文章好好分析一下@Transactional。
一些常见的场景
首先大家都知道,@Transactional是基于AOP的,那么AOP失效的场景,@Transactional也就会失效了,不需要去记每一种失效的场景。我们通过下面例子,加深一下理解。
公共代码
我们这里为了方便测试使用的是Mybatis-Plus
Controll层
@RestController
@RequestMapping(value = "/user",produces = "text/html;charset=utf-8")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* 测试Mybatis-Plus 新增
*/
@GetMapping("/save")
public void save() {
User user = new User();
boolean save = userService.save(user);
}
}
Service接口
public interface UserService extends IService<User> {
void save(User user) throws RuntimeException;
}
Service实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
public void save(User user) throws RuntimeException {
}
}
场景一
场景一: 用户——>调用Controller(bean)的save方法——>调用Service的mySave方法(该方法上有@Transactional注解)——>调用doSave方法,向数据库插入数据
代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
@Transactional
public void mySave(User user) throws RuntimeException {
doSave(user);
}
public void doSave(User user){
super.save(user);
}
}
分析
谁调用的加了注解的方法?Spring的bean调用的,有事务;事务范围Service的mySave方法
场景二
场景二: 用户——>调用Controller(bean)的save方法——>调用Service的mySave方法——>this(当前类的对象)调用doSave方法(该方法上有@Transactional注解),向数据库插入数据
代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
public void mySave(User user) throws RuntimeException {
this.doSave(user);
}
@Transactional
public void doSave(User user){
super.save(user);
}
}
分析
谁调用的加了注解的方法?当前对象调用的,没事务
场景三
场景三: 用户——>调用Controller(bean)的save方法——>调用Service的mySave方法——>service(bean)调用doSave方法(该方法上有@Transactional注解),向数据库插入数据
代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
public void mySave(User user) throws RuntimeException {
userService.doSave(user);
}
@Transactional
public void doSave(User user){
super.save(user);
}
}
分析
谁调用的加了注解的方法?bean调用的,有事务;事务范围Service的doSave方法
场景四
场景四: 用户——>调用Controller(bean)的save方法——>调用Service的mySave方法(该方法上有@Transactional注解)——>this(当前类的对象)调用doSave方法,向数据库插入数据
代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
@Transactional
public void mySave(User user) throws RuntimeException {
this.doSave(user);
}
public void doSave(User user){
super.save(user);
}
}
分析
谁调用的加了注解的方法?bean调用的,有事务;事务范围Service的mySave方法
场景五
场景五: 用户——>调用Controller(bean)的save方法——>调用Service的mySave方法(该方法上有@Transactional注解)——>this(当前类的对象)调用doSave方法(该方法为私有方法),向数据库插入数据
代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
@Transactional
public void mySave(User user) throws RuntimeException {
this.doSave(user);
}
private void doSave(User user){
super.save(user);
}
}
问题
问:事务能生效吗?
小结
AOP失效的场景,@Transactional也会失效。
源码分析
如果是代理对象,调用事务方法,那么会走下面的增强方法
TransactionInterceptor#invoke
@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
// The TransactionAttributeSource should be passed the target class
// as well as the method, which may be from an interface.
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}
具体逻辑在这个方法中
TransactionAspectSupport#invokeWithinTransaction
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
// 1、获取对应事务属性
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
// 2、获取 beanFactory 中的 transactionManage,确定事务管理器
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
// 获取需要被代理的方法
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// 3、如果是声明式事务 @Transactional,则走这段逻辑
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
// 4、创建 TransactionInfo
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
// 5、执行目标的方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
// 6、异常处理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// 7、提交事务前清除事务信息
cleanupTransactionInfo(txInfo);
}
// 8、提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
// 3、如果是编程式事务,则走下面逻辑。这里我们忽略他
else {
.....
}
}
综上所述,@Transactional的处理步骤大致如下:
- 获取事务属性。
- 确定事务管理器,加载配置中配置的TransactionManager。
- 不同的事务处理方式使用不同的逻辑。
- 创建 TransactionInfo,为了在目标方法执行前获取事务并且收集事务信息。
- 执行目标方法。
- 异常处理。
- 提交事务前清除事务信息。
- 提交事务。 接下来看看创建TransactionInfo的方法
TransactionAspectSupport#createTransactionIfNecessary
protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
// If no name specified, apply method identification as transaction name.
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
// 获取事务信息
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
// 构建事务信息
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
该方法主要做了两件事情
- 获取事务。
- 构建事务信息。 获取事务信息的方法
AbstractPlatformTransactionManager#getTransaction
@Override
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
// 真正的获取 transaction的方法
Object transaction = doGetTransaction();
// Cache debug flag to avoid repeated checks.
boolean debugEnabled = logger.isDebugEnabled();
if (definition == null) {
// Use defaults if no transaction definition given.
definition = new DefaultTransactionDefinition();
}
// 判断当前线程是否存在事务
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(definition, transaction, debugEnabled);
}
// Check definition settings for new transaction
// 验证事务超时的设置
if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
}
// No existing transaction found -> check propagation behavior to find out how to proceed.
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
}
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
// 这个方法很重要,后面会具体分析,可以说事务就是从这个方法开始的
doBegin(transaction, definition);
// 根据需要初始化事务同步
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException | Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
else {
......
}
}
跟进doBegin方法
DataSourceTransactionManager#doBegin
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
// 1、判断是否复用当前线程连接
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
Connection newCon = obtainDataSource().getConnection();
if (logger.isDebugEnabled()) {
logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
// 2、设置隔离级别
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
// 如有必要,切换到手动提交。
// 这在一些 JDBC 驱动程序中非常昂贵,
// 所以我们不想不必要地这样做(例如,如果我们已经显式配置连接池以设置它)。
// 3、设置手动提交,并交给spring管理
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
// 4、设置事务是否激活为true
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);
int timeout = determineTimeout(definition);
// 5、设置超时时间
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// 6、将连接绑定到当前线程
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, obtainDataSource());
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
综上所述,我们调用doBegin方法时,大致流程如下:
- 判断是否复用当前线程连接
- 设置隔离级别
- 设置手动提交,并交给spring管理
- 设置事务是否激活为true
- 设置超时时间
- 将连接绑定到当前线程 这一条线走完了,让我们回到
TransactionAspectSupport#createTransactionIfNecessary#prepareTransactionInfo
protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
@Nullable TransactionAttribute txAttr, String joinpointIdentification,
@Nullable TransactionStatus status) {
TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
if (txAttr != null) {
// We need a transaction for this method...
if (logger.isTraceEnabled()) {
logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
// 如果不兼容的 tx 已经存在,事务管理器将标记错误。
txInfo.newTransactionStatus(status);
}
else {
// The TransactionInfo.hasTransaction() method will return false. We created it only
// to preserve the integrity of the ThreadLocal stack maintained in this class.
if (logger.isTraceEnabled()) {
logger.trace("Don't need to create transaction for [" + joinpointIdentification +
"]: This method isn't transactional.");
}
}
// We always bind the TransactionInfo to the thread, even if we didn't create
// a new transaction here. This guarantees that the TransactionInfo stack
// will be managed correctly even if no transaction was created by this aspect.
txInfo.bindToThread();
return txInfo;
}
该方法就是构造了一个TransactionInfo对象,并将他绑定给了线程。 接着我们回到最初的方法
TransactionAspectSupport#invokeWithinTransaction 第五步执行了目标方法,如果报错,那么会调用以下方法
TransactionAspectSupport#completeTransactionAfterThrowing
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
// 这里是@Transactional默认哪种异常会尝试进行回滚
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
try {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by commit exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by commit exception", ex);
throw ex2;
}
}
}
}
默认调用的回滚配置
DefaultTransactionAttribute#rollbackOn
public boolean rollbackOn(Throwable ex) {
// 默认运行时异常,或者error才回滚
return (ex instanceof RuntimeException || ex instanceof Error);
}
源码调试
我们针对场景五进行调试
关键依赖版本如下:
<!--spring-web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
阅读静态源码后,标记的断点如下:
结果:
调用Service的mySave方法(该方法上有@Transactional注解)的时候,就进入了invoke方法,事务生效了。
例子地址
这里不提供完整代码例子,提供mybatis-plus的半成品小例,读者可以自行修改尝试。
从生产问题再看@Transactional
这是小七在生产中遇到的一个问题,大体上是为一个第三方服务增加一个重试和兜底方案。因为只是一个单纯的http调用,小七第一时间就想到了spring的retry框架,结果用上@retry后,非但没有重试,反而报数据库表被锁的异常。查看了以前的代码,发现正常流程下,代码是ok的,但是异常流程直接GG,遂做以下记录。
例子
伪代码如下:
@Transactional
public void mytest() throws InterruptedException {
System.out.println("开始执行主线程逻辑...");
User user = new User();
user.setName("测试");
user.setPassword("1222");
super.save(user);
// 错误一:另起线程相当于,另外开启了一个事务,一旦发生异常不会回滚主线程的事务
ThreadPoolUtil.execute(()->{
// 错误二:该方法中再次对user表进行了操作(不同事务操作同一张表,极易造成锁表)
this.mytest1(user);
});
// 模拟主线程剩下的操作(请求三方接口,http连接保持一分钟)
// 错误三:结合错误二来看,主线程长时间不提交事务,子线程也不能提交事务,最后会造成数据库被锁
Thread.sleep(60000);
System.out.println("主线程逻辑执行完毕...");
}
@Transactional
private void mytest1(User user){
// 一系列操作复杂逻辑操作
System.out.println("开始执行子线程逻辑...");
user.setName("测试2");
super.updateById(user);
if (1==1){
throw new RuntimeException("更新操作出错");
}
System.out.println("开始执行子线程执行完成...");
}
小结
1、尽量避免在事务中远程调用等响应时间过长的方法
2、避免AOP失效的场景下使用@Transactional
加餐
@Transactional与@Test一起使用的时候
这个场景下会默认rollback=true,也就是说,不管有没有异常,事务都是会回滚的
@Transactional标记的方法有锁的时候
伪代码如下:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
@Transactional
public void mySave(User user) throws RuntimeException {
this.doSave(user);
}
private synchronized void doSave(User user){
super.save(user);
}
}
这个时候锁是有可能会失效的,特别是在高并发的情况下。为什么呢?通过源码我们可以知道, synchronized代码块里执行的内容是在事务里面的,在事务commit前可能会有多个线程进入代码块,导致读取的数据都是一致的,不是更新后的,也就是说事务的范围大于了锁的范围,造成锁失效的情况。
怎么解决这个问题呢?要么升级锁的范围,要么降低事务的范围。但是如果真的遇到了这种问题,多半是设计不合理,小七觉得最好还是好好梳理一下代码,看看怎么重构好。
Transaction rolled back because it has been marked as rollback-only
伪代码如下:
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
UserService userService;
@Override
@Transactional
public void mySave(User user) throws RuntimeException {
super.save(user);
try {
userService.doSave(user);
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional
public void doSave(User user){
super.save(user);
System.out.println(1/0);
}
}
事物的传播性默认为Propagation.REQUIRED,如果有事务存在,则加入原事务,如果不存在则开启新事务。
这里我们可以将mySave和doSave看成同一个事务中。
如果doSave方法发生异常,那么这个事务被标记成要回滚了,也就是说doSave是想要回滚的,但是mySave中却将这个异常捕获了,那么mySave肯定会想要提交事务,你说这个时候Spring能怎么办?他肯定是懵逼的,不得不说Spring的作者是真牛逼,他早就想到了这种情况,于是Spring的作者写了以下代码:
AbstractPlatformTransactionManager#processRollback
// Raise UnexpectedRollbackException if we had a global rollback-only marker
if (unexpectedRollback) {
throw new UnexpectedRollbackException(
"Transaction rolled back because it has been marked as rollback-only");
}
这就是事务嵌套抛出异常的原因。
总结
本文只是一个对@Transactional的简单入门,带着读者初略的看了一下源码,至于很多细节都没涉及,因为篇幅有限,感兴趣的读者,可以自行阅读。
转载自:https://juejin.cn/post/7296371846755450914