欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring的事物回滚问题

程序员文章站 2022-05-21 19:25:26
...

、概述

想必大家一想到事务,就想到ACID,或者也会想到CAP。但笔者今天不讨论这个,哈哈~本文将从应用层面稍带一点源码,来解释一下我们平时使用事务遇到的一个问题但让很多人又很棘手的问题:Transaction rolled back because it has been marked as rollback-only,中文翻译为:事务已回滚,因为它被标记成了只回滚。囧,中文翻译出来反倒更不好理解了,本文就针对此种事务异常做一个具体分析:

2、栗子

我们如果使用了spring来管理我们的事务,将会使事务的管理变得异常的简单,比如如下方法就有事务:

@Transactional
@Override
public boolean create(User user) {
    int i = userMapper.insert(user);
    System.out.println(1 / 0); //此处抛出异常,事务回滚,因此insert不会生效
    return i == 1;
}
  • 这应该是我们平时使用的一个缩影。但本文不对事务的基础使用做讨论,只讨论异常情况。但本文可以给读者导航到我的另外一篇博文,介绍了事务不生效的N种可能性:【小家java】spring事务不生效的原因大解读

看下面这个例子,将是我们今天讲述的主题:

@Transactional
@Override
public boolean create(User user) {
    int i = userMapper.insert(user);
    personService.addPerson(user);
    return i == 1;
}

//下面是personService的addPerson方法,也是有事务的
@Transactional
@Override
 public boolean addPerson(User user) {
     System.out.println(1 / 0);
     return false;
 }

这种写法是我们最为普通的写法,显然是可以回滚的。但是如果上面这么写:

 @Transactional
 @Override
  public boolean create(User user) {
      int i = userMapper.insert(user);
      try {
          personService.addPerson(user);
      } catch (Exception e) {
          System.out.println("不断程序,用来输出日志~");
      }
      return i == 1;
  }

这里我们把别的service方法try住,不希望它阻断我们的程序继续执行。表面上看合乎情理没毛病,but:
Spring的事物回滚问题

