事务
事务是由一句或多句操作语句组成的集合,数据库系统或计算机操作系统需要确保组成事务的语句要么全都执行执行成功,要么全都不执行。
在Spring Boot项目里,是用@Transactional注解来实现事务,请注意@Transactional
注解不仅可以标识在基于JPA的数据库操作方法上,也可以标识在业务处理方法上,也就说在Spring Boot项目里,事务不仅可以由若干条数据库操作语句构成,也可以是由若干个业务操作构成。
要么全做要么全都不做的事务
比如某转账操作包含如下两个操作数据库的动作。
1 从小张的账户里扣除100元
2 往小李的账户里添加100元
如果在第2步时发生异常,即无法向小李的账户里添加100元,那么该转账操作应该是全部撤销,即撤销“从小张账户里扣除100”元的操作。
从中大家能看到,事务有“要么全都做,要么全都不做”的特性,如果事务中有动作执行失败,那么应当撤销事务里已经成功执行的其它操作,从而回退到事务发生前的状态。
对此,一般对事务由如下两类操作。
- 提交事务,这是指事务里的所有操作都正常,这样就能通过提交事务,把相关操作对应的数据修改提交到数据库里。
- 回滚事务,也叫回退事务,这是指事务里有操作发生异常,就需要通过回滚操作,把数据库的状态回滚到事务发生前。
用@Transactional注解管理事务
事务本来是个数据库层面的概念,不过在Spring Boot开发场景里,可以通过事务管理器和@Transactional**注解,在业务层面管理事务,从而无需了解事务在数据库层面的底层实现。
@Transactional注解的常用参数如下表所示。
参数使用范例 | 说明 |
---|---|
timeout = 20 | 事务的超时时间,单位是秒,超过这时间事务还没有返回,则抛出超时异常 |
readOnly = false | 这个事务是不是可读的 |
isolation = Isolation.DEFAULT | 该参数叫事务隔离级别,定义事务并发时的处理方式,建议别设太高,设默认值即可 |
propagation = Propagation.REQUIRED | 该参数叫事务传播机制,定义了当一个事务方法被另一个事务方法调用时,该事务方法应该如何处理 |
rollbackFor = Exception.class | 设置该事务遇到哪类异常时,需要回滚 |
定义事务隔离级别
在操作事务的场景里,为了确保并发读写数据的正确性,引入事务隔离级别这个概念,和它相关的有脏读、不可重复读和幻读这三个概念。
脏读是指一个事务读了其它事务还没有提交的数据。比如张三的工资原本是10000,财务人员把他的工资修改成15000,但没有提交该修改事务,此时另一个事务读取张三的工资 ,发现是15000,但财务又回滚了该修改工资的事务,所以工资又成了10000。在此类场景里,读取到的15000 就是一个脏数据。如果在第一个事务提交前,其它事务不能读取其修改过的值,就能避免该问题。
不可重复读是指一个事务的操作导致另一个事务前后两次读取到不同的数据,比如在事务1中,张三读到自己的工资是10000,但针对工资的操作尚未完成,在另一个事务中,财务修改了他的工资为15000,并提交了事务,这时在事务1中,张三再次读取工资时,就会变成15000,这就叫不可重复读。具体的解决办法是,只有在修改事务完全提交之后,才可以允许读取数据。
幻读是指一个事务的操作会导致另一个事务前后两次查询的结果不同。比如在事务1里,能读取到了5条工资是10000的员工记录,但此时事务2又插入了一条工资是10000的员工记录,那么事务1再次查询“工资是10000的员工”时,就会返回11条数据,这就叫幻读。解决办法是,如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。
对此,可以通过设置@Transactional注解的isolation参数,来具体定义事务隔离级别,从而设置在事务里,对脏读、不可重复读和幻读的允许程度,具体的参数值说明如下表所示。 t
参数取值 | 说明 |
---|---|
Isolation.READ_UNCOMMITTED | 允许脏读、不可重复读和幻读 |
Isolation.READ_COMMITTED | 禁止脏读,但允许不可重复读和幻读 |
Isolation.REPETATABLE_READ | 禁止脏读和不可重复读,但允许幻读 |
Isolation.SERIALIZABLE | 禁止脏读、不可重复读和幻读 |
Isolation.DEFAULT | 采用数据库的默认值 |
这里请注意,并不是事务隔离级别设置得越高就越好,相反在实际开发中,如果把该参数设置过高,甚至还有可能引发产线问题。
假设把该参数设置成禁止脏读、不可重复读和幻读的参数值SERIALIZABLE,如果有执行修改功能的事务未被提交,那么读数据的操作一直会延后直至这些事务提交后。
大家可以想象下,如果执行修改功能的事务运行时间很长(比如半小时),而这个时间段里有请求要查询该数据,那么这个请求就会一直处于等待状态,对应地该请求的数据库连接也会一直持续着。以此类推,如果在事务运行的这段时间里来了足够多的请求,这些请求的连接同样也不会被释放,这样的连接请求积累到一定数量,足以导致数据库崩溃。
所以,没有特殊情况,不要去设置事务隔离级别的参数,用Isolation. DEFAULT所对应的默认值即可。
定义事务传播机制
在**@Transactional注解里,可以通过propagation来定义事务传播方式。在Spring容器里,定义了7种事务传播机制的值,由此规定了在嵌套情况下,事务之间相互调用时的7**种协调方式。
在下表里,整理7种事务传播机制的取值,以及对应的协调事务的方式。
参数取值 | 说明 |
---|---|
Propagation.REQUIRED | 表示当前事务必须在一个具有事务的方法里运行,如果该事务的调用方已经处在一个事务里,那么该事务可以在该外部事务中运行,否则的话得重新开启一个事务。 |
Propagation. SUPPORTS | 表示当前事务方法不需要在事务环境中运行,但如果该事务的调用方已经处在一个事务里,那么本事务也能在调用方的事务中运行。 |
Propagation.MANDATORY | 表示当前事务方法方法必须在一个事务环境中运行,否则将抛异常 |
Propagation.REQUIRES_NEW | 表示当前事务方法必须运行在它自己的独立的事务中,对此数据库系统将为该方法创建一个新的事务。 |
Propagation.NOT_SUPPORTED | 表示该事务方法不应该在一个事务中运行。如果该事务的调用方处在一个事务环境里,那么该事务方法将直到这个事务提交或者回滚才恢复执行。 |
Propagation.NEVER | 表示该事务方法不应该在一个事务中运行,否则就抛出异常。 |
Propagation.NESTED | 表示如果当前事务的调用方法处在一个事务环境中,那么该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。 |
@transactional注解使用建议
由于**@transactional**注解管理着事务,而事务一旦设置不当,就会导致数据库死锁,进而引发产线问题,所以在使用该注解时,一般会遵循如下的原则。
1. 该注解可以修饰在方法上,也可以修饰在类上,如果修饰在类上时,该类里的所有方法都会用事务的方式来管理。从使用原则上来看,事务的作用范围应当尽可能地小,所以如果没有特殊情况,尽量把该注解作用在方法上。
2. 该注解可以作用在业务逻辑类、控制器类等的方法上,如果作用在控制器类的方法上,就表示该方法对应的请求将会以事务的方式来管理,如果作用在业务逻辑类上,则表示该业务动作将被以事务的方式来管理。究竟该作用在哪个级别的方法上?这是由业务需求来决定,没有固定结论。
3. 一般需要用timeout参数来设置事务的超时时间,且超时时间不应设置过长,以免因长时间等待而导致的连接数积累。
4. 对于事务隔离级别和事务传播机制这两个参数,使用时尤其得谨慎,如果没有特殊需求,一般不设置,而采用默认值。