SpringBoot通过@Transactional管理事务

SpringBoot通过@Transactional管理事务
强烈推介IDEA2020.2破解激活,IntelliJ IDEA 注册码,2020.2 IDEA 激活码

1. 数据库事务

事务(Transaction),指访问并可能更新数据库中各种数据项的一个程序执行单元(unit),它通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起。当在数据库中更改数据成功时,在事务中更改的数据便会提交,不再改变。否则,事务就取消或者回滚,更改无效。

例如 网上购物,其交易过程至少包括以下几个步骤的操作:

  • 更改客户所购商品的库存信息;
  • 保存客户付款信息;
  • 生成订单并且保存到数据库中;
  • 更改用户相关信息,例如购物数量等。

在正常情况下,这些操作都将顺利进行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果在执行的途中遇到突然断电或者其他意外情况,导致这一系列过程中任何一个环节出了差错,例如在更细商品库存信息时发生异常、顾客银行账户余额不足等,都将导致整个交易过程失败。而一旦失败,我们需要保持数据库的状态不被失败的交易影响:即原有的库存信息没有被更新、用户也没有付款、订单也没有生成。否则,数据库的信息将会不一致(如莫名其妙少了1的库存),或者出现更为严重的不可预测的后果。数据库事务正是用来保证这种情况下交易的平稳性和可预测性的技术。

1.1 事务特性

ACID 表示事务的特性:原子性、一致性、隔离性和持久性。

  • 原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会
    导致整个事务的失败;
  • 一致性(Consistent):事务结束后系统状态是一致的;
  • 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;
  • 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通
    过日志和同步备份可以在故障发生后重建数据。

注意:严格而言,数据库事务属性都是由数据库管理系统来进行保证的,在整个应用程序的运行过程中,应用程序无须去考虑数据库的ACID实现。

一般情况下,通过执行COMMIT(提交)或ROLLBACK(回滚)语句来终止事务。当执行COMMIT语句时,自从事务启动以来对数据库所做的一切更改就成为永久性的,即被写入到磁盘,而当执行ROLLBACK语句时,自从事务启动以来对数据库所做的一切更改都会被撤销,并且数据库中内容返回到事务开始之前所处的状态。无论什么情况,在事务完成时,都能保证回到一致性状态。

1.2 并发控制

数据库系统,一个明显的特点是多个用户共享数据库资源,尤其是多个用户可以同时存取相同数据。保证事务ACID的特性是事务处理的重要任务,而并发操作有可能会破坏其ACID特性。

而DBMS并发控制机制的责任是:对并发操作进行正确调度,保证事务的隔离更一般,确保数据库的一致性。

如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题。由于并发操作带来的数据不一致性包括:

  • 脏读(Dirty Read): A 事务读取 B 事务尚未提交的数据并在此基础上操作,而 B 事务执行回滚,那么 A 读取到的数据就是脏数据。
  • 幻读(Phantom Read):事务 A 重新执行一个查询,返回一系列符合查询条件的行,发现其中插入了被事务 B 提交的行(幻读的重点在于新增或者删除 (数据条数变化)同样的条件, 第1次和第2次读出来的记录数不一样)。
  • 不可重复读(Unrepeatable Read):事务 A 重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务 B 修改过了(不可重复读的重点是修改,同样的条件, 你读取过的数据, 再次读取出来发现值不一样了)。
1.3 事务隔离级别

