Spring基础八-Spring事务
前言
实际开发中,我们写入数据时不是单表写入,往往需要同时写入多个表,比如创建一个用户需要在A表中写入账户信息,在B表中写入名字年龄等信息,实际中在A表中写入成功,B表中写入失败,此时如果不添加事务回滚那就造成脏数据,所以B表中写入失败后我们需要将A表的写入回滚,通俗地讲就是两个写入必须都成功或者都失败。
事务的特性
- 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性(Consistency): 执行事务前后,数据保持一致;
- 隔离性(Isolation): 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;
- 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
这四个事务特性简称: ACID.
数据库原生事务
在没有Spring事务管理前我们使用事务是怎样的?
Connection conn = DriverManager.getConnection();
try {
//将自动提交设置为
conn.setAutoCommit(false);
//todo 执行CRUD操作
//当两个操作成功后手动提交
conn.commit();
} catch (Exception e) {
//一旦其中一个操作出错都将回滚,所有操作都不成功
conn.rollback();
e.printStackTrace();
} finally {
conn.colse();
}
获取数据库链接,将自动提交变为false,执行crud操作,统一提交,出现异常回滚,关闭链接,所有的操作都是模版代码。
有了Spring事务管理只需要在需要回滚的方法上加上@Transactional注解,这样就可以进行回滚了。(实际中很多时候都不生效...)
事务管理器——PlatformTransactionManager
PlatformTransactionManager是事务管理器的顶层接口。事务的管理是受限于具体的数据源的(例如,JDBC对应的事务管理器就是DatasourceTransactionManager),因此PlatformTransactionManager只规定了事务的基本操作:创建事务,提交事物和回滚事务。
TransactionDefinition
PlatformTransactionManager中getTransaction(@Nullable TransactionDefinition definition)获取到事务,TransactionDefinition接口中定义了5个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等的常量。
- int getPropagationBehavior(); //返回事务的传播行为
- int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
- String getName(); //返回事务的名字
- int getTimeout(); // 返回事务必须在多少秒内完成
- boolean isReadOnly(); // 返回是否优化为只读事务
多用户访问数据库是常见的场景,这就是所谓的事务的并发。事务并发所可能存在的问题:
-
脏读(Dirty read):当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
-
不可重复读(Unrepeatableread):指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
-
幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
-
丢失更新(Lost to modify):指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
所以JDBC定义了五种事务隔离级别来解决这些并发导致的问题:
- TRANSACTION_NONE JDBC 驱动不支持事务
- TRANSACTION_READ_UNCOMMITTED 允许脏读、不可重复读和幻读。
- TRANSACTION_READ_COMMITTED 禁止脏读,但允许不可重复读和幻读。
- TRANSACTION_REPEATABLE_READ 禁止脏读和不可重复读,单运行幻读。
- TRANSACTION_SERIALIZABLE 禁止脏读、不可重复读和幻读。
隔离级别越高,意味着数据库事务并发执行性能越差,能处理的操作就越少。可以通过conn.setTransactionLevel去设置你需要的隔离级别。
JDBC规范虽然定义了事务的以上支持行为,但是各个JDBC驱动,数据库厂商对事务的支持程度可能各不相同。
出于性能的考虑我们一般设置TRANSACTION_READ_COMMITTED就差不多了,剩下的通过使用数据库的锁来帮我们处理。
Spring 的事务管理都是依赖于数据库的事务来进行的,Spring对其进行封装,让使用更加便捷。
事务传播行为
当事务方法被另一个事务方法调用时(业务层方法之间互相调用的事务问题),必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
支持当前事务的情况
- TransactionDefinition.PROPAGATION_REQUIRED: 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况
- TransactionDefinition.PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
其他情况
- TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED 是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。
Spring注解式事务
@EnableTransactionManagement
使用该注解开启Spring的注解式事务
注解看起来并没有特别之处,都是一些属性的配置。但它却通过@Import引入了另一个配置TransactionManagentConfigurationSelector。
在Spring中,Selector通常都是用来选择一些Bean,向容器注册BeanDefinition的(严格意义上Selector仅时选择过程,注册的具体过程是在ConfigurationClasspathPostProcessor解析时,调用ConfigurationClassParser触发)。
主要的逻辑就是根据代理模式,注册不同的BeanDefinition。
对Proxy的模式而言,注入的有两个:
- AutoProxyRegistrar
- ProxyTransactionManagementConfiguration
AutoProxyRegistrar
Registrar同样也是用来向容器注册Bean的,在Proxy的模式下,它会调用AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);向容器中注册InfrastructureAdvisorAutoProxyCreator。而这个类就是我们上文提到的AbstractAdvisorAutoProxyCreator的子类。
从而,我们完成了我们的第一个条件——AOP代理。
ProxyTransactionManagementConfiguration
ProxyTransactionManagementConfiguration是一个配置类,如果算上其继承的父类,一共是声明了四个类:
- TransactionalEventListenerFactory
- BeanFactoryTransactionAttributeSourceAdvisor
- TransactionAttributeSource
- TransactionInterceptor
后三个类相对比较重要。
-
BeanFactoryTransactionAttributeSourceAdvisor
从名字看就知道这是一个Advisor,那么它身上应该有Pointcut和Advise。其中的Pointcut是TransactionAttributeSourcePointcut,主要是一些filter和matches之类的方法,用来匹配被代理类。而Adivise就是我们之后要介绍的TransactionInterceptor。 -
TransactionAttributeSource
TransactionAttributeSource只是一个接口,扩展了TransactionDefinition,增加了isCandidateClass()的方法(可以用来帮助Pointcut匹配)。这里使用的具体实现是AnnotationTransactionAttributeSource。因为注解式事务候选类(即要被代理的类)是通过@Transactional注解标识的,并且所有的事务属性也都来自@Transactional注解。 -
TransactionInterceptor
刚才我们说了,TransactionInterceptor就是我们找的Advise。
这个类稍微复杂一点,首先根据事务处理相关的逻辑都放在了其父类TransactionAspectSupport中。此外,为了适配动态代理的反射调用(两种代理方式),实现了MethodInterceptor接口。也就是说,反射发起的入口是MethodInterceptor.invoke(),而反射逻辑在TransactionAspectSupport.invokeWithinTransaction()中。我们可以简单看invokeWithTransaction()方法中的部分代码:@Nullable protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, the method is non-transactional. TransactionAttributeSource tas = getTransactionAttributeSource(); final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); //获取事物管理器 final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. // 1. 打开事务(内部就是getTransactionStatus的过程) TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal; try { // This is an around advice: Invoke the next interceptor in the chain. // This will normally result in a target object being invoked. // 2. 执行业务逻辑(具体的crud sql) retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception //3. 出现异常进行事务回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { //4. 重置TransactionInfo ThreadLocal cleanupTransactionInfo(txInfo); } //5. 提交事物 commitTransactionAfterReturning(txInfo); return retVal; } else { //省略... } }
事务失效
- 数据库存储引擎不支持
常见的像mySQL的myISAM存储引擎就不支持事务功能,spring事务是依赖与数据库的引擎,当数据库自身都不支持事务,那Spring的事务肯定是无效的。 - 未指定RollbackOn,且抛出的异常并非RuntimeException
当我们不指定RollbackOn 时默认使用 RuntimeException 与 Error进行回滚,如果抛出的异常不属于这两类但是我们又没有指定RollbackOn回滚就会失效
3. 同一个类中调用事务方法
这是在Proxy模式下才会失效的。
根据上文我们了解了Spring事务是机遇动态代理的,而当在类当中调用事务的方法时,动态代理是无法生效的,因为此时你拿到的this指向的已经是被代理类(Target),而非代理类(Proxy)。
4. non-public的方法导致
如果你将@Transactional注解应用在一个non-public的方法上(即便是protected和defualt的方法),你会发现事务同样不生效(也是在Proxy模式下)。GCLIB的局限应该是在private或是final的方法上,private方法代理失效还能理解,为什么protected和defualt方法也不行呢?其实,non-public方法失效的真正原因不是动态代理的限制,而是Spring有意为之。