前面讲解了Sring的AOP,可以知道它是用来抽取公共代码,增强方法的。而在JDBC操作数据库进行数据处理时,有很多重复的公共代码;事务的提交与回滚跟AOP的约定流程很相似。因此,Spring数据库事务编程的思想基于AOP的设计思想,数据库事务处理是AOP的一种典型应用。
首先我们要对事务常用概念有一个了解。
什么事务:
数据库事务四个特性(ACID):
事务的操作方法:
这里仅讨论声明式事务管理
Spring AOP的约定,会将我们的代码织入到约定的流程中。基于AOP思想的事务处理,也有这样一个约定,其中最重要的注解是@Transactional
@Transactional
TransactionDefinition
里,记录哪些类或方法需要采用什么策略去启动事务功能。Spring数据库事务约定:
具体流程:当事务启动时,Spring会根据事务定义器内的配置设置事务。首先根据传播行为确定事务策略;然后是隔离级别、超越时间、只读等内容设置。直到调用开发者的业务代码,此时若没有异常,Spring数据库拦截器会替我们提交事务;如果发生异常,需要判断事务定义器内配置,若事务定义器约定了该类型异常不回滚,则提交事务;若没有配置或配置回滚,则进行事务回滚并抛出异常。
@Transactional源码
从源码中知可以配置哪些信息:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { //通过bean name制定事务管理器 @AliasFor("transactionManager") String value() default ""; //同value属性 @AliasFor("value") String transactionManager() default ""; String[] label() default {}; //制定传播行为(重点) Propagation propagation() default Propagation.REQUIRED; //制定隔离级别(重点) Isolation isolation() default Isolation.DEFAULT; //制定超时时间(单位秒) int timeout() default -1; String timeoutString() default ""; //是否只读事件 boolean readOnly() default false; //方法在发生指定异常时回滚,默认所有异常回滚 Class<? extends Throwable>[] rollbackFor() default {}; //方法在发生指定异常名称时回滚,默认所有异常回滚 String[] rollbackForClassName() default {}; //方法在发生指定异常时“不”回滚,默认所有异常回滚 Class<? extends Throwable>[] noRollbackFor() default {}; //方法在发生指定异常名称时“不”回滚,默认所有异常回滚 String[] noRollbackForClassName() default {}; }
从上面分析可知,隔离级别isolation与传播行为propagation是@Transactional注解的两个十分重要的配置项,因此这里单独拿出来讲。
丢失更新:
三类读的问题:
四类隔离级别:
用来解决上述三类读问题,隔离级别由高到低分为:未提交读、读写提交、可重复读、串行化。
未提交读:
读写提交:
可重复度:
串行化:
使用合理的隔离级别解决三类读的问题:
考虑性能,在实际中会以读写提交为主,其能防止脏读,不能避免不可重复读与幻读。为了克服数据不一致性与性能问题,可以使用乐观锁或使用Redis作为数据载体。
对于隔离级别,不同数据库支持不同:Oracle支持读写提交和串行化,默认读写提交;MySQL支持4种,默认可重复读。
修改隔离级别的方法:
@Service @Transactional(isolation = Isolation.REPEATABLE_READ) public class UserService{
#隔离级别数字配置含义: #-1 数据库默认隔离级别 #1 未提交读 #2 读写提交 #4 可重复读 #8 串行化 #tomcat数据源默认隔离级别 spring.datasource.tomcat.default-transaction-isolation=2 #dbcp2数据库连接池默认隔离级别 #spring.datasource.dbcp2.default-transaction-isolation=2
传播行为是方法间调用事务采取的策略问题。如在处理批量文件时,大部分成功,小部分失败,我们只希望那小部分失败的回滚。
Spring在Propagation源码中定义了7种传播行为:
public enum Propagation { /** * 需要事务,默认传播行为,如果当前存在事务,就沿用当前事务, * 否则新建一个事务运行子方法 */ REQUIRED(0), /** * 支持事务,如果当前存在事务,就沿用当前事务, * 否则继续采用无事务方式运行子方法 */ SUPPORTS(1), /** * 必须使用事务,如果当前存在事务,就沿用当前事务, * 如果当前没有事务,则会抛出异常 */ MANDATORY(2), /** * 无论当前事务是否存在,都会创建新事务运行方法, * 这样新事务就可以拥有新的锁和隔离级别等特性,与当前事务相互独立 */ REQUIRES_NEW(3), /** * 不支持事务,当前存在事务时,将挂起事务,运行方法 */ NOT_SUPPORTED(4), /** * 不支持事务,如果当前存在事务时,则抛出异常,否则继续使用无事务机制运行 */ NEVER(5), /** * 在当前方法调用子方法时,如果子方法发生异常 * 只回滚子方法执行过的sql,而不回滚当前方法的事务 */ NESTED(6); private final int value; private Propagation(int value) { this.value = value; } public int value() { return this.value; } }
其中,REQUIRED
、REQUIRES_NEW
、NESTED
三种传播行为最常用。
添加传播行为方法:
@Service @Transactional(propagation = Propagation.REQUIRED) public class UserService{
对于NESTED
而言,并不是所有数据库支持保存点技术,因此Spring的内部规则是:如果数据库支持保存点技术,就启用保存点技术;反之则新建一个任务去运行子方法,相当于REQUIRES_NEW
。
NESTED
与REQUIRES_NEW
的区别是:前者会沿用当前事务的隔离级别和锁等特性,后者拥有自己的隔离级别和锁等特性。
一个类自身方法之间的调用,每次调用不能产生新的事务。
失效原因:
AOP原理是动态代理,而自调用是类自身的调用,不是代理对象去调用,就不会产生AOP,开发者代码无法织入到约定流程中去。
自调用失效问题解决: