Spring 事务传递与隔离

Spring 事务传递与隔离

本文讨论下Spring注解@Transactional 及其隔离(isolation)和传播(propagation)属性的设置.

1. @Transactional注解

@Transactional注解可以用在数据库事务操作的方法上。并可以设置事务的相关属性:隔离属性(isolation), 超时属性(timeout), 只读属性(read-only)以及回滚条件,也可以指定事务管理器。

1.1. 实现细节

Spring创建代理或操纵类字节码来管理事务的创建、提交和回滚。使用代理方式Spring会忽略内部方法调用事务,即使有@Transactional注解也会被忽略,代理需要通过其他类进行调用。
例如,一个方法callMethod并标记了 @Transactional注解,Spring会包装一些事务管理代码环绕在方法执行过程:

createTransactionIfNecessary();
try {
    callMethod();
    commitTransactionAfterReturning();
} catch (exception) {
    completeTransactionAfterThrowing();
    throw exception;
}

1.2. 使用@Transactional注解

事务注解可以在接口、类或直接在方法上。实际会按照优先级进行覆盖,从低到高优先级为:接口、父类、类、接口方法、父类方法、类方法。

对于类级别注解,则Spring应用注解设置至所有没有标记注解的public方法,如果在private、protecte方法上使用注解,Spring会忽略。
下面通过示例说明:

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

通常不建议在接口上设置事务,但在Spring Data @Repository情况下是可行的。这里在类上增加注解覆盖接口或父类的设置:

@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) {
    // ...
}

2. 事务传播

传播性定义业务逻辑的事务边界。Spring根据传播性设置负责启动或暂停事务。

Spring根据传播属性调用TransactionManager::getTransaction 方法获取或创建事务。它支持所有类型的TransactionManager的部分传播属性,一些传播属性仅被TransactionManager特定实现支持。下面详细描述不同传播属性。

2.1. REQUIRED

REQUIRED是缺省属性。Spring检查是否有活动事务,如果没有则创建新的事务,否则业务逻辑追加至当前活动事务中:

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

缺省属性也可以不指定:

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

对应伪代码如下:

if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return createNewTransaction();

2.2. SUPPORTS

SUPPORTS属性,Spring首先检查是否有活动事务存在,存在则使用,反之,则无事务进行执行。

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

对应伪代码:

if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return emptyTransaction;

2.3. MANDATORY

当属性设置为MANDATORY属性时,如果存在活动事务,则使用之。反之抛异常,即强制使用事务。

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

对应伪代码:

if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
throw IllegalTransactionStateException;

2.4. NEVER

NEVER属性的逻辑:如果存在活动事务则抛异常。

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

对应伪代码:

if (isExistingTransaction()) {
    throw IllegalTransactionStateException;
}
return emptyTransaction;

2.5. NOT_SUPPORTED

如果存在活动事务则Spring首先挂起当前事务,然后业务逻辑在无事务下执行。

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

JTATransactionManager支持开箱即用的事务挂起。其他方法通过持有对事务引用,然后从线程上下文中清除它来模拟挂起。

2.6. REQUIRES_NEW

REQUIRES_NEW属性,如果存在活动事务则Spring挂起当前事务,然后创建新的事务。

@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();

2.7. NESTED

对于NESTED属性,Spring检查是否存在事务,如果存在则标记保存点。意味着如果后续业务执行遇到异常,那么回滚至保存点。如果没有活动事务时与REQUIRED属性一样。

DataSourceTransactionManager 支持该属性,一些JTATransactionManager的实现可能也支持。JpaTransactionManager仅对JDBC连接支持NESTED属性,如果设置nestedTransactionAllowed 属性为true且JDBC驱动支持保存点,那么JPA事务中的JDBC代码也工作。属性设置如下:

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

3. 事务隔离

隔离属性是ACID ( Atomicity, Consistency, Isolation及 Durability)其中之一,隔离描述并发事务应用的更改如何对彼此可见。每种隔离级别防止事务中零个或多个并发副作用:

  • 脏读(Dirty read): 读取并发事务中未提交的信息
  • 不可重复读(Nonrepeatable read): 如果并发事务更新相同行并提交,重复读一行获得不同值
  • 幻读(Phantom read): 如果其他事务增加或删除查询行并提交,则重复查询一定范围记录返回值不同

我们可以通过@Transactional::isolation设置事务的隔离级别。Spring提供了5个枚举值:DEFAULT, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE。

3.1. 缺省隔离属性

当Spring创建新事务时,缺省隔离级别使用RDBMS,因此改变数据库时应该注意。
我们也应该考虑使用不同隔离属性调用一组方法的场景,正常流程隔离仅应用于新事务创建时。因此出于某种原因,不想让一个方法在不同的隔离状态下执行,我们必须将TransactionManager::setValidateExistingTransaction设置为true。伪代码如下:

if (isolationLevel != ISOLATION_DEFAULT) {
    if (currentTransactionIsolationLevel() != isolationLevel) {
        throw IllegalTransactionStateException
    }
}

下面看看其他隔离级别。

3.2. READ_UNCOMMITTED

READ_UNCOMMITTED是最低的隔离级别,最大化允许并发访问。
它受到上述三种并发性副作用的影响。具有此隔离的事务将读取其他并发事务未提交数据,此外不可重复读取和幻读都可能发生。因此我们可以在重新读取行或重新执行范围查询时获得不同的结果。可以在方法或类上设置隔离级别:

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

Postgres 不支持 READ_UNCOMMITTED 隔离属性,会用 READ_COMMITED 代替。Oracle 也不支持 READ_UNCOMMITTED。

3.3. READ_COMMITTED

第二级隔离READ_COMMITTED可以防止脏读,其他并发副作用仍可能发生。并发事务的未提交改变没有影响,但已提交的改变再次查询也会改变。
设置隔离代码:

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

READ_COMMITTED Postgres, SQL Server及Oracle的缺省级别。

3.4. REPEATABLE_READ

第三个隔离级别是REPEATABLE_READ,防止脏读和不可重复读,因此不会受并发事务未提交改变影响。当重复查询行不会得到不同结果,但可能获得新增记录或删除部分行。

该级别可以防止丢失更新,当两个或多个并发事务读并更新相同行会发生丢失更新。REPEATABLE_READ根本不允许同时访问行,因此丢失更新不会发生。

设置代码:

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

REPEATABLE_READ Mysql的缺省级别,Oracle 不支持 REPEATABLE_READ。

3.5. SERIALIZABLE

SERIALIZABLE 是最高隔离级别。可以防止上述所有的问题,但也导致最低并发访问效率,因为并发事务按照顺序执行。也就是并发执行一组SERIALIZABLE级别事务与顺序执行结果一样。设置代码:

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

4. 总结

本文我们探讨了事务的传递与隔离属性,并详细解释了不同属性的含义及对并发事务的影响。

上一篇:Spring数据@Transactional无法回滚


下一篇:@Transactional注解失效的解决方案