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. 总结
本文我们探讨了事务的传递与隔离属性,并详细解释了不同属性的含义及对并发事务的影响。