基本介绍
事务是数据一致性最基本的保证,也就是说一个事务中的操作要么都成功,要么都失败,不允许部分成功。我们常说的事务就是jdbc事务,当然Java中还有其他事务,并且在使用jdbc事务有很多注意点,请详细了解“注意点”中的内容。但是这里有个误区,因为我们一般是使用spring的注解@Transactional来实现事务,所以很多人会认为spring提供了事务,其实spring本身并没有提供事务,它只是对jdbc的事务进行了封装,然后通过AOP动态代理来实现事务的功能,这样简化了jdbc事务调用的相关步骤,让我们更专注于业务功能。
注意关键词“动态代理”,这意味着要生成一个代理类,那么我们就不能在一个类内直接调用事务方法,否则无法代理,而且该事务方法必须是public,如果定义成 protected、private 或者默认可见性,则无法调用!
实现原理
注意点(容易引起事务不生效)
- 使用的数据库引擎是否支持事务
- 加事务的方法必须是public,否则事务不起作用(这一点由Spring的AOP特性决定的,理论上而言,不public也能切入,但spring可能是觉得private自己用的方法,应该自己控制,不应该用事务切进去吧)。另外private 方法, final 方法 和 static 方法不能添加事务,加了也不生效
- 处理的业务和事务的入口必须在一个线程内,否则事务不生效
- 对于jdbc事务而言,必须是一个connection中才有效的
- Spring的事务管理默认只对出现运行期异常(java.lang.RuntimeException及其子类)进行回滚(至于为什么spring要这么设计:因为spring认为Checked的异常属于业务的,coder需要给出解决方案而不应该直接扔该框架)。如果业务需要,一定要抛出checked异常的话,可以通过rollbackFor属性指定异常类型即可。
- 确认调用的类是否被代理了
- 在本类中调用另一个带有事务的方法,事务时不生效的。这个时最容易犯的错误,不生效的原因主要是和事务的实现原理有关
- @EnableTransactionManagement // 启注解事务管理,等同于xml配置方式的 <tx:annotation-driven />备注:本系列所有博文的讨论都针对于springboot而不再对spring做说明
- 事务注解不能注到接口上,要写到具体类上,否者不生效
类型
- JDBC事务
- JAT事务
- 容器事务
特性
- 原子性:一个事务中所有对数据库的操作是一个不可分割的操作序列,要么全做,要么全部做。
- 一致性:数据不会因为事务的执行而遭到破坏。
- 隔离性:一个事务的执行,不受其他事务(进程)的干扰。既并发执行的个事务之间互不干扰。
- 持久性:一个事务一旦提交,它对数据库的改变将是永久的。
7种传播属性
- PROPAGATION_REQUIRED -- 支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。
- PROPAGATION_SUPPORTS -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
- PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
- PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。会启动一个独立的新事务,这个事务将被完全 commited 或 rollback 而不依赖于外部事务,它拥有自己的隔离范围,自己的锁等等。当内部事务开始执行时,外部事务将被挂起,内务事务结束时,外部事务将继续执行
- PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。会开始一个 "嵌套的" 事务,它是已经存在事务的一个真正的子事务。 嵌套事务开始执行时,它将取得一个
savepoint
,如果这个嵌套事务失败,将回滚到此savepoint
。 嵌套事务是外部事务的一部分,只有外部事务结束后它才会被提交,这个规则同样适用于rollback。
前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。 它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)
基本场景
图一:事务不生效:.@Transactional的事务开启 ,或者是基于接口的 或者是基于类的代理被创建。所以在同一个类中一个无事务的方法调用另一个有事务的方法,事务是不会起作用的(这就是业界老问题:类内部方法调用事务不生效的问题原因)。
图二:事务生效
图三:事务生效
图四:事务生效
图五:事务生效(稍微解释一下,这里虽然是方法内部调用,但是事务切入了addInfo方法,所以即使内部抛出异常,也是可以生效的。当年我竟然惊讶,看来还是太年轻,哈哈)
图六:事务不生效(准确的说这叫没有事务)
图七:如何使本类中调用方法的事务生效。
这是我们解决方法内部调用事务不生效的最常用方法之一:内部维护一个注入自己的Bean,然后使用这个属性来调用方法。其实还有一种方法,那就是利用Aop上下文来获取代理对象(((TestService)AopContext.currentProxy()).create(); ),然后通过代理对象来调用。这里需要注意:Aop上下文spring默认是关闭的,需要手动开启:<aop:aspectj-autoproxy expose-proxy="true"/>
嵌套事务
七种事务传播属性的应用
1: REQUIRED
加入当前正要执行的事务不在另外一个事务里,那么就起一个新的事务:
比如说,DemoServiceB.demoMethodB的事务级别定义为REQUIRED, 那么由于执行DemoServiceA.demoMethodA的时候,DemoServiceA.demoMethodA已经起了事务,这时调用DemoServiceB.demoMethodB,
DemoServiceB.demoMethodB看到自己已经运行在DemoServiceA.demoMethodA的事务内部,就不再起新的事务。而假如DemoServiceA.demoMethodA运行的时候发现自己没有在事务中,他就会为自己分配一个事务。
这样,在DemoServiceA.demoMethodA或者在DemoServiceB.demoMethodB内的任何地方出现异常,事务都会被回滚。即使DemoServiceB.demoMethodB的事务已经被提交,但是DemoServiceA.demoMethodA在接下来fail要回滚,DemoServiceB.demoMethodB也要回滚
REQUIRED保证其处理过程同一个事务,如果调用的同一个类的配置的REQUIRED的方法,且此方法存在TRY CATCH代码块, 如果此代码块出现异常,程序可以继续执行。这其实使因为同类调用的方法的事务根本不生效。
但如果调用的其他类的配置REQUIRED方法,且TRY CATCH住,则全部的提交全部回滚,且报出异常:Transaction rolled back because it has been marked as rollback-only因为事务报出异常后要全部回滚,包括父类的调用。
如果service中包含多个dao的方法,其都属于同一个事务,其中报错全部回滚,try catch住不影响程序代码的继续执行.
2: SUPPORTS
如果当前在事务中,即以事务的形式运行,如果当前不再一个事务中,那么就以非事务的形式运行
3: MANDATORY
必须在一个事务中运行。也就是说,他只能被一个父事务调用。否则,他就要抛出异常
4: REQUIRES_NEW
这个就比较绕口了。 比如我们设计DemoServiceA.demoMethodA的事务级别为REQUIRED,DemoServiceB.demoMethodB的事务级别为REQUIRES_NEW,
那么当执行到DemoServiceB.demoMethodB的时候,DemoServiceA.demoMethodA所在的事务就会挂起,DemoServiceB.demoMethodB会起一个新的事务,等待DemoServiceB.demoMethodB的事务完成以后,
他才继续执行。他与REQUIRED 的事务区别在于事务的回滚程度了。因为DemoServiceB.demoMethodB是新起一个事务,那么就是存在
两个不同的事务。如果DemoServiceB.demoMethodB已经提交,那么DemoServiceA.demoMethodA失败回滚,DemoServiceB.demoMethodB是不会回滚的。如果DemoServiceB.demoMethodB失败回滚,
如果他抛出的异常被DemoServiceA.demoMethodA捕获,DemoServiceA.demoMethodA事务仍然可能提交。
5: NOT_SUPPORTED
当前不支持事务。比如DemoServiceA.demoMethodA的事务级别是REQUIRED ,而DemoServiceB.demoMethodB的事务级别是NOT_SUPPORTED ,
那么当执行到DemoServiceB.demoMethodB时,DemoServiceA.demoMethodA的事务挂起,而他以非事务的状态运行完,再继续DemoServiceA.demoMethodA的事务。
6: NEVER
不能在事务中运行。假设DemoServiceA.demoMethodA的事务级别是REQUIRED, 而DemoServiceB.demoMethodB的事务级别是NEVER ,
那么DemoServiceB.demoMethodB就要抛出异常了。
7: NESTED
理解Nested的关键是savepoint。他与REQUIRES_NEW的区别是,REQUIRES_NEW另起一个事务,将会与他的父事务相互独立,
而Nested的事务和他的父事务是相依的,他的提交是要等和他的父事务一块提交的。也就是说,如果父事务最后回滚,他也要回滚的。
例如:类serviceA和其中的方法methodA,类serviceB和其中的方法methodB,并且methodA的事务类型为REQUIRED,methodB的事务类型为NESTED.
调用代码如下:
代码一:
ServiceA { /**
* 事务属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
ServiceB.methodB();
} } ServiceB { /**
* 事务属性配置为 PROPAGATION_REQUIRES_NEW
*/
void methodB() {
} }
改成:
代码二:
ServiceA { /**
* 事务属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
try {
ServiceB.methodB();
} catch (SomeException) {
// 执行其他业务, 如 ServiceC.methodC();
}
} }
这种方式也是潜套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点. (题外话 : 看到这种代码, 似乎似曾相识, 想起了 prototype.js 中的 Try 函数 )
如果代码不做任何修改(代码一), 那么如果内部事务(即 ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback (+MyCheckedException),也就是如果设置了rollbackfor,并且methodB抛出的异常符合rollbackfor设置的异常,那么methodA就会回滚,否者不会滚。
学习链接