Spring 的事务管理
Spring 的 AOP 我们都知道,面向切面的编程,可以对目标方法进行增强(前置通知、后置通知、返回通知等),而事务我们也知道,数据库在进行操作的时候,需要对修改的东西保持原子性等,对于失败的事务进行回滚,成功的事务进行提交。对应的也就是 rollback 和 commit。
编程式事务管理
也就是直接将事务管理代码切入到业务方法中,来控制事务的提交和回滚,也就是和原始的 JDBC 管理方式类似。
Spring 中事务的相关对象
PlatformTransactionManager
该接口由 Spring 提供的事务管理器,它提供了常用的操作事务的方法。
方法 | 说明 |
---|---|
TransactionStatus getTransaction(TransactionDefination defination) | 获取事务的状态信息 |
void commit(TransactionStatus status) | 提交事务 |
void rollback(TransactioStatus status) | 回滚事务 |
- PlatformTransactionManager 接口类型,不同的 Dao 层级技术则有不同实现类。
- 例如 mybatis 是 org.springframework.jdbc.datasource.DataSourceTransactionManager
- 例如 hibernate 是 org.springframework.orm.hibernate5.HibernateTransactionManager
TransactionDefinition
它是事务的定义信息对象,里面有如下方法
方法 | 说明 |
---|---|
int getIsolationLevel() | 获得事务的隔离级别 |
int getPropogationBehavior() | 获得事务的传播行为 |
int getTimeout() | 获得超时时间 |
boolean isReadOnly() | 是否只读 |
TransactionStatus
事务的状态对象,提供事务具体的运行状态(也就是事务是否回滚等等)
方法 | 说明 |
---|---|
boolean hasSavepoint() | 是否存储回滚点 |
boolean isCompleted() | 事务是否完成 |
boolean isNewTransaction() | 是否为新事务 |
boolean isRollbackOnly() | 事务是否回滚 |
关系
PlatformTransactionManager (平台事务管理器)用来事务如何控制,控制行为,TransactionDefinition 维护事务的属性信息,TransactionStatus 是事务运行过程中,封装一些事务的状态信息,并且这些状态信息,不是自己定义的,而是被动的。所以我们也可以说。
平台事务管理器 + 事务定义信息对象 = 事务状态对象。
声明式事务控制
基于 XML
什么是声明式事务控制
顾名思义,也就是采用声明的方式来处理事务。在 Spring 中,声明式的事务这里也就是在 XML 文件中,配置事务。用 XML 的声明事务来代替代码式的事务处理。
作用
- 事务管理不侵入开发组件,也就是业务逻辑,业务逻辑不会意识到正在事务管理之中。事实也如此,业务代码我们更重要的关注业务逻辑,而事务管理属于系统层级的服务,不也业务逻辑的一部分。如果想要修改事务管理,只需要更改配置文件即可。
- 对于事务管理,不需要改变代码逻辑,只需要对特定的文件修改,就能移除事务管理,重新编译一次即可。
Spring 的事务管理式基于 AOP
实现
如何实现呢?首先要明确
- 谁是切点
- 谁是通知
- 配置切面
切点:
假如我这里有一个 AccountServiceImpl 的 bean 它用来处理各种业务逻辑,所以也就是我们的切点。
<bean id="accountService" class="com.test.service.impl.AccountServiceImpl"></bean>
通知:
也就是事务的增强。(这里要引用 tx 命名空间)
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" isolation="REPEATABLE_READ" read-only="false" propagation="REQUIRED" timeout="-1" rollback-for="IOException.class/>
</tx:attributes>
</tx:advice>
<tx:attributes >
标签是用来设置事务的各种信息,包括哪些方法需要事务管理,事务的隔离级别,事务的传播行为。<tx:method>
代表切点方法的事务参数配置- name 切点方法的名称,这里指定任何名字都可以,可以指定为带有 update*字符 的方法配置事务。
- isolation 事务的隔离级别
- propogation 事务传播行为
- timeout 超时时间
- read-only 是否只读
- rollback-for 事务回滚属性
配置平台事务管理器:
<bean id="platformTransactionManager" class="org.springframework.transaction.PlatformTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
AOP 切入:
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.test.service.impl.*.*(..))" ></aop:advisor>
</aop:config>
<aop:advicor>
标签中, advice-ref 用来指定哪个事务增强器,而 pointcut 为切面表达式,指定类下哪个方法配置事务。
扩展
事务的隔离级别
用来解决事务的脏读、幻读、不可重复读
- Isolation.READ_COMMITTED 只允许事务读取已经被其他事务提交的变更,可以避免脏读,但是幻读和不可重复读的问题仍然不可避
- Isolation.READ_UNCOMMITTED 允许事务读取被其他事务未提交的变更,脏读、幻读和不可重复读的问题都会出现
- Isolation.REPEATABLE_READ 确保事务可以多次重一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复度,但是幻读依然存在
- Isolation.SERIALIZABLE 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对表数据进行增删改操作,所有并发问题的都可以避免,但是性能十分低下
事务的传播行为
假如我有两个方法,buyBooks 是买多本书,而买多本书的逻辑就是多次调用买一本书。
当事务方法被另外一个事务方法调用的时候,那么我们必须要指定事务如何传播,可能存在的情况有:
- buyBooks 中的事务传播到 buyBook 方法中去,而买一本书的方法的事务不起作用。
- buyBooks 中的事务传播到 buyBook 方法中去,但是买一本书方法会使用自己的事务。
所以对于的事务传播方式的选择,有以下几种
-
REQUIRED:如果当前没有事务,就新建-一个事务, 如果已经存在一个事务中,加入到这个事务中。-般的选择(默认值) (常用)
-
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
-
MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常
-
REQUERS_ NEW:使用当前方法的事务,意味着当前方法调用一次,就会提交或者回滚 (常用)
-
NOT_ SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
-
NEVER:以非事务方式运行,如果当前存在事务,抛出异常
-
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行REQUIRED类似的操作
超时时间
由于事务可以在行或者表上获得锁,因此长时间的事务会占用资源,并对整体性能产生影响,所以我们要对事务设置超市,如果超时,事务直接回滚。
- 默认值是-1,没有超时限制。如果有,以秒为单位进行设置
是否只读
可以使用 readOnly 设置只读,只读属性有两个值可以设置
-
true : 表示这个事务只能进行查询操作,不能进行增删改操作 [mysql不支持]
-
false : 默认的,表示非只读事务,可以进行增删改查操作
事务的回滚属性
什么是事务的回滚属性,它指的是如果我们执行事务的过程中出现异常,就会发生回滚,这是一个宽泛的概念,因为有些异常是不会导致事务的回滚。那么哪些会发生回滚?哪些不会?
- Error 错误,事务会回滚
- 受检查异常:除 RuntimeException 及其子类的异常,不会导致事务的回滚,例如 IOException
- 未受检查异常:RuntimeException 及其子类的异常,会导致事务回滚
Spring 的事务是基于 AOP 实现的,AOP 事务回滚的原理是基于在显示的抛出,并且不经过其他处理,AOP 代理才能检测到异常的发生(因为如果你自己先捕捉并且处理了,没有抛出去,肯定也就检测不到了)。才能进行回滚。默认 AOP 只捕获 runtimeexception 的异常,但可以通过。
回到上面的,如果说想要希望受检查的某些异常在出现的情况下也希望能够进行事务回滚,可以使用rollbackFor,**rollbackForClassName **属性设置
rollbackFor= {IOException.class,NotBoundException.class}
rollbackForClassName= {"IOException","NotBoundException"}
如果希望未受检查的某些异常在出现的情况下不进行事务回滚,我们可以使用弄 RollbackFor 和 RollbackForClassName 属性设置
RollbackFor= {RuntimeException.class}
RollbackForClassName= {"RuntimeException"}
基于注解
上面解释了基于 XML 的事务,那么基于注解就简单多了。
步骤
- 声明平台事务管理器
- 启用事务注解
- 使用 @Transactional 注解切点方法(也就是需要事务的方法)
声明平台事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
启用事务注解
<tx:annotation-driven transaction-manager="transactionManager"/>
只有在使用注解配置事务的情况下,才进行此项配置,如果是基于xml的配置,则不需要。
<tx:annotation-driven>
该标签是启用事务注解标签。- transaction-manager 属性:配置事务注解的事务管理器,如果事务管理器的 bean id 名字是 transactionManager,那么可以不用配置该属性。但是如果不是的话,一定要配置该属性。
使用注解
可以查看 @Transactional 的源码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
你可以发现到,很多都是基于 XML 的字眼。propagation 事务传播行为,isolation 事务的隔离级别,timeout 超时时间等等。这里就不多说了。
- propagation 事务传播行为
- isolation 事务的隔离级别
- timeout 超时时间
- readOnly 是否只读
- rollbackFor ,rollbackForClassName 哪些异常回滚
- noRollbackFor,noRollbackForClassName 哪些异常不回滚。
只要在切点方法上(业务方法)写上注解,设置对应的属性即可
@Autowired
private AccountMapper accountMapper;
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public void save(Account account) throws FileNotFoundException {
accountMapper.save(account);
}