说起事务管理,首先我们要明确事务的概念,了解我们为什么要进行事务管理。
事务管理是对于一系列数据库操作进行管理,一个事务包含一个或多个SQL语句,是逻辑管理的工作单元(原子单元) ---- 百度百科
通俗的来说,每当我们使用后台对数据库表进行并发操作时,我们就会接触到事务。如果,我们能确保,不会在同一段时间内,多次访问数据库。那我就无需使用事务管理。
在业务需要在同一时间段内对同一数据库表进行操作时,我们会遇到以下几个问题:
- 脏读 : 一个事务读取到另一事务未提交的更新数据
- 不可重复读 : 在同一事务中, 多次读取同一数据返回的结果有所不同, 换句话说,后续读取可以读到另一事务已提交的更新数据. 相反, "可重复读"在同一事务中多次 读取数据时, 能够保证所读数据一样,也就是后续读取不能读到另一事务已提交的更新数据 ,即:即在一个事务中,两次查询的结果不一致(针对的update操作)
- 虚读(幻读) : 一个事务读到另一个事务已提交的insert数据,即在一个事务中,两次查询的结果不一致(针对的insert操作、delete操作)
我举一个例子来说明这些概念:
现在某一银行系统,有两个用户在同时进行业务操作。用户X:查询自己的余额;用户Y:向用户X的账户转账100元。在用户Y为转账前,用户X查询得自己余额为200元。在用户Y开始转账业务时,用户X可以查询到自己的余额为300元。此时用户Y的转账业务并未提交,用户X就以为转账成功。这就是脏读。
如果用户Y的转账业务最后出现异常,业务未能提交,数据库回滚业务。那此时用户X查询自己的余额,会发现是200。那对用户X而言,在同一业务中对同一数据有不同的查询结果。这就是不可重复读。
那什么是幻读呢?假如用户X在同一个业务中查询两次自己的流水记录。而用户Y正好在用户X两次查询中,转账给用户X。那用户X在同一业务中,查询自己的流水记录的结果就会不同。这就是幻读。
正是因为我们对数据库表操作的并行性,会导致一些系列问题。为了解决这些问题,我们引入了事务管理。
事务管理有四大特性:
- 原子性:事务里面的操作单元不可切割,要么全部成功,要么全部失败
- 一致性:事务执行前后,业务状态和其他业务状态保持一致.
- 隔离性:一个事务执行的时候最好不要受到其他事务的影响
- 持久性:一旦事务提交或者回滚.这个状态都要持久化到数据库中
数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。设置事务隔离级别可以很好的解决事务并发操作中可能会出现脏读,不可重复读,幻读。但是隔离级别越高,数据库的可并发性就越差。
- Read uncommitted:读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。这就会导致出现 脏读、不可重复读、幻读等一些系列问题。所以一般不建议数据库事务设置此隔离级别。
- Read commited:读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。这可以杜绝 脏读 问题。但是这并不能解决不可重复读的问题。
- Repeatable read:重复读,就是在开始读取数据(事务开启)时,不再允许修改操作。这一个隔离级别就 可以很好的防止 在同一事务的两次访问操作。
- Serializable:序列化。Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
除此之外,如果事务中嵌套事务,如何处理事务的优先级、事务的传播行为。
事务的传播行为:事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法时的事务如何传播。
public void methodA(){
methodB();
//doSomething
}
@Transaction(Propagation=XXX)
public void methodB(){
//doSomething
}
代码中methodA()方法嵌套调用了methodB()方法,methodB()的事务传播行为由@Transaction(Propagation=XXX)设置决定。这里需要注意的是methodA()并没有开启事务,某一个事务传播行为修饰的方法并不是必须要在开启事务的外围方法中调用。
SpringBoot 的 @Transactional 可以使我们很方便的声明 事务的管理方式。
使用propagation属性管理事务的传播行为:
- @Transactional(propagation=Propagation.REQUIRED) 如果有事务, 那么加入事务,没有的话新建一个(默认情况下)
- @Transactional(propagation=Propagation.NOT_SUPPORTED)
容器不为这个方法开启事务 - @Transactional(propagation=Propagation.REQUIRES_NEW)
不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务 - @Transactional(propagation=Propagation.MANDATORY)
必须在一个已有的事务中执行,否则抛出异常 - @Transactional(propagation=Propagation.NEVER)
必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反) - @Transactional(propagation=Propagation.SUPPORTS)
如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
使用 isolation 属性管理事务的隔离级别:
- @Transactional(isolation = Isolation.READ_UNCOMMITTED)
读取未提交数据(会出现脏读, 不可重复读) 基本不使用 - @Transactional(isolation = Isolation.READ_COMMITTED)
读取已提交数据(会出现不可重复读和幻读) - @Transactional(isolation = Isolation.REPEATABLE_READ)
可重复读(会出现幻读) - @Transactional(isolation = Isolation.SERIALIZABLE)
串行化,事务之间不相互影响。
除此之外还有一些其他设置:
(1)事务的超时时间(默认是30秒)
- @Transactional(timeout=30) //
(2)用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。
- @Transactional(rollbackFor={RuntimeException.class, Exception.class})
- @Transactional(rollbackFor=RuntimeException.class)
(3)用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。
- @Transactional(rollbackForClassName=“RuntimeException”)
- @Transactional(rollbackForClassName={“RuntimeException”,“Exception”})
(4)用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。
- @Transactional(noRollbackFor=RuntimeException.class)
- @Transactional(noRollbackFor={RuntimeException.class, Exception.class})
(5)该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚
- @Transactional(noRollbackForClassName=“RuntimeException”)
- @Transactional(noRollbackForClassName={“RuntimeException”,“Exception”})
(6)用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false
- @Transactional(readOnly=true)
注意事项
- 在默认的代理模式下,只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。在同一个类中的两个方法直接调用,是不会被 Spring 的事务拦截器拦截,就像上面的 save 方法直接调用了同一个类中的 method1方法,method1 方法不会被 Spring 的事务拦截器拦截。可以使用 AspectJ 取代 Spring AOP 代理来解决这个问题,但是这里暂不讨论。
- @Transactional 注解应该只被应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。