为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。

  • 读未提交(Read Uncommitted):如果一个事务已经开始写数据,则不允许其他事务同时进行写操作,但允许其他事务读此行数据。最低的隔离级别,最直接的效果就是一个事务可以读取另一个事务并未提交的更新结果。

  • 读提交(Read Committed):通常是大部分数据库采用的默认隔离级别,它在Read Uncommitted隔离级别基础上所做的限定更进一步, 在该隔离级别下,一个事务的更新操作结果只有在该事务提交之后,另一个事务才可能读取到同一笔数据更新后的结果。 所以,Read Committed可以避免Read Uncommitted隔离级别下存在的脏读问题, 但无法避免不可重复读取和幻读的问题。

  • 可重复读取(Repeatable Read):Repeatable Read隔离级别可以保证在整个事务的过程中,对同一笔数据的读取结果是相同的,不管其他事务是否同时在对同一笔数据进行更新,也不管其他事务对同一笔数据的更新提交与否。 Repeatable Read隔离级别避免了脏读和不可重复读取的问题,但无法避免幻读。(mysql默认隔离级别)

  • 序列化(Serializable):最为严格的隔离级别,所有的事务操作都必须依次顺序执行,可以避免其他隔离级别遇到的所有问题,是最为安全的隔离级别, 但同时也是性能最差的隔离级别,因为所有的事务在该隔离级别下都需要依次顺序执行,所以,并发度下降,吞吐量上不去,性能自然就下来了。 因为该隔离级别极大的影响系统性能,所以,很少场景会使用它。通常情况下,我们会使用其他隔离级别加上相应的并发锁的机制来控制对数据的访问,这样既保证了系统性能不会损失太大,也能够一定程度上保证数据的一致性。

2. @Transactional

Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多。声明式事务有两种方式,一种是在配置文件中做相关的事务规则声明,另一种是基于@Transactional 注解的方式。
使用@Transactional的相比传统的我们需要手动开启事务,然后提交事务来说。它提供如下方便:

  • 根据你的配置,设置是否自动开启事务
  • 自动提交事务或者遇到异常自动回滚

由于在Spring boot中使用到的是mybatis,会自动配置一个DataSourceTransactionManager,所以我们只需在方法(或者类)加上 @Transactional注解,就自动纳入 Spring 的事务管理了。

2.1 @Transactional使用

@Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。

虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或者接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用@Transactional 注解,这将被忽略,也不会抛出任何异常。

默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰:

/* * 情况一:都有事务注解,异常在子方法出现,事务生效 */
@Override
@Transactional
public Long addBook(Book book) {
   
    Long result = add(book);
    return result;
}

@Transactional
public Long add(Book book){
   
    Long result =  bookDao.addBook(book);
    int i = 1/0;
    return result;
}

/* * 情况二:都有事务注解,异常在主方法出现,事务生效 */
@Override
@Transactional
public Long addBook(Book book) {
   
    Long result = add(book);
    int i = 1/0;
    return result;
}

@Transactional
public Long add(Book book){
   
    Long result =  bookDao.addBook(book);
    return result;
}

/* * 情况三:只有主方法有事务注解,异常在子方法出现,事务生效 */
@Override
@Transactional
public Long addBook(Book book) {
   
    Long result = add(book);
    return result;
}

public Long add(Book book){
   
    Long result =  bookDao.addBook(book);
    int i = 1/0;
    return result;
}

/* * 情况四:只有主方法有事务注解,异常在主方法出现,事务生效 */
@Override
@Transactional
public Long addBook(Book book) {
   
    Long result = add(book);
    int i = 1/0;
    return result;
}

public Long add(Book book){
   
    Long result =  bookDao.addBook(book);
    return result;
}

/* * 情况五:只有子方法有事务注解,异常在子方法出现,事务不生效 */
@Override
public Long addBook(Book book) {
   
    Long result = add(book);
    return result;
}

@Transactional
public Long add(Book book){
   
    Long result =  bookDao.addBook(book);
    int i = 1/0;
    return result;
}
2.2 注意事项

Spring的事务管理默认是针对unchecked exception回滚,也就是默认对Error异常RuntimeException异常以及其子类进行事务回滚,且必须抛出异常(抛出异常之后,事务会自动回滚,数据不会插入到数据库。),若使用try-catch对其异常捕获则不会进行回滚!(Error异常和RuntimeException异常抛出时不需要方法调用throws或try-catch语句));而checked exception 则必须用try语句块进行处理或者把异常交给上级方法处理总之就是必须写代码处理它。

但是我们平时做业务处理时,需要捕获异常,所以可以手动抛出RuntimeException异常或者添加rollbackFor = Exception.class(也可以指定相应异常)来解决这个问题。

/* * 捕获异常时,要想使事务生效,需要手动抛出RuntimeException异常或者添加rollbackFor = Exception.class */
@Override
@Transactional
public Long addBook(Book book) {
   
    Long result = null;
    try {
   
        result = bookDao.addBook(book);
        int i = 1/0;
    } catch (Exception e) {
   
        e.printStackTrace();
        throw new RuntimeException();
    }
    return result;
}

