关于事务的概念
详细可参阅MySQL知识点总结中的“事务”部分
简单来说Spring事务就是接口在执行时,为了保证ACID属性,避免出现并发一致性问题而进行的一组操作
Spring事务的属性
org.springframework.transaction
- 事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上
- 事务属性包含了几个方面:传播行为、隔离规则、回滚规则、事务超时、是否只读
传播行为
-
PropagationBehavior事务传播行为指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何运行。
-
事务传播行为类型 说明 0: PROPAGATION_REQUIRED
如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。外围方法开启事务的情况下 Propagation.REQUIRED
修饰的内部方法会加入到外围方法的事务中,所有Propagation.REQUIRED
修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,整个事务均回滚。1: PROPAGATION_SUPPORTS
支持当前事务,如果当前没有事务,就以非事务方式执行。 2: PROPAGATION_MANDATORY
使用当前的事务,如果当前没有事务,就抛出异常。 3: PROPAGATION_REQUIRES_NEW
新建事务,如果当前存在事务,把当前事务挂起。在外围方法开启事务的情况下 Propagation.REQUIRES_NEW
修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。4: PROPAGATION_NOT_SUPPORTED
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 5: PROPAGATION_NEVER
以非事务方式执行,如果当前存在事务,则抛出异常。 6: PROPAGATION_NESTED
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。外围方法开启事务的情况下 Propagation.NESTED
修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务
隔离规则
主要为了解决并发一致性问题,如何解决可以参考mysql的隔离级别与锁的关系
隔离规则 | 说明 |
---|---|
-1:ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 对于MYSQL来说就是可重复读 |
1:ISOLATION_READ_UNCOMMITTED | 读未提交,防止丢失修改,但会出现脏读、不可重复读、幻读 |
2:ISOLATION_READ_COMMITTED | 读已提交,防止脏读,但会出现不可重复读和幻读 |
4:ISOLATION_REPEATABLE_READ | 可重复读,mysql默认隔离级别,防止不可重复读,但会出现幻读 |
8:ISOLATION_SERIALIZABLE | 可串行化,最高隔离级别,防止幻读,性能极低 |
超时
事务的定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束
是否只读
当代码读取但不修改数据时,可以使用只读事务,加快执行效率。
Spring实现事务
- spring事务分为声明式事务(declarative)和编程式事务(programmatic)
- 声明式事务:基于Spring AOP,通过注解或XML配置实现。有助于用户将操作与事务规则进行解耦,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。显然声明式事务要优于编程式事务,这正是Spring倡导的非侵入式的开发方式。
- 编程式事务:允许用户在代码中精确定义事务的边界,通过编程代码在业务逻辑时需要时自行实现,粒度更小,Spring推荐使用
TransactionTemplate
。
声明式事务
xml形式参考官网示例即可,这里着重记录下注解形式
@Transactional 属性
属性 | 说明 |
---|---|
value | 当在配置文件中有多个TransactionManager , 可以用该属性指定选择哪个事务管理器 |
transactionManager | 同上 |
propagation | 事务的隔离规则,默认值采用 DEFAULT,对于MYSQL来说就是可重复读RR |
isolation | 事务的传播行为,默认为PROPAGATION_REQUIRED |
timeout | Optional transaction timeout. Applies only to propagation values of REQUIRED or REQUIRES_NEW . |
timeoutString | 事务超时时间 |
readOnly | 是否只读事务 |
rollbackFor | 用于指定能够触发事务回滚的异常类型 |
rollbackForClassName | 同上,指定类名 |
noRollbackFor | 用于指定不会触发事务回滚的异常类型 |
noRollbackForClassName | 同上,指定类名 |
@Transactional作用范围
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional
@Target({ElementType.TYPE, ElementType.METHOD})
表明了可以作用于类、接口(包括注释类型)或枚举声明- 作用于类时表示所有该类的public方法都配置相同的事务属性信息,会导致事务控制的粒度太大,注解参数无法根据每个类方法的实际需求设置,因此推荐设置在方法上。
- 作用于方法时,当类和方法都配置了该注解,方法的事务会覆盖类的事务配置信息。
- 作用于接口:不推荐这种使用方法
- 首先接口的设计应该是面向抽象和规范的,而事务注解属于实现细节。将事务注解直接放在接口上,会暴露框架相关的实现细节(代理对象需要实现接口),破坏了接口的抽象性。
- 接口上的注解会被所有实现该接口的类继承(@Inherited),这可能导致事务行为的不一致或冲突。不同实现类对事务的要求可能不同,有些实现类可能需要使用其他的事务管理方式,将事务注解放在接口上会限制了灵活性。
- 将事务注解放在接口上,意味着对该接口所有方法都启用了事务。但实际应用中,不同的方法可能有不同的事务需求,有些方法可能并不需要事务支持。过度使用事务会增加数据库的负担,并可能导致性能问题。
@Inherited
表明了子类可以继承父类的事务属性
@Transactional的异常回滚
- spring默认只在运行时异常(runtimeException)和error中会进行回滚,可检查异常并不会回滚。
- 可检查异常虽说像
IOException
会强制进行try…catch的处理,但还有一些不需要try catch处理也不属于RuntimeException的异常,所以进行rollbackFor配置还是比较健全的手段@Transactional(rollbackFor = Exception.class)
- 假如业务有对可检查异常进行处理,则无需加
@Transactional(rollbackFor = Exception.class)
@Transactional的原理
-
基于SpringAOP去实现,默认是JDK动态代理,若目标类无实现接口,则为CGLib。(用的around通知)
-
(1)判断生成代理对象:通过@Transactional注解来标记方法(定义切点),在Bean初始化过程中判断是否要对当前Bean创建代理对象,并且拿到@Transactional注解的属性;
(2)定义代理对象的回调逻辑,即执行代理逻辑:在执行目标方法前打开事务,执行过程中捕获异常执行回滚逻辑,在执行完目标方法后提交事务;
@Transactional的失效场景
1.目标方法非public方法
2.目标方法用final/static修饰
- 无论是JDK动态代理还是CGLib,都需要重写或继承被代理类的方法,final和static不允许重写,所以会失效
- JDK动态代理是jvm通过继承
proxy
类的protected InvocationHandler
,从而调用其invoke方法。因为java为单继承,所以通过实现接口的方式来实现拓展。这也是为什么jdk动态代理一定要求要接口实现类的原因。
3.同一个类中的方法直接内部调用
- 方法被事务管理是因为Spring AOP为其生成代理了对象,但是直接this调用同类方法,调用的是被代理类对象的方法,而非代理类方法,因此,在同类中的方法直接内部调用,会导致事务失效
- 同类调用,不涉及事务传播,相当于A调用了this.B,那么只是把代码融合到一块,并无把B的事务属性给带过去。
4.事务方法所在的类未被Spring管理
- 使用Spring事务的前提是:对象要被Spring IOC容器管理,需要创建bean实例;打了注解,但是忘了在当前类加@Service注解,导致事务不生效。
5.事务定义了为异步方法
- 如果使用了 Spring 的 @Async 注解将方法标记为异步执行,@Transactional 注解可能会失效。这是因为异步方法会在新的线程中执行,而事务是与线程绑定的,因此在新的线程中无法获取到原始线程上下文的事务信息,导致事务失效。
编程式事务
上面的这些内容都是基于@Transactional注解的,这种事务使用方式叫做声明式事务;其实,Spring还提供了另外一种创建事务的方式,即通过硬编码的方式使用Spring中提供的事务相关的类来控制事务,这种方式叫做编程式事务;
编程式事务主要有两种用法:
- 通过事务管理器PlatformTransactionManager控制事务
- 通过事务模板TransactionTemplate控制事务;常用的是TransactionTemplate,如下:
@Resource
private DataSourceTransactionManager transactionManager;
// 同类方法调用 使用编程式事务
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(transactionStatus -> {
queryData();
insertData();
return Boolean.TRUE;
});
});
- 避免了由于Spring AOP问题导致的事务失效的问题,如同类事务方法调用;
- 可以对代码块加事务,能够更小粒度的更精确的控制事务的范围,一定程度避免事务滥用;
- 但是耦合性高,是直接代码侵入业务的操作