【译】Spring中@Transcational注解的事务隔离级别与传播行为

本文最后更新于:2020年9月10日 下午

前言

在本教程中,我们将学习@Transactional注解的事务隔离级别和传播行为。

@Transcational注解是什么?

我们可以使用@Transactional注解为一个方法添加事务支持。

我们可以通过它指定设置事务的传播行为、超时时间与回滚条件。此外,我们还可以通过它指定一个事务管理器。

Spring通过创建代理对象或者操纵字节码来实现事务的创建、提交与回滚。

在通过代理模式实现事务管理的情况下,对于来自类内部的调用@Transactional会失效。

简单地说,假如我们有一个叫callMethod的方法而且该方法被@Transactional注解标记过。

Spring将使用一些用于事务管理的代码将这个方法进行包装,@Transactional注解的实现可以使用下面的伪代码表示:

// 如有必要则创建事务
createTransactionIfNecessary();
try {
    // 调用callMethod方法
    callMethod();
    // 调用成功则提交事务
    commitTransactionAfterReturning();
} catch (exception) {
    // 调用失败则回滚事务
    completeTransactionAfterThrowing();
    throw exception;
}

怎样使用@Transcational注解

我们可以在接口、类、方法上面使用这个注解,位于接口、类、方法上的@Transactional注解会按照不同的优先级发生覆盖,优先级底的将被高的覆盖掉,覆盖的优先级从低到高分别是:

  • 接口
  • 超类
  • 接口方法
  • 超类方法
  • 类方法

Spring会把一个类上标注的@Transactional注解应用到该类所有public的方法上,所以我们不需要再单独给方法上标注@Transactional

但是,如果我们在privateprotected方法上标注@Transcational注解,那么Spring将会忽略这些注解而且不给出任何错误提示。

让我们从在接口上标注@Transcational注解开始:

@Transactional
public interface TransferService {
    void transfer(String user1, String user2, double val);
}

通常情况下,不推荐在接口上标注@Transactional注解,然而在某些接口上使用也是可以接受的,比如:Spring Data的@Repository接口。

我们可以将这个注解标注在类的定义上,这样可以覆盖接口或超类中标注的@Transactional注解:

@Service
@Transactional
public class TransferServiceImpl implements TransferService {
    @Override
    public void transfer(String user1, String user2, double val) {
        // ...
    }
}

现在,让我们将这个注解直接标注在方法的定义上:

@Transactional
public void transfer(String user1, String user2, double val) {
    // ...
}

事务传播行为

事务传播行为定义了我们业务逻辑的事务边界,Spring根据我们配置的事务传播行为开始或者中断事务。

Spring通过调用TransactionManager::getTransaction方法然后根据事务的传播行为来获取或者创建一个事务,它支持部分在TransactionManager中定义的事务传播行为,但是还有一些特殊的事务传播行为需要通过一些特殊的 TransactionManager实现去支持。

REQUIRED

REQUIRED是默认的事务传播行为。对于该传播行为而言,Spring会检查当前线程上下文中是否存在一个活跃的事务。

如果不存在活跃的事务则会创建一个新的事务。

如果存在一个活跃的事务则将当前的业务逻辑算入这个活跃的事务范围中:

@Transactional(propagation = Propagation.REQUIRED)
public void requiredExample(String user) { 
    // ... 
}

由于REQUIRED是默认的事务传播行为,所以上面的代码可以简写成:

@Transactional
public void requiredExample(String user) { 
    // ... 
}

来我们看一下创建具有REQUIRED传播行为事务的伪代码:

 // 检测是否已经位于事务中
 if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    // 返回已有的事务
    return existing;
}
// 否则不使用事务
return createNewTransaction();

SUPPORTS

对于SUPPORTS而言,Spring会先检查线程上下文中是否存在一个活跃的事务,如果存在一个活跃的事务则使用它。如果没有,则以不使用事务的方式去执行supportsExample()方法::

@Transactional(propagation = Propagation.SUPPORTS)
public void supportsExample(String user) { 
    // ... 
}

让我们看看使用SUPPORTS传播行为创建事务的伪代码:

// 检测是否已经位于事务中
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    // 返回已有的事务
    return existing;
}
// 否则不使用事务
return emptyTransaction;

MANDATORY

当传播行为被设为MANDATORY时,如果Spring没有在线程上下文中找到一个活跃的事务则会抛出一个异常:

@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample(String user) { 
    // ... 
}

让我们看一下该选项对应的伪代码表示:

// 检测是否已经位于事务中
if (isExistingTransaction()) 
{
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    // 返回已有的事务
    return existing;
}
// 否则抛出异常
throw IllegalTransactionStateException;

NEVER

当传播行为被设为NEVER时,Spring在线程上下文中发现一个活跃事务时会抛出一个异常:

@Transactional(propagation = Propagation.NEVER)
public void neverExample(String user) { 
    // ... 
}

让我们看看使用NEVER传播行为创建事务的伪代码:

// 检测是否已经位于事务中
if (isExistingTransaction()) {
    // 如果位于事务中就抛出异常
    throw IllegalTransactionStateException;
}
// 否则不使用事务
return emptyTransaction;

NOT_SUPPORTED

当传播行为被设为NOT_SUPPORTED时,Spring会将线程上下文中现有的事务进行挂起,然后再以不使用事务的方式去执行notSupportedExample()方法:

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedExample(String user) { 
    // ... 
}

JTATransactionManager事务管理器以开箱即用的方式支持真正的事务挂起。其它 的事务管理器则是通过保存现有事务的引用然后将其从线程上下文中清除的方法来模拟事务挂起。

REQUIRES_NEW

当传播行为设为REQUIRES_NEW时,Spring发现线程上下文中存在一个活跃的事务时,则先将其挂起,然后创建一个新的事务去执行requiresNewExample()方法:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewExample(String user) { 
    // ... 
}