@Override
@Transactional(rollbackFor = Exception.class)
public Long addBook(Book book) {
   
    Long result = null;
    try {
   
        result = bookDao.addBook(book);
        int i = 1/0;
    } catch (Exception e) {
   
        e.printStackTrace();
        throw e;
    }
    return result;
}

示例代码参考:https://blog.csdn.net/u013929527/article/details/102596243

2.3 @Transactional注解属性介绍及使用

value 和 transactionManager 属性:

它们两个是一样的意思。当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器。

propagation 属性:

事务的传播行为,默认值为 Propagation.REQUIRED。可选的值有:

  • Propagation.REQUIRED:
    如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。如a方法和b方法都添加了注解,使用默认传播模式,则a方法内部调用b方法,会把两个方法的事务合并为一个事务。
    这里又会存在问题,如果b方法内部抛了异常,而a方法catch了b方法的异常,那这个事务还能正常运行吗?答案是不行!会抛出异常org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only,因为当ServiceB中抛出了一个异常以后,ServiceB会把当前的transaction标记为需要rollback。但是ServiceA中捕获了这个异常,并进行了处理,认为当前transaction应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException。
  • Propagation.SUPPORTS:
    如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
    Propagation.MANDATORY:
    如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
  • Propagation.REQUIRES_NEW:
    重新创建一个新的事务,如果当前存在事务,暂停当前的事务。这个属性可以实现:类A中的a方法加上默认注解@Transactional(propagation = Propagation.REQUIRED),类B中的b方法加上注解@Transactional(propagation = Propagation.REQUIRES_NEW),然后在a方法中调用b方法操作数据库,再在a方法最后抛出异常,会发现a方法中的b方法对数据库的操作没有回滚,因为Propagation.REQUIRES_NEW会暂停a方法的事务。
  • Propagation.NOT_SUPPORTED:
    以非事务的方式运行,如果当前存在事务,暂停当前的事务。
  • Propagation.NEVER:
    以非事务的方式运行,如果当前存在事务,则抛出异常。
  • Propagation.NESTED:
    和 Propagation.REQUIRED 效果一样。

isolation 属性:

事务的隔离级别,默认值为 Isolation.DEFAULT。可选的值有:

  • Isolation.DEFAULT:使用底层数据库默认的隔离级别。
  • Isolation.READ_UNCOMMITTED
  • Isolation.READ_COMMITTED
  • Isolation.REPEATABLE_READ
  • Isolation.SERIALIZABLE

timeout 属性:

事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。

readOnly 属性:

指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。

rollbackFor 属性:

用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。

noRollbackFor 属性:

抛出指定的异常类型,不回滚事务,也可以指定多个异常类型。

参考文章:https://blog.csdn.net/Abysscarry/article/details/80189232

2.4 @Transactional使用建议
  • 一个功能是否要事务,必须纳入设计、编码考虑。不能仅仅完成了基本功能就ok。
  • 如果加了事务,必须做好开发环境测试(测试环境也尽量触发异常、测试回滚),确保事务生效。
  • 不要在接口上声明@Transactional ,而要在具体类的方法上使用 @Transactional 注解,否则注解可能无效。
  • 不要图省事,将@Transactional放置在类级的声明中,放在类声明,会使得所有方法都有事务。故@Transactional应该放在方法级别,不需要使用事务的方法,就不要放置事务,比如查询方法。否则对性能是有影响的。
  • 最后有个关键的一点:和锁同时使用需要注意:由于Spring事务是通过AOP实现的,所以在方法执行之前会有开启事务,之后会有提交事务逻辑。而synchronized代码块执行是在事务之内执行的,可以推断在synchronized代码块执行完时,事务还未提交,其他线程进入synchronized代码块后,读取的数据不是最新的。
    所以必须使synchronized锁的范围大于事务控制的范围,把synchronized加到Controller层或者大于事务边界的调用层!
本文来源MrKorbin,由架构君转载发布,观点不代表Java架构师必看的立场,转载请标明来源出处:https://javajgs.com/archives/25291

发表评论