1 事务
1.1 事务管理方式
spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。
1.2 自动提交(AutoCommit)与连接关闭时的是否自动提交
默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,spring会将底层连接的自动提交特性设置为false。
当一个连接关闭时,如果有未提交的事务应该如何处理?JDBC规范没有提及,C3P0默认的策略是回滚任何未提交的事务。这是一个正确的策略,但JDBC驱动提供商之间对此问题并没有达成一致。
1.3 事务隔离级别
隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:
1、TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED
2、TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
3、TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
4、TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
5、TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
MYSQL: 默认为REPEATABLE_READ级别
SQLSERVER: 默认为READ_COMMITTED
脏读 :一个事务读取到另一事务未提交的更新数据
不可重复读 :在同一事务中, 多次读取同一数据返回的结果有所不同, 换句话说,
后续读取可以读到另一事务已提交的更新数据. 相反, "可重复读"在同一事务中多次
读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据
幻读 :一个事务读到另一个事务已提交的insert数据。
1.4 事务传播行为
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
1、TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
2、TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
3、TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
4、TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
5、TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
6、TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
7、TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED
例如:@Transactional(propagation=Propagation.REQUIRED)
1.5 事务超时
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制。@Transactional(timeout=30) //默认是30秒
1.6 事务只读属性
只读事务用于客户代码只读但不修改数据的情形,只读事务用于特定情景下的优化,比如使用Hibernate的时候。默认为读写事务。
@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
只读标志只在事务启动时应用,否则即使配置也会被忽略。启动事务会增加线程开销,数据库因共享读取而锁定(具体跟数据库类型和事务隔离级别有关)。通常情况下,仅是读取数据时,不必设置只读事务而增加额外的系统开销。
1.7 spring事务回滚规则
指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。
默认配置下,spring只有在抛出的异常为运行时unchecked异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常(编译器会检查到的异常叫受检查异常)则不会导致事务回滚。
可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。如: @Transactional(notRollbackFor=RunTimeException.class)、@Transactional(rollbackFor=Exception.class)
还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用setRollbackOnly()后你所能执行的唯一操作就是回滚。
1.8 @Transactional注解
@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。
虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效,因为注解是不能继承的,这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装(将被确认为严重的)。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。
2 Transaction和EntityManager
2.1 关于persistence context(持久化上下文)和database transaction(事务)
@Transactional本身定义了单个事务的范围。这个事务在persistence context的范围内。JPA中的持久化上下文是EntityManager,内部实现使用了Hibernate Session(使用Hibernate作为持久化provider)。持久化上下文仅仅是一个同步对象,它记录了有限集合的Java对象的状态,并且保证这些对象的变化最终持久化到数据库。这是与单个事务非常不同的概念。一个Entity Manager可以跨越多个事务使用,而且的确是这样使用的。EntityManager何时跨越多个事务?最常见的情况是应用使用Open Session In View模式处理懒初始化异常时。这种情况下视图层运行的多个查询处于独立的事务中,而不是单事务的业务逻辑,但这些查询由相同的entity manager管理。另一种情况是开发人员将持久化上下文标记为PersistenceContextType.EXTENDED,这表示它能够响应多个请求。
如何定义EntityManager和Transaction之间的关系?这由应用开发者来选择,但是JPA Entity Manager最常用的方式是“Entity Manager per application transaction”(每个事务都有自己的实体管理器)模式。entity manager注入的常用方法是:
@PersistenceContext
private EntityManager em;
这里默认为“Entity Manager per transaction”模式。这种模式下如果在@Transactional方法内部使用该Entity Manager,那么该方法将在单一事务中运行。EntityManager是一个接口,注入到spring bean中的不是entity manager本身,而是在运行时代理具体entity manager的context aware proxy(上下文感知代理)。
2.2 事务的实现
实现了EntityManager接口的持久化上下文代理并不是声明式事务管理的唯一部分,事实上包含三个组成部分:
EntityManager Proxy本身、事务的切面、事务管理器
这三部分以及它们之间的相互作用如下:。
1、事务的切面
事务的切面是一个“around(环绕)”切面,在注解的业务方法前后都可以被调用。实现切面的具体类是TransactionInterceptor。
事务的切面有两个主要职责:
在’before’时,切面提供一个调用点,来决定被调用业务方法应该在正在进行事务的范围内运行,还是开始一个新的独立事务。
在’after’时,切面需要确定事务被提交,回滚或者继续运行。
在’before’时,事务切面自身不包含任何决策逻辑,是否开始新事务的决策委派给事务管理器完成。
2、事务管理器
事务管理器需要解决下面两个问题:
新的Entity Manager是否应该被创建?是否应该开始新的事务?这些需要事务切面’before’逻辑被调用时决定。事务管理器的决策基于以下两点:
事务是否正在进行
事务方法的propagation属性(比如REQUIRES_NEW总要开始新事务)
如果事务管理器确定要创建新事务,那么将创建一个新的entity manager,entity manager绑定到当前线程,从数据库连接池中获取连接,将连接绑定到当前线程,使用ThreadLocal变量将entity manager和数据库连接都绑定到当前线程。事务运行时他们存储在线程中,当它们不再被使用时,事务管理器决定是否将他们清除。程序的任何部分如果需要当前的entity manager和数据库连接都可以从线程中获取。
3、EntityManager proxy
当业务方法调用entityManager.persist()时,这不是由entity manager直接调用的。而是业务方法调用代理,代理从线程获取当前的entity manager,事务管理器将entity manager绑定到线程。
3 关于UnexpectedRollbackException异常
3.1 正常回滚
### ServiceA.java
public class ServiceA {
@Transactional
public void callB() {
serviceB.doSomething();
}
}
### ServiceB.java
public class ServiceB {
@Transactional
public void doSomething() {
throw new RuntimeException("Service B throw Exception");
}
}
因为在被spring事务代理方法中, 被代理的方法(dosomething())内部有异常抛出,事务会标记为回滚,并在最外层调用那(service里的callB中)回滚。 这种情况下transaction会正常的rollback。
3.2 多层嵌套事务的回滚
在外层调用者对异常进行捕获。
### ServiceA.java
public class ServiceA {
@Transactional
public void callB() {
try {
serviceB.doSomething();
} catch(Exception e) {
e.print();
}
}
}
### ServiceB.java
public class ServiceB {
@Transactional
public void doSomething() {
throw new RuntimeException("Service B throw Exception");
}
}
这个时候,我们再调用ServiceA的callB。程序会抛出org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only这样一个异常信息。因为在ServiceA和ServiceB中的@Transactional propagation都采用的默认值:REQUREID。根据我们前面讲过的REQUIRED特性,当ServiceA调用ServiceB的时候,他们是处于同一个transaction中。当ServiceB中抛出了一个异常以后,ServiceB会把当前的transaction标记为需要rollback(设置rollBackOnly状态),继续执行到serviceA, 可是被ServiceA中的方法捕获了并进行了处理,也就不回滚了,一直执行到最后commit。在commit时spring会判断回滚标志(rollBackOnly状态),若有回滚标记, 回滚并抛出UnexpectedRollbackException异常(因为出现了前后不一致)。
在一个嵌套事务中,只要某个方法边界抛出了事务没有在当前方法被捕获,就会创建一个回滚标记,当最后事务要提交时,观察到最后有回滚标记,就会报该错误。
3.3 多层嵌套事务的回滚
对出异常的事务使用新事务。
### ServiceA.java
public class ServiceA {
@Transactional
public void callB() {
try {
serviceB.doSomething();
} catch(Exception e) {
e.print();
}
}
}
### ServiceB.java
public class ServiceB {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 修改此处
public void doSomething() {
throw new RuntimeException("Service B throw Exception");
}
}
此时,程序可以正常的退出了,也没有抛出UnexpectedRollbackException。
原因是因为当ServiceA调用ServiceB时,serviceB的doSomething是在一个新的transaction中执行的。当doSomething抛出异常以后,仅仅是把新创建的transaction rollback了,而不会影响到ServiceA的transaction。ServiceA就可以正常的进行commit。
4 实体状态转换图
JPA实体状态转化图图中虚线框代表在持久化上下文中,所谓在持久化上下文的意思就是一旦该实体对象处于该环境中的时候,那么此实体的属性发生了任何的改变都会同步到数据库中,无需再自己手工调用管理器的方法,该记录会自动同步,这些操作是由AbstractFlushingEventListener完成。
网友评论