NOT_SUPPORTED选项类似,对于真正的事务挂起,我们需要使用JTATransactionManager事务管理器

该选项对应的伪代码实现:

// 检测是否已经位于事务中
if (isExistingTransaction()) {
    // 先挂起已存在的事务
    suspend(existing);
    try {
        // 然后创建一个新事务
        return createNewTransaction();
    } catch (exception) {
        // 如果新事务发生异常则恢复旧事务
        resumeAfterBeginException();
        throw exception;
    }
}
// 没有找到旧事务,直接创建新事务
return createNewTransaction();

NESTED

对于NESTED而言,Spring会检测线程上下文中是否存在活跃的事务。

如果已经存在则在该事务上建立一个保存点,这意味着如果我们的nestedExample()方法发生异常,该事务将会回滚到我们建立的保存点处。

如果没有检测到活跃事务的存在,那么该选项的行为与REQUIRED选项一样。

DataSourceTransactionManager支持以开箱即用的方式使用该选项。此外,JTATransactionManager的某些实现也支持这个选项。

JpaTransactionManager仅对JDBC连接支持NESTED选项。但是,如果我们将nestedTransactionAllowed标志设置为true,而且我们的JDBC驱动程序也支持保存点功能,则它也适用于在JPA事务中的JDBC访问代码。

最后,让我们把传播行为设为NESTED

@Transactional(propagation = Propagation.NESTED)
public void nestedExample(String user) { 
    // ... 
}

事务隔离级别

隔离性是ACID属性中的一个:

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)

隔离性表示的是数据在并发事务间的可见性。

每种级别的隔离可以防止在并发事务中零或多个并发副作用的发生:

  • 脏读:可以读取到并发事务还未提交的更改。
  • 不可重复读:在并发事务中,某个事务对某行数据更改后,其它事务对该行的多次读取可能会获取到不同的结果。
  • 幻读:在并发事务中,如果一个事务添加或删除了一些行并提交,其它事务在重新执行范围查询后可能获得不同的结果。

我们可以通过@Transactional::isolation设置事务的隔离级别。在Spring中,它可以使用下面五个枚举值:

  • DEFAULT
  • READ_UNCOMMITTED
  • READ_COMMITTED
  • REPEATABLE_READ
  • SERIALIZABLE

Spring中的隔离级别管理

在Spring中事务的默认隔离级别是DEFAULT。所以,当Spring创建新事务时,隔离级别是我们数据库中所设置的隔离级别。因此,如果我们在对数据库中的数据进行修改时需要注意这一点。

我们还要思考在调用链中的,当各个方法对应不同的事务隔离级别时会出现什么情况。

通常情况下,在创建事务时也会设置事务的隔离级别,如果我们不想让我们的调用链中的方法存在不同的事务隔离级别,我们可以设置TransactionManager::setValidateExistingTransactiontrue,它的功能使用伪代码表示为:

// 如果隔离级别不是DEFAULT
if (isolationLevel != ISOLATION_DEFAULT) {
    // 当前事务设置的隔离级别与已存在事务的隔离级别不一致则抛出异常
    if (currentTransactionIsolationLevel() != isolationLevel) {
        throw IllegalTransactionStateException
    }
}

现在让我们深入了解不同的隔离级别与其作用。

READ_UNCOMMITTED

READ_UNCOMMITTED是隔离性最低的级别,具有最高的并发访问性能,该级别下会发生脏读、不可重复读与幻读的情况。

我们可以为方法或类设置这个隔离级别:

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void log(String message) {
    // ...
}

Postgres不支持READ_UNCOMMITTED隔离级别,它使用READ_COMMITED隔离级别作为替代。Oracle不支持READ_UNCOMMITTED级别。

READ_COMMITTED

隔离性的第二个级别是READ_COMMITTED。它可以防止脏读的发生。

这个隔离级别下还是存在不可重复读与幻读的情况,所以在并发事务中未提交的更改对我们没有影响,但是,在事务提交后,对同样数据的重新查询可能会发生不同的结果。

下面的代码设置事务的隔离级别为READ_COMMITTED

@Transactional(isolation = Isolation.READ_COMMITTED)
public void log(String message){
    // ...
}

REPEATABLE_READ

隔离性的第三个级别是REPEATABLE_READ。它可以防止脏读、不可重复读的发生。因此,我们不受并发事务中未提交更改的影响。

同样,当我们重新查询一行数据时,我们不会得到不同的结果。但是在重新执行范围查询时,我们可能会获得新添加或删除的行。

此外,它是满足防止更新丢失的要求最低级别,当两个或多个并发事务读取并更新同一行时,就会发生更新丢失。REPEATABLE_READ不允许并发事务同时访问同一行数据。因此,不会出现更新丢失的情况。

下面的代码设置事务的隔离级别为READ_COMMITTED

@Transactional(isolation = Isolation.REPEATABLE_READ) 
public void log(String message){
    // ...
}

REPEATABLE_READ是Mysql中的默认隔离级别。Oracle不支持REPEATABLE_READ隔离级别。

SERIALIZABLE

SERIALIZABLE是隔离等级最高的级别。它可以防止脏读、不可重复读与幻读。

因为它把并发的事务变成了串行的,所以它可能会导致最低的并发访问性能,

换句话说,并行执行一组事务的结果与串行执行它们的结果相同。

现在看一下如何把隔离级别设为SERIALIZABLE

@Transactional(isolation = Isolation.SERIALIZABLE)
public void log(String message){
    // ...
}

总结

在本教程中,我们详细探讨了@Transaction的传播行为。然后,我们学习了并发事务的副作用与隔离级别。

与往常一样,你可以在GitHub上找到完整的代码。