事务在Spring中是如何运作的
在了解嵌套事务之前,可以先看下单个事务在Spring中的处理流程,以便后面可以更清晰地认识嵌套事务的逻辑。
Spring事务使用AOP的机制实现,会在@Transactional注解修饰的方法前后分别织入开启事务的逻辑,以及提交或回滚的逻辑。@Transactional可以修饰在方法或者类上,区别就在于修饰于类上的,会对该类下符合条件的方法(例如private修饰的方法就不符合条件)前后都织入事务的逻辑。
spring事务传播机制
声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。 当然了,要想实现事务管理和业务代码的抽离,就必须得用到 Spring 当中的AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
声明式
在配置文件中设置以下6项
required
如果客户端没有事务 在bean中新起一个事务
如果客户端有事务bean 中就加进去
子事务 | 主事务 | 结果 |
异常 | 正常,并try-catch异常 | 均回滚 |
正常 | 异常 | 均回滚 |
正常 | 异常,并try-catch异常 | 不回滚 |
requiresNew
不管客户端有没有事务服务器段都新起一个事务
如果客户端有事务就将事务挂起
子事务 | 主事务 | 结果 |
异常 | 正常,并try-catch异常 | 子回滚,主不回滚 |
正常 | 异常 | 子不回滚,主回滚 |
异常 | 正常 | 均回滚 |
supports
如果客户端没有事务服务端也没有事务
如果客户端有事务服务端就加一个事务
mandatcry
如果客户端没有事务服务端就会报错
如果客户端有事务服务端就加事务
notSupported
不管客户端有没有事务服务端都没有事务
如果客户端有事务服务端就挂起
never
不管客户端有没有事务服务端都没有事务
如果客户端有事务就报错
NESTED
如果当前存在事务,则在嵌套事务内执行。
如果当前没有事务,则进行与REQUIRED类似的操作
子事务 | 主事务 | 结果 |
异常 | 正常,并try-catch异常 | 子回滚,主不回滚 |
正常 | 异常 | 均回滚 |
异常 | 正常 | 均回滚 |
编程式事务
Javax.transaction.UserTranscation
通常情况下只需要一个 @Transactional 就搞定了(代码侵入性降到了最低),就像这样:
/** * 模拟转账 */ @Transactional public void handle() { // 转账 transfer(double money); // 减自己的钱 Reduce(double money); }
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。 你比如说,使用 TransactionTemplate 来管理事务:
@Autowired private TransactionTemplate transactionTemplate; public void testTransaction() { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { try { // .... 业务代码 } catch (Exception e){ //回滚 transactionStatus.setRollbackOnly(); } } }); }
再比如说,使用 TransactionManager 来管理事务:
@Autowired private PlatformTransactionManager transactionManager; public void testTransaction() { TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { // .... 业务代码 transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); } }
就编程式事务管理而言,Spring 更推荐使用 TransactionTemplate。 在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。
事务隔离级别
前面我们已经了解了数据库的事务隔离级别,再来理解 Spring 的事务隔离级别就容易多了。 TransactionDefinition 中一共定义了 5 种事务隔离级别:
ISOLATION_DEFAULT,使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。ISOLATION_READ_UNCOMMITTED,最低的隔离级别,可能会出现脏读、幻读或者不可重复读。ISOLATION_READ_COMMITTED,允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。ISOLATION_REPEATABLE_READ,对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE,最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能。 通常情况下,我们采用默认的隔离级别 ISOLATION_DEFAULT 就可以了,也就是交给数据库来决定。
事务的超时时间
事务超时 timeout ,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。 假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。
事务的只读属性
事务的只读属性readOnly, 如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。 为什么一个查询操作还要启用事务支持呢? 这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。 那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。 有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。
事务的回滚策略
回滚策略rollbackFor,用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
如果你想要回滚特定的异常类型的话,可以这样设置:
@Transactional(rollbackFor= MyException.class)
事务的不回滚策略
不回滚策略noRollbackFor,用于指定不触发事务回滚的异常类型,可以指定多个异常类型。
@Transaction失效场景
- 作用于非public方法上,之所以会失效是因为在Spring AOP 代理时,如下图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute
方法,获取Transactional 注解的事务配置信息。 此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
注意:protected、private修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。
- propagation设置问题,会导致事务不生效,也就事务不会回滚
- rollbackFor指定事务回滚的异常类型
- 同个类中的调用被@transaction修饰的方法,会失效,因为只有当事务方法被当前类以外的代码调用,才会由spring生成的代理对象来管理。
- try catch导致失效
- 数据库不支持事务