20211030Spring @Transactional 中的事务传播和隔离
1.介绍
介绍 @Transactional 注释,以及它的隔离和传播设置。
2.什么是@Transactional?
可以使用@Transactional 将方法包装在数据库事务中。
它允许为事务设置传播、隔离、超时、只读和回滚条件。 还可以指定事务管理器。
2.1. @Transactional 实现细节
Spring 创建一个代理,或者操作类字节码,来管理事务的创建、提交和回滚。 在代理的情况下,Spring 会忽略内部方法调用中的 @Transactional。
简单地说,如果我们有一个 callMethod 这样的方法,并且我们将它标记为 @Transactional,Spring 将围绕调用的 invocation@Transactional 方法包装一些事务管理代码:
createTransactionIfNecessary();
try {
callMethod();
commitTransactionAfterReturning();
} catch (exception) {
completeTransactionAfterThrowing();
throw exception;
}
2.2. 如何使用@Transactional
可以将注解放在接口、类的定义上,或者直接放在方法上。 它们根据优先级顺序相互覆盖; 从低到高依次是:接口、超类、类、接口方法、超类方法、类方法。
Spring 将类级别的注释应用于没有用 @Transactional 注释的这个类的所有公共方法。
但是,如果将注解放在私有或受保护的方法上,Spring 将忽略它而不会出错。
在接口上添加注解的示例:
@Transactional
public interface TransferService {
void transfer(String user1, String user2, double val);
}
通常不建议在接口上设置@Transactional; 但是,对于带有 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) {
// ...
}
3.事务的传播性
传播性定义了业务逻辑的事务边界。 Spring 设法根据我们设置的传播性设置启动和暂停事务。
Spring 调用 TransactionManager::getTransaction 根据传播来获取或创建事务。 它支持所有类型的 TransactionManager 的一些传播,但有一些只由 TransactionManager 的特定实现支持。
看看不同的传播方式以及它们是如何工作的。
3.1. 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();
3.2. SUPPORTS
对于 SUPPORTS,Spring 首先检查是否存在活动事务。 如果存在事务,则将使用现有事务。 如果没有事务,则以非事务方式执行:
@Transactional(propagation = Propagation.SUPPORTS)
public void supportsExample(String user) {
// ...
}
伪代码如下:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
return emptyTransaction;
3.3. MANDATORY
当传播为 MANDATORY 时,如果存在活动事务,则将使用它。 如果没有活动事务,则 Spring 会抛出异常:
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryExample(String user) {
// ...
}
伪代码如下:
if (isExistingTransaction()) {
if (isValidateExistingTransaction()) {
validateExisitingAndThrowExceptionIfNotValid();
}
return existing;
}
throw IllegalTransactionStateException;
3.4. NEVER
对于 NEVER 传播的事务逻辑,如果有活动事务,Spring 会抛出异常:
@Transactional(propagation = Propagation.NEVER)
public void neverExample(String user) {
// ...
}
伪代码如下:
if (isExistingTransaction()) {
throw IllegalTransactionStateException;
}
return emptyTransaction;
3.5. NOT_SUPPORTED
如果当前存在事务,首先Spring将其挂起,然后在没有事务的情况下执行业务逻辑:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedExample(String user) {
// ...
}
JTATransactionManager支持开箱即用的实际事务暂停。另一些则通过持有对现有挂起的引用,然后从线程上下文中清除它来模拟挂起
3.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();
3.7. NESTED
对于 NESTED 传播,Spring 检查事务是否存在,如果存在,则标记一个保存点。 这意味着如果业务逻辑执行抛出异常,那么事务将回滚到这个保存点。 如果没有活动事务,它的工作方式类似于 REQUIRED。
DataSourceTransactionManager 支持这种开箱即用的传播。 JTATransactionManager 的一些实现也可能支持这一点。
JpaTransactionManager 仅支持 JDBC 连接的 NESTED。 但是,如果我们将 nestedTransactionAllowed 标志设置为 true,那么如果我们的 JDBC 驱动程序支持保存点,它也适用于 JPA 事务中的 JDBC 访问代码。
@Transactional(propagation = Propagation.NESTED)
public void nestedExample(String user) {
// ...
}
4.事务隔离
隔离是常见的 ACID 属性之一:原子性、一致性、隔离性和持久性。 隔离描述了并发事务应用的更改如何彼此可见。
每个隔离级别可防止对事务产生零个或多个并发副作用:
- 脏读:读取并发事务未提交的变化
- 不可重复读:如果并发事务更新同一行并提交,则在重新读取行时获得不同的值
- 幻读:如果另一个事务添加或删除范围内的某些行并提交,则在重新执行范围查询后获取不同的行
可以通过@Transactional::isolation 设置事务的隔离级别。 它在 Spring 中有这五个枚举:DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。
4.1. Spring中的隔离管理
默认隔离级别为 DEFAULT。 因此,当 Spring 创建新事务时,隔离级别将是RDBMS 的默认隔离。 因此,如果更改数据库,应该小心。
还应该考虑调用具有不同隔离的方法链的情况。 在正常流程中,隔离仅在创建新事务时适用。 因此,如果出于某种原因不想让一个方法在不同的隔离中执行,必须将 TransactionManager::setValidateExistingTransaction 设置为 true。
if (isolationLevel != ISOLATION_DEFAULT) {
if (currentTransactionIsolationLevel() != isolationLevel) {
throw IllegalTransactionStateException
}
}
接下来深入了解不同的隔离级别及其影响。
4.2. READ_UNCOMMITTED
READ_UNCOMMITTED 是最低的隔离级别,允许最多的并发访问。
因此,它受到所有三个提到的并发副作用的影响。 具有此隔离的事务读取其他并发事务的未提交数据。 此外,不可重复读和幻读都可能发生。 因此,在重新读取行或重新执行范围查询时会获得不同的结果。
可以在方法或类上设置隔离级别:
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void log(String message) {
// ...
}
Postgres 不支持 READ_UNCOMMITTED 隔离,而是回退到 READ_COMMITED。 此外,Oracle 不支持或允许 READ_UNCOMMITTED。
4.3. READ_COMMITTED
第二个隔离级别 READ_COMMITTED 可防止脏读。
其余的并发副作用仍然可能发生。 所以并发事务中未提交的更改对我们没有影响,但是如果一个事务提交了它的更改,可能会通过重新查询而改变结果。
@Transactional(isolation = Isolation.READ_COMMITTED)
public void log(String message){
// ...
}
READ_COMMITTED 是 Postgres、SQL Server 和 Oracle 的默认级别。
4.4. REPEATABLE_READ
第三级隔离 REPEATABLE_READ 可防止脏读和不可重复读。 所以不会受到并发事务中未提交更改的影响。
此外,当重新查询一行时,不会得到不同的结果。 然而,在重新执行范围查询时,可能会得到新添加或删除的行。
此外,它是防止丢失更新所需的最低级别。 当两个或多个并发事务读取和更新同一行时,就会发生更新丢失。 REPEATABLE_READ 根本不允许同时访问一行。 因此丢失的更新不会发生。
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void log(String message){
// ...
}
REPEATABLE_READ 是 Mysql 中的默认级别。 Oracle 不支持 REPEATABLE_READ。
4.5. SERIALIZABLE
SERIALIZABLE 是最高级别的隔离。 它可以防止所有提到的并发副作用,但会导致最低的并发访问率,因为它按顺序执行并发调用。
换句话说,一组可串行化事务的并发执行与串行执行的结果相同。
@Transactional(isolation = Isolation.SERIALIZABLE)
public void log(String message){
// ...
}