这里需要注意:如果我是这么写:

    @Transactional
    @Override
    public boolean addPerson(User user) {
        userMapper.updateByIdSelective(user);
        try {
            editById(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Transactional
    @Override
    public boolean editById(User user) {
        System.out.println(1 / 0);
        return false;
    }

也是不会产生上面所述的那个rollback-only异常的:

    @Transactional
    @Override
    public boolean addPerson(User user) {
        try {
            editById(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Transactional
    @Override
    public boolean editById(User user) {
        userMapper.updateByIdSelective(user);
        System.out.println(1 / 0);
        return false;
    }

但是,我们的updateByIdSelective持久化是生效了的。分析如下:

  1. 为什么update持久化生效?
    因为addPerson有事务,所以editById理论上也有事务应该回滚才对,但是由于上层方法给catch住了,所以是没有回滚的,所以持久化生效。

  2. 为何没发生roolback-only的异常呢?
    原因是因为editById的事务是沿用的addPerson的事务。所以其实上仍然是只有一个事务的,所以catch住不允许回滚也是没有任何问题的,因为事务本身是属于addPerson的,而不属于editById。

但是我们这么来玩:

  @Transactional
    @Override
    public boolean addPerson(User user) {
        try {
            personService.editById(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Transactional
    @Override
    public boolean editById(User user) {
        userMapper.updateByIdSelective(user);
        System.out.println(1 / 0);
        return false;
    }

就毫无疑问会抛出如下异常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
  • 1

但这么玩,去掉addPerson方法的事务,只保留editById的事务呢?

@Override
    public boolean addPerson(User user) {
        try {
            personService.editById(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Transactional
    @Override
    public boolean editById(User user) {
        userMapper.updateByIdSelective(user);
        System.out.println(1 / 0);
        return false;
    }

发现rollback-only异常是永远不会出来的。

因此我们可以得出结论,rollback-only异常,是发生在异常本身才有可能出现,发生在子方法内部是不会出现的。因此这种现象最多是发生在事务嵌套里。


备注一点:如果你catch住后继续向上throw,也是不会出现这种情况的。


引发了这个血案。这是上面意思呢?其实很好解释:在create准备return的时候,transaction已经被addPerson设置为rollback-only了,但是create方法给抓住消化了,没有继续向外抛出,所以create结束的时候,transaction会执commit操作,所以就报错了。看看处理回滚的源码:

private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
	try {
		boolean unexpectedRollback = unexpected;

		try {
			triggerBeforeCompletion(status);

			if (status.hasSavepoint()) {
				if (status.isDebug()) {
					logger.debug("Rolling back transaction to savepoint");
				}
				status.rollbackToHeldSavepoint();
			}
			else if (status.isNewTransaction()) {
				if (status.isDebug()) {
					logger.debug("Initiating transaction rollback");
				}
				doRollback(status);
			}
			else {
				// Participating in larger transaction
				if (status.hasTransaction()) {
					if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - marking existing transaction as rollback-only");
						}
						doSetRollbackOnly(status);
					}
					else {
						if (status.isDebug()) {
							logger.debug("Participating transaction failed - letting transaction originator decide on rollback");
						}
					}
				}
				else {
					logger.debug("Should roll back transaction but cannot - no transaction available");
				}
				// Unexpected rollback only matters here if we're asked to fail early
				if (!isFailEarlyOnGlobalRollbackOnly()) {
					unexpectedRollback = false;
				}
			}
		} catch (RuntimeException | Error ex) {
			triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN);
			throw ex;
		}

		triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK);

		// 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");
		}
	}
	finally {
		cleanupAfterCompletion(status);
	}
}

简单分析:addPerson()有事务,然后处理的时候有这么一句:
Spring的事物回滚问题
这个时候把参数unexpectedRollback置为false了,所以当create事务需要回滚的时候,如下:
Spring的事物回滚问题
所以,就之前抛出异常了,这个解释很合理了吧。因为之前事务被设置过禁止回滚了。然后遇到了这个问题,我们有没有解决办法呢?其实最简单的决绝办法是:

@Override
public boolean addPerson(User user) {
    System.out.println(1 / 0);
    return false;
}

因为有源码里这么一句话:status.isNewTransaction() 所以我尝试用一个新事务也是能解决这个问题的

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public boolean addPerson(User user) {
    System.out.println(1 / 0);
    return false;
}

但有时候我们并不希望是这样子,怎么办呢?

这个时候其实可以不通过异常来处理,或者通过自定义异常的方式来处理。

**如果某个子方法有异常,spring将该事务标志为rollback only。**如果这个子方法没有将异常往上整个方法抛出或整个方法未往上抛出,那么改异常就不会触发事务进行回滚,事务就会在整个方法执行完后就会提交,这时就会造成Transaction rolled back because it has been marked as rollback-only的异常。

另外一种并不推荐的解决办法如下:

<property name="globalRollbackOnParticipationFailure" value="false" />
  • 1

这个方法也能解决,但显然影响到全局的事务属性,所以极力不推荐使用。

如果isGlobalRollbackOnParticipationFailure为false,则会让主事务决定回滚,如果当遇到exception加入事务失败时,调用者能继续在事务内决定是回滚还是继续。然而,要注意是那样做仅仅适用于在数据访问失败的情况下且只要所有操作事务能提交

Tips:

Spring aop 异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获runtimeException的异常

换句话说:service上的事务方法不要自己try catch(或者catch后throw new runtimeExcetpion()也成)这样程序异常时才能被aop捕获进而回滚。

另外一种方案:
在service层方法的catch语句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚,这样上层就无需去处理异常(这也是比较推荐的做法)

3、使用场景

事务的场景无处不在。而这种场景一般发生在for循环里面处理一些事情,但又不想被阻断总流程,这个时候要catch的话请一定注意了

4、最后

事务被spring包装得已经隐藏了很多细节,方便了我们的同时,也屏蔽了很多底层实现。因此有时候我们对源码多一些了解,能让我们解决问题的时候更加的顺畅